Browse Source

Merge pull request #427 from LykosAI/main

v2.8.0
pull/430/head v2.8.0
JT 10 months ago committed by GitHub
parent
commit
f0dfa05d27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 198
      .github/workflows/release.yml
  2. 1
      .gitignore
  3. 18
      Avalonia.Gif/Avalonia.Gif.csproj
  4. 10
      Avalonia.Gif/BgWorkerCommand.cs
  5. 12
      Avalonia.Gif/BgWorkerState.cs
  6. 10
      Avalonia.Gif/Decoding/BlockTypes.cs
  7. 8
      Avalonia.Gif/Decoding/ExtensionType.cs
  8. 10
      Avalonia.Gif/Decoding/FrameDisposal.cs
  9. 36
      Avalonia.Gif/Decoding/GifColor.cs
  10. 653
      Avalonia.Gif/Decoding/GifDecoder.cs
  11. 20
      Avalonia.Gif/Decoding/GifFrame.cs
  12. 19
      Avalonia.Gif/Decoding/GifHeader.cs
  13. 43
      Avalonia.Gif/Decoding/GifRect.cs
  14. 8
      Avalonia.Gif/Decoding/GifRepeatBehavior.cs
  15. 23
      Avalonia.Gif/Decoding/InvalidGifStreamException.cs
  16. 23
      Avalonia.Gif/Decoding/LzwDecompressionException.cs
  17. 81
      Avalonia.Gif/Extensions/StreamExtensions.cs
  18. 297
      Avalonia.Gif/GifImage.cs
  19. 147
      Avalonia.Gif/GifInstance.cs
  20. 15
      Avalonia.Gif/IGifInstance.cs
  21. 20
      Avalonia.Gif/InvalidGifStreamException.cs
  22. 180
      Avalonia.Gif/WebpInstance.cs
  23. 8
      Build/AppEntitlements.entitlements
  24. 12
      Build/EmbeddedEntitlements.entitlements
  25. 26
      Build/build_macos_app.sh
  26. 62
      Build/codesign_embedded_macos.sh
  27. 37
      Build/codesign_macos.sh
  28. 32
      Build/notarize_macos.sh
  29. 165
      CHANGELOG.md
  30. 10
      README.md
  31. 13
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  32. 5
      StabilityMatrix.Avalonia/App.axaml
  33. 333
      StabilityMatrix.Avalonia/App.axaml.cs
  34. 28
      StabilityMatrix.Avalonia/Assets.cs
  35. BIN
      StabilityMatrix.Avalonia/Assets/AppIcon.icns
  36. 141
      StabilityMatrix.Avalonia/Collections/SearchCollection.cs
  37. 59
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
  38. 13
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs
  39. 38
      StabilityMatrix.Avalonia/Controls/BetterComboBox.cs
  40. 64
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  41. 46
      StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs
  42. 113
      StabilityMatrix.Avalonia/Controls/HyperlinkIconButton.cs
  43. 2
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  44. 159
      StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
  45. 2
      StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml
  46. 2
      StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
  47. 6
      StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml
  48. 2
      StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml
  49. 4
      StabilityMatrix.Avalonia/Controls/StarsRating.axaml
  50. 15
      StabilityMatrix.Avalonia/Controls/StarsRating.axaml.cs
  51. 122
      StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
  52. 7
      StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml.cs
  53. 98
      StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
  54. 7
      StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml.cs
  55. 40
      StabilityMatrix.Avalonia/Converters/EnumAttributeConverter.cs
  56. 13
      StabilityMatrix.Avalonia/Converters/EnumAttributeConverters.cs
  57. 36
      StabilityMatrix.Avalonia/Converters/FileUriConverter.cs
  58. 53
      StabilityMatrix.Avalonia/Converters/FuncCommandConverter.cs
  59. 250
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  60. 22
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  61. 49
      StabilityMatrix.Avalonia/DialogHelper.cs
  62. 67
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  63. 20
      StabilityMatrix.Avalonia/Extensions/NotificationLevelExtensions.cs
  64. 54
      StabilityMatrix.Avalonia/Extensions/NotificationServiceExtensions.cs
  65. 76
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  66. 17
      StabilityMatrix.Avalonia/Helpers/UriHandler.cs
  67. 113
      StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
  68. 12
      StabilityMatrix.Avalonia/Languages/Cultures.cs
  69. 191
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  70. 933
      StabilityMatrix.Avalonia/Languages/Resources.de.resx
  71. 103
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  72. 245
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  73. 57
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  74. 933
      StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx
  75. 65
      StabilityMatrix.Avalonia/Languages/Resources.resx
  76. 187
      StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx
  77. 33
      StabilityMatrix.Avalonia/Models/CommandItem.cs
  78. 11
      StabilityMatrix.Avalonia/Models/ITemplateKey.cs
  79. 79
      StabilityMatrix.Avalonia/Models/ImageSource.cs
  80. 8
      StabilityMatrix.Avalonia/Models/ImageSourceTemplateType.cs
  81. 15
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  82. 11
      StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs
  83. 2
      StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs
  84. 4
      StabilityMatrix.Avalonia/Models/InferenceProjectType.cs
  85. 10
      StabilityMatrix.Avalonia/Models/PackageManagerNavigationOptions.cs
  86. 52
      StabilityMatrix.Avalonia/Models/SelectableItem.cs
  87. 66
      StabilityMatrix.Avalonia/Program.cs
  88. 7
      StabilityMatrix.Avalonia/Services/INavigationService.cs
  89. 20
      StabilityMatrix.Avalonia/Services/INotificationService.cs
  90. 48
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  91. 178
      StabilityMatrix.Avalonia/Services/NotificationService.cs
  92. 92
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  93. 254
      StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
  94. 48
      StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml
  95. 50
      StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml
  96. 37
      StabilityMatrix.Avalonia/Styles/TextBoxStyles.axaml
  97. 230
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  98. 54
      StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
  99. 62
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  100. 57
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  101. Some files were not shown because too many files have changed in this diff Show More

198
.github/workflows/release.yml

@ -43,6 +43,10 @@ on:
- stable
- preview
- development
test-release-artifacts:
type: boolean
description: "[Debug] Test release artifacts?"
default: false
jobs:
release-linux:
@ -171,11 +175,122 @@ jobs:
with:
name: StabilityMatrix-${{ env.platform-id }}
path: ./out/${{ env.out-name }}
release-macos:
name: Release (macos-arm64)
env:
platform-id: osx-arm64
app-name: "Stability Matrix.app"
out-name: "StabilityMatrix-macos-arm64.dmg"
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: olegtarasov/get-tag@v2.1.2
if: github.event_name == 'release'
id: tag_name
with:
tagRegex: "v(.*)"
- name: Set Version from Tag
if: github.event_name == 'release'
run: |
echo "Using tag ${{ env.GIT_TAG_NAME }}"
echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $GITHUB_ENV
- name: Set Version from manual input
if: github.event_name == 'workflow_dispatch'
run: |
echo "Using version ${{ github.event.inputs.version }}"
echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
- name: Set up .NET 8
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Install dependencies
run: dotnet restore -p:PublishReadyToRun=true
- name: Check Version
run: echo $RELEASE_VERSION
- name: .NET Msbuild (App)
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: >
dotnet msbuild ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
-t:BundleApp -p:UseAppHost=true -p:SelfContained=true
-p:Configuration=Release -p:RuntimeIdentifier=${{ env.platform-id }}
-p:Version=$RELEASE_VERSION
-p:PublishDir=out
-p:PublishReadyToRun=true
-p:CFBundleShortVersionString=$RELEASE_VERSION
-p:CFBundleName="Stability Matrix"
-p:CFBundleDisplayName="Stability Matrix"
-p:CFBundleVersion=$RELEASE_VERSION
-p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }}
-p:SentryUploadSymbols=true -p:SentryUploadSources=true
- name: Post Build (App)
run: mkdir -p signing && mv "./StabilityMatrix.Avalonia/out/Stability Matrix.app" "./signing/${{ env.app-name }}"
- name: Codesign app bundle
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: ./Build/codesign_macos.sh "./signing/${{ env.app-name }}"
- name: Notarize app bundle
env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: ./Build/notarize_macos.sh "./signing/${{ env.app-name }}"
- name: Zip Artifact (App)
working-directory: signing
run: zip -r -y "../StabilityMatrix-${{ env.platform-id }}-app.zip" "${{ env.app-name }}"
- name: Upload Artifact (App)
uses: actions/upload-artifact@v2
with:
name: StabilityMatrix-${{ env.platform-id }}-app
path: StabilityMatrix-${{ env.platform-id }}-app.zip
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies for dmg creation
run: >
npm install --global create-dmg
brew install graphicsmagick imagemagick
- name: Create dmg
working-directory: signing
run: >
create-dmg "${{ env.app-name }}" --overwrite --identity "${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}"
- name: Rename dmg
working-directory: signing
run: mv "$(find . -type f -name "*.dmg")" "${{ env.out-name }}"
- name: Zip Artifact (dmg)
working-directory: signing
run: zip -r -y "../StabilityMatrix-${{ env.platform-id }}-dmg.zip" "${{ env.out-name }}"
- name: Upload Artifact (dmg)
uses: actions/upload-artifact@v2
with:
name: StabilityMatrix-${{ env.platform-id }}-dmg
path: StabilityMatrix-${{ env.platform-id }}-dmg.zip
publish-release:
name: Publish GitHub Release
needs: [ release-linux, release-windows ]
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.github-release == 'true' }}
runs-on: ubuntu-latest
steps:
@ -194,11 +309,12 @@ jobs:
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build
# Zip each build (except macos which is already dmg)
- name: Zip Artifacts
run: |
cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD
cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD
unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip"
- name: Create Github Release
id: create_release
@ -209,15 +325,75 @@ jobs:
files: |
StabilityMatrix-win-x64.zip
StabilityMatrix-linux-x64.zip
StabilityMatrix-macos-arm64.dmg
fail_on_unmatched_files: true
tag_name: v${{ github.event.inputs.version }}
body: ${{ steps.release_notes.outputs.release_notes }}
draft: ${{ github.event.inputs.github-release-draft == 'true' }}
prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }}
test-artifacts:
name: Test Release Artifacts
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test-release-artifacts == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract Release Notes
id: release_notes
run: |
RELEASE_NOTES="$(awk -v version="${{ github.event.inputs.version }}" '/## v/{if(p) exit; if($0 ~ version) p=1}; p' CHANGELOG.md)"
RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}"
RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}"
RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}"
echo "::set-output name=release_notes::$RELEASE_NOTES"
echo "Release Notes:"
echo "$RELEASE_NOTES"
# Downloads all previous artifacts to the current working directory
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build (except macos which is already dmg)
- name: Zip Artifacts
run: |
cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD
cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD
unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip"
# Check that the zips and CHANGELOG.md are in the current working directory
- name: Check files
run: |
if [ ! -f StabilityMatrix-win-x64.zip ]; then
echo "StabilityMatrix-win-x64.zip not found"
exit 1
else
echo "StabilityMatrix-win-x64.zip found"
sha256sum StabilityMatrix-win-x64.zip
fi
if [ ! -f StabilityMatrix-linux-x64.zip ]; then
echo "StabilityMatrix-linux-x64.zip not found"
exit 1
else
echo "StabilityMatrix-linux-x64.zip found"
sha256sum StabilityMatrix-linux-x64.zip
fi
if [ ! -f StabilityMatrix-macos-arm64.dmg ]; then
echo "StabilityMatrix-macos-arm64.dmg not found"
exit 1
else
echo "StabilityMatrix-macos-arm64.dmg found"
sha256sum StabilityMatrix-macos-arm64.dmg
fi
if [ ! -f CHANGELOG.md ]; then
echo "CHANGELOG.md not found"
exit 1
fi
publish-auto-update-github:
name: Publish Auto-Update Release (GitHub)
needs: [ release-linux, release-windows ]
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.auto-update-release == 'true' && github.event.inputs.auto-update-release-mode == 'github url' }}
runs-on: ubuntu-latest
steps:
@ -233,7 +409,7 @@ jobs:
python-version: '3.11'
- name: Install Python Dependencies
run: pip install stability-matrix-tools>=0.2.18 --upgrade
run: pip install stability-matrix-tools>=0.3.0 --upgrade
- name: Publish Auto-Update Release
env:
@ -246,7 +422,7 @@ jobs:
publish-auto-update-b2:
name: Publish Auto-Update Release (B2)
needs: [ release-linux, release-windows ]
needs: [ release-linux, release-windows, release-macos ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.auto-update-release == 'true' && github.event.inputs.auto-update-release-mode == 'upload to b2' }}
runs-on: ubuntu-latest
steps:
@ -261,18 +437,19 @@ jobs:
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build
# Zip each build (except macos which is already dmg)
- name: Zip Artifacts
run: |
cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD
cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD
unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip"
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Python Dependencies
run: pip install stability-matrix-tools>=0.2.18 --upgrade
run: pip install stability-matrix-tools>=0.3.0 --upgrade
# Check that the zips and CHANGELOG.md are in the current working directory
- name: Check files
@ -285,6 +462,10 @@ jobs:
echo "StabilityMatrix-linux-x64.zip not found"
exit 1
fi
if [ ! -f StabilityMatrix-macos-arm64.dmg ]; then
echo "StabilityMatrix-macos-arm64.dmg not found"
exit 1
fi
if [ ! -f CHANGELOG.md ]; then
echo "CHANGELOG.md not found"
exit 1
@ -303,4 +484,5 @@ jobs:
--changelog CHANGELOG.md
--win-x64 StabilityMatrix-win-x64.zip
--linux-x64 StabilityMatrix-linux-x64.zip
--macos-arm64 StabilityMatrix-macos-arm64.dmg
-y

1
.gitignore vendored

@ -397,3 +397,4 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
.husky/pre-commit

18
Avalonia.Gif/Avalonia.Gif.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.7" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>
</Project>

10
Avalonia.Gif/BgWorkerCommand.cs

@ -0,0 +1,10 @@
namespace Avalonia.Gif
{
internal enum BgWorkerCommand
{
Null,
Play,
Pause,
Dispose
}
}

12
Avalonia.Gif/BgWorkerState.cs

@ -0,0 +1,12 @@
namespace Avalonia.Gif
{
internal enum BgWorkerState
{
Null,
Start,
Running,
Paused,
Complete,
Dispose
}
}

10
Avalonia.Gif/Decoding/BlockTypes.cs

@ -0,0 +1,10 @@
namespace Avalonia.Gif.Decoding
{
internal enum BlockTypes
{
Empty = 0,
Extension = 0x21,
ImageDescriptor = 0x2C,
Trailer = 0x3B,
}
}

8
Avalonia.Gif/Decoding/ExtensionType.cs

@ -0,0 +1,8 @@
namespace Avalonia.Gif.Decoding
{
internal enum ExtensionType
{
GraphicsControl = 0xF9,
Application = 0xFF
}
}

10
Avalonia.Gif/Decoding/FrameDisposal.cs

@ -0,0 +1,10 @@
namespace Avalonia.Gif.Decoding
{
public enum FrameDisposal
{
Unknown = 0,
Leave = 1,
Background = 2,
Restore = 3
}
}

36
Avalonia.Gif/Decoding/GifColor.cs

@ -0,0 +1,36 @@
using System.Runtime.InteropServices;
namespace Avalonia.Gif
{
[StructLayout(LayoutKind.Explicit)]
public readonly struct GifColor
{
[FieldOffset(3)]
public readonly byte A;
[FieldOffset(2)]
public readonly byte R;
[FieldOffset(1)]
public readonly byte G;
[FieldOffset(0)]
public readonly byte B;
/// <summary>
/// A struct that represents a ARGB color and is aligned as
/// a BGRA bytefield in memory.
/// </summary>
/// <param name="r">Red</param>
/// <param name="g">Green</param>
/// <param name="b">Blue</param>
/// <param name="a">Alpha</param>
public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue)
{
A = a;
R = r;
G = g;
B = b;
}
}
}

653
Avalonia.Gif/Decoding/GifDecoder.cs

@ -0,0 +1,653 @@
// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer
// as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer)
// Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
// Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved.
// The rest of the source file is licensed under MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Avalonia;
using Avalonia.Media.Imaging;
using static Avalonia.Gif.Extensions.StreamExtensions;
namespace Avalonia.Gif.Decoding
{
public sealed class GifDecoder : IDisposable
{
private static readonly ReadOnlyMemory<byte> G87AMagic = "GIF87a"u8.ToArray().AsMemory();
private static readonly ReadOnlyMemory<byte> G89AMagic = "GIF89a"u8.ToArray().AsMemory();
private static readonly ReadOnlyMemory<byte> NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory();
private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10);
private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100);
private static readonly GifColor TransparentColor = new(0, 0, 0, 0);
private static readonly int MaxTempBuf = 768;
private static readonly int MaxStackSize = 4096;
private static readonly int MaxBits = 4097;
private readonly Stream _fileStream;
private readonly CancellationToken _currentCtsToken;
private readonly bool _hasFrameBackups;
private int _gctSize,
_bgIndex,
_prevFrame = -1,
_backupFrame = -1;
private bool _gctUsed;
private GifRect _gifDimensions;
// private ulong _globalColorTable;
private readonly int _backBufferBytes;
private GifColor[] _bitmapBackBuffer;
private short[] _prefixBuf;
private byte[] _suffixBuf;
private byte[] _pixelStack;
private byte[] _indexBuf;
private byte[] _backupFrameIndexBuf;
private volatile bool _hasNewFrame;
public GifHeader Header { get; private set; }
public readonly List<GifFrame> Frames = new();
public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height);
public GifDecoder(Stream fileStream, CancellationToken currentCtsToken)
{
_fileStream = fileStream;
_currentCtsToken = currentCtsToken;
ProcessHeaderData();
ProcessFrameData();
Header.IterationCount = Header.Iterations switch
{
-1 => new GifRepeatBehavior { Count = 1 },
0 => new GifRepeatBehavior { LoopForever = true },
> 0 => new GifRepeatBehavior { Count = Header.Iterations },
_ => Header.IterationCount
};
var pixelCount = _gifDimensions.TotalPixels;
_hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore);
_bitmapBackBuffer = new GifColor[pixelCount];
_indexBuf = new byte[pixelCount];
if (_hasFrameBackups)
_backupFrameIndexBuf = new byte[pixelCount];
_prefixBuf = new short[MaxStackSize];
_suffixBuf = new byte[MaxStackSize];
_pixelStack = new byte[MaxStackSize + 1];
_backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor));
}
public void Dispose()
{
Frames.Clear();
_bitmapBackBuffer = null;
_prefixBuf = null;
_suffixBuf = null;
_pixelStack = null;
_indexBuf = null;
_backupFrameIndexBuf = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int PixCoord(int x, int y) => x + y * _gifDimensions.Width;
static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) };
private void ClearImage()
{
Array.Fill(_bitmapBackBuffer, TransparentColor);
//ClearArea(_gifDimensions);
_prevFrame = -1;
_backupFrame = -1;
}
public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false)
{
if (_currentCtsToken.IsCancellationRequested)
return;
if (fIndex < 0 | fIndex >= Frames.Count)
return;
if (_prevFrame == fIndex)
return;
if (fIndex == 0 || forceClear || fIndex < _prevFrame)
ClearImage();
DisposePreviousFrame();
_prevFrame++;
// render intermediate frame
for (int idx = _prevFrame; idx < fIndex; ++idx)
{
var prevFrame = Frames[idx];
if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore)
continue;
if (prevFrame.FrameDisposalMethod == FrameDisposal.Background)
{
ClearArea(prevFrame.Dimensions);
continue;
}
RenderFrameAt(idx, writeableBitmap);
}
RenderFrameAt(fIndex, writeableBitmap);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap)
{
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
var curFrame = Frames[idx];
DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB);
if (_hasFrameBackups & curFrame.ShouldBackup)
{
Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels);
_backupFrame = idx;
}
DrawFrame(curFrame, _indexBuf);
_prevFrame = idx;
_hasNewFrame = true;
using var lockedBitmap = writeableBitmap.Lock();
WriteBackBufToFb(lockedBitmap.Address);
ArrayPool<byte>.Shared.Return(tmpB);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrawFrame(GifFrame curFrame, Memory<byte> frameIndexSpan)
{
var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable;
var cX = curFrame.Dimensions.X;
var cY = curFrame.Dimensions.Y;
var cH = curFrame.Dimensions.Height;
var cW = curFrame.Dimensions.Width;
var tC = curFrame.TransparentColorIndex;
var hT = curFrame.HasTransparency;
if (curFrame.IsInterlaced)
{
for (var i = 0; i < 4; i++)
{
var curPass = Pass[i];
var y = curPass.Start;
while (y < cH)
{
DrawRow(y);
y += curPass.Step;
}
}
}
else
{
for (var i = 0; i < cH; i++)
DrawRow(i);
}
//for (var row = 0; row < cH; row++)
void DrawRow(int row)
{
// Get the starting point of the current row on frame's index stream.
var indexOffset = row * cW;
// Get the target backbuffer offset from the frames coords.
var targetOffset = PixCoord(cX, row + cY);
var len = _bitmapBackBuffer.Length;
for (var i = 0; i < cW; i++)
{
var indexColor = frameIndexSpan.Span[indexOffset + i];
if (activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length)
return;
if (!(hT & indexColor == tC))
_bitmapBackBuffer[targetOffset] = activeColorTable[indexColor];
targetOffset++;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DisposePreviousFrame()
{
if (_prevFrame == -1)
return;
var prevFrame = Frames[_prevFrame];
switch (prevFrame.FrameDisposalMethod)
{
case FrameDisposal.Background:
ClearArea(prevFrame.Dimensions);
break;
case FrameDisposal.Restore:
if (_hasFrameBackups && _backupFrame != -1)
DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf);
else
ClearArea(prevFrame.Dimensions);
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearArea(GifRect area)
{
for (var y = 0; y < area.Height; y++)
{
var targetOffset = PixCoord(area.X, y + area.Y);
for (var x = 0; x < area.Width; x++)
_bitmapBackBuffer[targetOffset + x] = TransparentColor;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span<byte> indexSpan, byte[] tempBuf)
{
_fileStream.Position = curFrame.LzwStreamPosition;
var totalPixels = curFrame.Dimensions.TotalPixels;
// Initialize GIF data stream decoder.
var dataSize = curFrame.LzwMinCodeSize;
var clear = 1 << dataSize;
var endOfInformation = clear + 1;
var available = clear + 2;
var oldCode = -1;
var codeSize = dataSize + 1;
var codeMask = (1 << codeSize) - 1;
for (var code = 0; code < clear; code++)
{
_prefixBuf[code] = 0;
_suffixBuf[code] = (byte)code;
}
// Decode GIF pixel stream.
int bits,
first,
top,
pixelIndex;
var datum = bits = first = top = pixelIndex = 0;
while (pixelIndex < totalPixels)
{
var blockSize = _fileStream.ReadBlock(tempBuf);
if (blockSize == 0)
break;
var blockPos = 0;
while (blockPos < blockSize)
{
datum += tempBuf[blockPos] << bits;
blockPos++;
bits += 8;
while (bits >= codeSize)
{
// Get the next code.
var code = datum & codeMask;
datum >>= codeSize;
bits -= codeSize;
// Interpret the code
if (code == clear)
{
// Reset decoder.
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
available = clear + 2;
oldCode = -1;
continue;
}
// Check for explicit end-of-stream
if (code == endOfInformation)
return;
if (oldCode == -1)
{
indexSpan[pixelIndex++] = _suffixBuf[code];
oldCode = code;
first = code;
continue;
}
var inCode = code;
if (code >= available)
{
_pixelStack[top++] = (byte)first;
code = oldCode;
if (top == MaxBits)
ThrowException();
}
while (code >= clear)
{
if (code >= MaxBits || code == _prefixBuf[code])
ThrowException();
_pixelStack[top++] = _suffixBuf[code];
code = _prefixBuf[code];
if (top == MaxBits)
ThrowException();
}
first = _suffixBuf[code];
_pixelStack[top++] = (byte)first;
// Add new code to the dictionary
if (available < MaxStackSize)
{
_prefixBuf[available] = (short)oldCode;
_suffixBuf[available] = (byte)first;
available++;
if ((available & codeMask) == 0 && available < MaxStackSize)
{
codeSize++;
codeMask += available;
}
}
oldCode = inCode;
// Drain the pixel stack.
do
{
indexSpan[pixelIndex++] = _pixelStack[--top];
} while (top > 0);
}
}
}
while (pixelIndex < totalPixels)
indexSpan[pixelIndex++] = 0; // clear missing pixels
void ThrowException() => throw new LzwDecompressionException();
}
/// <summary>
/// Directly copies the <see cref="GifColor"/> struct array to a bitmap IntPtr.
/// </summary>
private void WriteBackBufToFb(IntPtr targetPointer)
{
if (_currentCtsToken.IsCancellationRequested)
return;
if (!(_hasNewFrame & _bitmapBackBuffer != null))
return;
unsafe
{
fixed (void* src = &_bitmapBackBuffer[0])
Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes);
_hasNewFrame = false;
}
}
/// <summary>
/// Processes GIF Header.
/// </summary>
private void ProcessHeaderData()
{
var str = _fileStream;
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
var tempBuf = tmpB.AsSpan();
var _ = str.Read(tmpB, 0, 6);
if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span))
throw new InvalidGifStreamException("Not a GIF stream.");
if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span)))
throw new InvalidGifStreamException(
"Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray())
);
ProcessScreenDescriptor(tmpB);
Header = new GifHeader
{
Dimensions = _gifDimensions,
HasGlobalColorTable = _gctUsed,
// GlobalColorTableCacheID = _globalColorTable,
GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize),
GlobalColorTableSize = _gctSize,
BackgroundColorIndex = _bgIndex,
HeaderSize = _fileStream.Position
};
ArrayPool<byte>.Shared.Return(tmpB);
}
/// <summary>
/// Parses colors from file stream to target color table.
/// </summary>
private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors)
{
var nBytes = 3 * nColors;
var target = new GifColor[nColors];
var n = stream.Read(rawBufSpan, 0, nBytes);
if (n < nBytes)
throw new InvalidOperationException("Wrong color table bytes.");
int i = 0,
j = 0;
while (i < nColors)
{
var r = rawBufSpan[j++];
var g = rawBufSpan[j++];
var b = rawBufSpan[j++];
target[i++] = new GifColor(r, g, b);
}
return target;
}
/// <summary>
/// Parses screen and other GIF descriptors.
/// </summary>
private void ProcessScreenDescriptor(byte[] tempBuf)
{
var width = _fileStream.ReadUShortS(tempBuf);
var height = _fileStream.ReadUShortS(tempBuf);
var packed = _fileStream.ReadByteS(tempBuf);
_gctUsed = (packed & 0x80) != 0;
_gctSize = 2 << (packed & 7);
_bgIndex = _fileStream.ReadByteS(tempBuf);
_gifDimensions = new GifRect(0, 0, width, height);
_fileStream.Skip(1);
}
/// <summary>
/// Parses all frame data.
/// </summary>
private void ProcessFrameData()
{
_fileStream.Position = Header.HeaderSize;
var tempBuf = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
var terminate = false;
var curFrame = 0;
Frames.Add(new GifFrame());
do
{
var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf);
switch (blockType)
{
case BlockTypes.Empty:
break;
case BlockTypes.Extension:
ProcessExtensions(ref curFrame, tempBuf);
break;
case BlockTypes.ImageDescriptor:
ProcessImageDescriptor(ref curFrame, tempBuf);
_fileStream.SkipBlocks(tempBuf);
break;
case BlockTypes.Trailer:
Frames.RemoveAt(Frames.Count - 1);
terminate = true;
break;
default:
_fileStream.SkipBlocks(tempBuf);
break;
}
// Break the loop when the stream is not valid anymore.
if (_fileStream.Position >= _fileStream.Length & terminate == false)
throw new InvalidProgramException("Reach the end of the filestream without trailer block.");
} while (!terminate);
ArrayPool<byte>.Shared.Return(tempBuf);
}
/// <summary>
/// Parses GIF Image Descriptor Block.
/// </summary>
private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf)
{
var str = _fileStream;
var currentFrame = Frames[curFrame];
// Parse frame dimensions.
var frameX = str.ReadUShortS(tempBuf);
var frameY = str.ReadUShortS(tempBuf);
var frameW = str.ReadUShortS(tempBuf);
var frameH = str.ReadUShortS(tempBuf);
frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX);
frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY);
currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH);
// Unpack interlace and lct info.
var packed = str.ReadByteS(tempBuf);
currentFrame.IsInterlaced = (packed & 0x40) != 0;
currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0;
currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1);
if (currentFrame.IsLocalColorTableUsed)
currentFrame.LocalColorTable = ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize);
currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf);
currentFrame.LzwStreamPosition = str.Position;
curFrame += 1;
Frames.Add(new GifFrame());
}
/// <summary>
/// Parses GIF Extension Blocks.
/// </summary>
private void ProcessExtensions(ref int curFrame, byte[] tempBuf)
{
var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf);
switch (extType)
{
case ExtensionType.GraphicsControl:
_fileStream.ReadBlock(tempBuf);
var currentFrame = Frames[curFrame];
var packed = tempBuf[0];
currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2);
if (
currentFrame.FrameDisposalMethod != FrameDisposal.Restore
&& currentFrame.FrameDisposalMethod != FrameDisposal.Background
)
currentFrame.ShouldBackup = true;
currentFrame.HasTransparency = (packed & 1) != 0;
currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10);
if (currentFrame.FrameDelay <= FrameDelayThreshold)
currentFrame.FrameDelay = FrameDelayDefault;
currentFrame.TransparentColorIndex = tempBuf[3];
break;
case ExtensionType.Application:
var blockLen = _fileStream.ReadBlock(tempBuf);
var _ = tempBuf.AsSpan(0, blockLen);
var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length);
if (blockHeader.SequenceEqual(NetscapeMagic.Span))
{
var count = 1;
while (count > 0)
count = _fileStream.ReadBlock(tempBuf);
var iterationCount = SpanToShort(tempBuf.AsSpan(1));
Header.Iterations = iterationCount;
}
else
_fileStream.SkipBlocks(tempBuf);
break;
default:
_fileStream.SkipBlocks(tempBuf);
break;
}
}
}
}

20
Avalonia.Gif/Decoding/GifFrame.cs

@ -0,0 +1,20 @@
using System;
namespace Avalonia.Gif.Decoding
{
public class GifFrame
{
public bool HasTransparency,
IsInterlaced,
IsLocalColorTableUsed;
public byte TransparentColorIndex;
public int LzwMinCodeSize,
LocalColorTableSize;
public long LzwStreamPosition;
public TimeSpan FrameDelay;
public FrameDisposal FrameDisposalMethod;
public bool ShouldBackup;
public GifRect Dimensions;
public GifColor[] LocalColorTable;
}
}

19
Avalonia.Gif/Decoding/GifHeader.cs

@ -0,0 +1,19 @@
// Licensed under the MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
namespace Avalonia.Gif.Decoding
{
public class GifHeader
{
public bool HasGlobalColorTable;
public int GlobalColorTableSize;
public ulong GlobalColorTableCacheId;
public int BackgroundColorIndex;
public long HeaderSize;
internal int Iterations = -1;
public GifRepeatBehavior IterationCount;
public GifRect Dimensions;
private GifColor[] _globarColorTable;
public GifColor[] GlobarColorTable;
}
}

43
Avalonia.Gif/Decoding/GifRect.cs

@ -0,0 +1,43 @@
namespace Avalonia.Gif.Decoding
{
public readonly struct GifRect
{
public int X { get; }
public int Y { get; }
public int Width { get; }
public int Height { get; }
public int TotalPixels { get; }
public GifRect(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
TotalPixels = width * height;
}
public static bool operator ==(GifRect a, GifRect b)
{
return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
}
public static bool operator !=(GifRect a, GifRect b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
return this == (GifRect)obj;
}
public override int GetHashCode()
{
return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode();
}
}
}

8
Avalonia.Gif/Decoding/GifRepeatBehavior.cs

@ -0,0 +1,8 @@
namespace Avalonia.Gif.Decoding
{
public class GifRepeatBehavior
{
public bool LoopForever { get; set; }
public int? Count { get; set; }
}
}

23
Avalonia.Gif/Decoding/InvalidGifStreamException.cs

@ -0,0 +1,23 @@
// Licensed under the MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
using System;
using System.Runtime.Serialization;
namespace Avalonia.Gif.Decoding
{
[Serializable]
public class InvalidGifStreamException : Exception
{
public InvalidGifStreamException() { }
public InvalidGifStreamException(string message)
: base(message) { }
public InvalidGifStreamException(string message, Exception innerException)
: base(message, innerException) { }
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

23
Avalonia.Gif/Decoding/LzwDecompressionException.cs

@ -0,0 +1,23 @@
// Licensed under the MIT License.
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
using System;
using System.Runtime.Serialization;
namespace Avalonia.Gif.Decoding
{
[Serializable]
public class LzwDecompressionException : Exception
{
public LzwDecompressionException() { }
public LzwDecompressionException(string message)
: base(message) { }
public LzwDecompressionException(string message, Exception innerException)
: base(message, innerException) { }
protected LzwDecompressionException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

81
Avalonia.Gif/Extensions/StreamExtensions.cs

@ -0,0 +1,81 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
namespace Avalonia.Gif.Extensions
{
[DebuggerStepThrough]
internal static class StreamExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort SpanToShort(Span<byte> b) => (ushort)(b[0] | (b[1] << 8));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Skip(this Stream stream, long count)
{
stream.Position += count;
}
/// <summary>
/// Read a Gif block from stream while advancing the position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReadBlock(this Stream stream, byte[] tempBuf)
{
stream.Read(tempBuf, 0, 1);
var blockLength = (int)tempBuf[0];
if (blockLength > 0)
stream.Read(tempBuf, 0, blockLength);
// Guard against infinite loop.
if (stream.Position >= stream.Length)
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block.");
return blockLength;
}
/// <summary>
/// Skips GIF blocks until it encounters an empty block.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SkipBlocks(this Stream stream, byte[] tempBuf)
{
int blockLength;
do
{
stream.Read(tempBuf, 0, 1);
blockLength = tempBuf[0];
stream.Position += blockLength;
// Guard against infinite loop.
if (stream.Position >= stream.Length)
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block.");
} while (blockLength > 0);
}
/// <summary>
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort ReadUShortS(this Stream stream, byte[] tempBuf)
{
stream.Read(tempBuf, 0, 2);
return SpanToShort(tempBuf);
}
/// <summary>
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte ReadByteS(this Stream stream, byte[] tempBuf)
{
stream.Read(tempBuf, 0, 1);
var finalVal = tempBuf[0];
return finalVal;
}
}
}

297
Avalonia.Gif/GifImage.cs

@ -0,0 +1,297 @@
using System;
using System.IO;
using System.Numerics;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
namespace Avalonia.Gif
{
public class GifImage : Control
{
public static readonly StyledProperty<string> SourceUriRawProperty = AvaloniaProperty.Register<
GifImage,
string
>("SourceUriRaw");
public static readonly StyledProperty<Uri> SourceUriProperty = AvaloniaProperty.Register<GifImage, Uri>(
"SourceUri"
);
public static readonly StyledProperty<Stream> SourceStreamProperty = AvaloniaProperty.Register<
GifImage,
Stream
>("SourceStream");
public static readonly StyledProperty<IterationCount> IterationCountProperty = AvaloniaProperty.Register<
GifImage,
IterationCount
>("IterationCount", IterationCount.Infinite);
private IGifInstance? _gifInstance;
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty = AvaloniaProperty.Register<
GifImage,
StretchDirection
>("StretchDirection");
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<GifImage, Stretch>(
"Stretch"
);
private CompositionCustomVisual? _customVisual;
private object? _initialSource = null;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
switch (change.Property.Name)
{
case nameof(SourceUriRaw):
case nameof(SourceUri):
case nameof(SourceStream):
SourceChanged(change);
break;
case nameof(Stretch):
case nameof(StretchDirection):
InvalidateArrange();
InvalidateMeasure();
Update();
break;
case nameof(IterationCount):
IterationCountChanged(change);
break;
case nameof(Bounds):
Update();
break;
}
base.OnPropertyChanged(change);
}
public string SourceUriRaw
{
get => GetValue(SourceUriRawProperty);
set => SetValue(SourceUriRawProperty, value);
}
public Uri SourceUri
{
get => GetValue(SourceUriProperty);
set => SetValue(SourceUriProperty, value);
}
public Stream SourceStream
{
get => GetValue(SourceStreamProperty);
set => SetValue(SourceStreamProperty, value);
}
public IterationCount IterationCount
{
get => GetValue(IterationCountProperty);
set => SetValue(IterationCountProperty, value);
}
public StretchDirection StretchDirection
{
get => GetValue(StretchDirectionProperty);
set => SetValue(StretchDirectionProperty, value);
}
public Stretch Stretch
{
get => GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
private static void IterationCountChanged(AvaloniaPropertyChangedEventArgs e)
{
var image = e.Sender as GifImage;
if (image is null || e.NewValue is not IterationCount iterationCount)
return;
image.IterationCount = iterationCount;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
var compositor = ElementComposition.GetElementVisual(this)?.Compositor;
if (compositor == null || _customVisual?.Compositor == compositor)
return;
_customVisual = compositor.CreateCustomVisual(new CustomVisualHandler());
ElementComposition.SetElementChildVisual(this, _customVisual);
_customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage);
if (_initialSource is not null)
{
UpdateGifInstance(_initialSource);
_initialSource = null;
}
Update();
base.OnAttachedToVisualTree(e);
}
private void Update()
{
if (_customVisual is null || _gifInstance is null)
return;
var dpi = this.GetVisualRoot()?.RenderScaling ?? 1.0;
var sourceSize = _gifInstance.GifPixelSize.ToSize(dpi);
var viewPort = new Rect(Bounds.Size);
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
var scaledSize = sourceSize * scale;
var destRect = viewPort.CenterRect(new Rect(scaledSize)).Intersect(viewPort);
if (Stretch == Stretch.None)
{
_customVisual.Size = new Vector2((float)sourceSize.Width, (float)sourceSize.Height);
}
else
{
_customVisual.Size = new Vector2((float)destRect.Size.Width, (float)destRect.Size.Height);
}
_customVisual.Offset = new Vector3((float)destRect.Position.X, (float)destRect.Position.Y, 0);
}
private class CustomVisualHandler : CompositionCustomVisualHandler
{
private TimeSpan _animationElapsed;
private TimeSpan? _lastServerTime;
private IGifInstance? _currentInstance;
private bool _running;
public static readonly object StopMessage = new(),
StartMessage = new();
public override void OnMessage(object message)
{
if (message == StartMessage)
{
_running = true;
_lastServerTime = null;
RegisterForNextAnimationFrameUpdate();
}
else if (message == StopMessage)
{
_running = false;
}
else if (message is IGifInstance instance)
{
_currentInstance?.Dispose();
_currentInstance = instance;
}
}
public override void OnAnimationFrameUpdate()
{
if (!_running)
return;
Invalidate();
RegisterForNextAnimationFrameUpdate();
}
public override void OnRender(ImmediateDrawingContext drawingContext)
{
if (_running)
{
if (_lastServerTime.HasValue)
_animationElapsed += (CompositionNow - _lastServerTime.Value);
_lastServerTime = CompositionNow;
}
try
{
if (_currentInstance is null || _currentInstance.IsDisposed)
return;
var bitmap = _currentInstance.ProcessFrameTime(_animationElapsed);
if (bitmap is not null)
{
drawingContext.DrawBitmap(
bitmap,
new Rect(_currentInstance.GifPixelSize.ToSize(1)),
GetRenderBounds()
);
}
}
catch (Exception e)
{
Logger.Sink?.Log(LogEventLevel.Error, "GifImage Renderer ", this, e.ToString());
}
}
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size availableSize)
{
var result = new Size();
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0;
if (_gifInstance != null)
{
result = Stretch.CalculateSize(
availableSize,
_gifInstance.GifPixelSize.ToSize(scaling),
StretchDirection
);
}
return result;
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
if (_gifInstance is null)
return new Size();
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0;
var sourceSize = _gifInstance.GifPixelSize.ToSize(scaling);
var result = Stretch.CalculateSize(finalSize, sourceSize);
return result;
}
private void SourceChanged(AvaloniaPropertyChangedEventArgs e)
{
if (
e.NewValue is null
|| (e.NewValue is string value && !Uri.IsWellFormedUriString(value, UriKind.Absolute))
)
{
return;
}
if (_customVisual is null)
{
_initialSource = e.NewValue;
return;
}
UpdateGifInstance(e.NewValue);
InvalidateArrange();
InvalidateMeasure();
Update();
}
private void UpdateGifInstance(object source)
{
_gifInstance?.Dispose();
_gifInstance = new WebpInstance(source);
// _gifInstance = new GifInstance(source);
_gifInstance.IterationCount = IterationCount;
_customVisual?.SendHandlerMessage(_gifInstance);
}
}
}

147
Avalonia.Gif/GifInstance.cs

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Gif.Decoding;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Gif
{
public class GifInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;
private readonly GifDecoder _gifDecoder;
private readonly WriteableBitmap? _targetBitmap;
private TimeSpan _totalTime;
private readonly List<TimeSpan> _frameTimes;
private uint _iterationCount;
private int _currentFrameIndex;
private readonly List<ulong> _colorTableIdList;
public CancellationTokenSource CurrentCts { get; }
internal GifInstance(object newValue)
: this(
newValue switch
{
Stream s => s,
Uri u => GetStreamFromUri(u),
string str => GetStreamFromString(str),
_ => throw new InvalidDataException("Unsupported source object")
}
) { }
public GifInstance(string uri)
: this(GetStreamFromString(uri)) { }
public GifInstance(Uri uri)
: this(GetStreamFromUri(uri)) { }
public GifInstance(Stream currentStream)
{
if (!currentStream.CanSeek)
throw new InvalidDataException("The provided stream is not seekable.");
if (!currentStream.CanRead)
throw new InvalidOperationException("Can't read the stream provided.");
currentStream.Seek(0, SeekOrigin.Begin);
CurrentCts = new CancellationTokenSource();
_gifDecoder = new GifDecoder(currentStream, CurrentCts.Token);
var pixSize = new PixelSize(_gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height);
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
GifPixelSize = pixSize;
_totalTime = TimeSpan.Zero;
_frameTimes = _gifDecoder
.Frames
.Select(frame =>
{
_totalTime = _totalTime.Add(frame.FrameDelay);
return _totalTime;
})
.ToList();
_gifDecoder.RenderFrame(0, _targetBitmap);
}
private static Stream GetStreamFromString(string str)
{
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
{
throw new InvalidCastException("The string provided can't be converted to URI.");
}
return GetStreamFromUri(res);
}
private static Stream GetStreamFromUri(Uri uri)
{
var uriString = uri.OriginalString.Trim();
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
{
return new FileStream(uriString, FileMode.Open, FileAccess.Read);
}
return AssetLoader.Open(uri);
}
public int GifFrameCount => _frameTimes.Count;
public PixelSize GifPixelSize { get; }
public void Dispose()
{
IsDisposed = true;
CurrentCts.Cancel();
_targetBitmap?.Dispose();
}
public bool IsDisposed { get; private set; }
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
{
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
{
return null;
}
if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
{
return null;
}
var elapsedTicks = stopwatchElapsed.Ticks;
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
var currentFrame = _frameTimes.IndexOf(targetFrame);
if (currentFrame == -1)
currentFrame = 0;
if (_currentFrameIndex == currentFrame)
return _targetBitmap;
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
return ProcessFrameIndex(currentFrame);
}
internal WriteableBitmap ProcessFrameIndex(int frameIndex)
{
_gifDecoder.RenderFrame(frameIndex, _targetBitmap);
_currentFrameIndex = frameIndex;
return _targetBitmap;
}
}
}

15
Avalonia.Gif/IGifInstance.cs

@ -0,0 +1,15 @@
using Avalonia.Animation;
using Avalonia.Media.Imaging;
namespace Avalonia.Gif;
public interface IGifInstance : IDisposable
{
IterationCount IterationCount { get; set; }
bool AutoStart { get; }
CancellationTokenSource CurrentCts { get; }
int GifFrameCount { get; }
PixelSize GifPixelSize { get; }
bool IsDisposed { get; }
WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed);
}

20
Avalonia.Gif/InvalidGifStreamException.cs

@ -0,0 +1,20 @@
using System;
using System.Runtime.Serialization;
namespace Avalonia.Gif
{
[Serializable]
internal class InvalidGifStreamException : Exception
{
public InvalidGifStreamException() { }
public InvalidGifStreamException(string message)
: base(message) { }
public InvalidGifStreamException(string message, Exception innerException)
: base(message, innerException) { }
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

180
Avalonia.Gif/WebpInstance.cs

@ -0,0 +1,180 @@
using Avalonia.Animation;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Gif;
public class WebpInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;
private readonly WriteableBitmap? _targetBitmap;
private TimeSpan _totalTime;
private readonly List<TimeSpan> _frameTimes;
private uint _iterationCount;
private int _currentFrameIndex;
private SKCodec? _codec;
public CancellationTokenSource CurrentCts { get; }
internal WebpInstance(object newValue)
: this(
newValue switch
{
Stream s => s,
Uri u => GetStreamFromUri(u),
string str => GetStreamFromString(str),
_ => throw new InvalidDataException("Unsupported source object")
}
) { }
public WebpInstance(string uri)
: this(GetStreamFromString(uri)) { }
public WebpInstance(Uri uri)
: this(GetStreamFromUri(uri)) { }
public WebpInstance(Stream currentStream)
{
if (!currentStream.CanSeek)
throw new InvalidDataException("The provided stream is not seekable.");
if (!currentStream.CanRead)
throw new InvalidOperationException("Can't read the stream provided.");
currentStream.Seek(0, SeekOrigin.Begin);
CurrentCts = new CancellationTokenSource();
var managedStream = new SKManagedStream(currentStream);
_codec = SKCodec.Create(managedStream);
var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height);
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
GifPixelSize = pixSize;
_totalTime = TimeSpan.Zero;
_frameTimes = _codec
.FrameInfo
.Select(frame =>
{
_totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration));
return _totalTime;
})
.ToList();
RenderFrame(_codec, _targetBitmap, 0);
}
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index)
{
codec.GetFrameInfo(index, out var frameInfo);
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
using var frameBuffer = targetBitmap.Lock();
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index));
if (result != SKCodecResult.Success)
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
}
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex)
{
codec.GetFrameInfo(index, out var frameInfo);
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
using var frameBuffer = targetBitmap.Lock();
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex));
if (result != SKCodecResult.Success)
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
}
private static Stream GetStreamFromString(string str)
{
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
{
throw new InvalidCastException("The string provided can't be converted to URI.");
}
return GetStreamFromUri(res);
}
private static Stream GetStreamFromUri(Uri uri)
{
var uriString = uri.OriginalString.Trim();
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
{
return new FileStream(uriString, FileMode.Open, FileAccess.Read);
}
return AssetLoader.Open(uri);
}
public int GifFrameCount => _frameTimes.Count;
public PixelSize GifPixelSize { get; }
public void Dispose()
{
IsDisposed = true;
CurrentCts.Cancel();
_targetBitmap?.Dispose();
_codec?.Dispose();
}
public bool IsDisposed { get; private set; }
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
{
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
{
return null;
}
if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
{
return null;
}
var elapsedTicks = stopwatchElapsed.Ticks;
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
var currentFrame = _frameTimes.IndexOf(targetFrame);
if (currentFrame == -1)
currentFrame = 0;
if (_currentFrameIndex == currentFrame)
return _targetBitmap;
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
return ProcessFrameIndex(currentFrame);
}
internal WriteableBitmap ProcessFrameIndex(int frameIndex)
{
if (_codec is null)
throw new InvalidOperationException("The codec is null.");
if (_targetBitmap is null)
throw new InvalidOperationException("The target bitmap is null.");
RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex);
_currentFrameIndex = frameIndex;
return _targetBitmap;
}
}

8
Build/AppEntitlements.entitlements

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

12
Build/EmbeddedEntitlements.entitlements

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

26
Build/build_macos_app.sh

@ -0,0 +1,26 @@
#!/bin/sh
while getopts v: flag
do
case "${flag}" in
v) version=${OPTARG};;
*) echo "Invalid option";;
esac
done
dotnet \
msbuild \
StabilityMatrix.Avalonia \
-t:BundleApp \
-p:RuntimeIdentifier=osx-arm64 \
-p:UseAppHost=true \
-p:Configuration=Release \
-p:CFBundleShortVersionString="$version" \
-p:SelfContained=true \
-p:CFBundleName="Stability Matrix" \
-p:CFBundleDisplayName="Stability Matrix" \
-p:CFBundleVersion="$version" \
-p:PublishDir="$(pwd)/out/osx-arm64/bin" \
# Copy the app out of bin
cp -r ./out/osx-arm64/bin/Stability\ Matrix.app ./out/osx-arm64/Stability\ Matrix.app

62
Build/codesign_embedded_macos.sh

@ -0,0 +1,62 @@
#!/bin/sh
echo "Signing file: $1"
# Setup keychain in CI
if [ -n "$CI" ]; then
# Turn our base64-encoded certificate back to a regular .p12 file
echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
fi
# Sign all files
PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P )
ENTITLEMENTS="$PARENT_PATH/EmbeddedEntitlements.entitlements"
echo "Using entitlements file: $ENTITLEMENTS"
# App
if [ "$1" == "*.app" ]; then
echo "[INFO] Signing app contents"
find "$1/Contents/MacOS/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname"
fi
done
echo "[INFO] Signing app file"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v
# Directory
elif [ -d "$1" ]; then
echo "[INFO] Signing directory contents"
find "$1"|while read fname; do
if [[ -f $fname ]] && [[ ! $fname =~ /(*.(py|msg|enc))/ ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname"
fi
done
# File
elif [ -f "$1" ]; then
echo "[INFO] Signing file"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v
# Not matched
else
echo "[ERROR] Unknown file type"
exit 1
fi

37
Build/codesign_macos.sh

@ -0,0 +1,37 @@
#!/bin/sh
echo "Signing file: $1"
# Setup keychain in CI
if [ -n "$CI" ]; then
# Turn our base64-encoded certificate back to a regular .p12 file
echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
fi
# Sign all files
PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P )
ENTITLEMENTS="$PARENT_PATH/AppEntitlements.entitlements"
echo "Using entitlements file: $ENTITLEMENTS"
find "$1/Contents/MacOS/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname"
fi
done
echo "[INFO] Signing app file"
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v

32
Build/notarize_macos.sh

@ -0,0 +1,32 @@
#!/bin/sh
echo "Notarizing file: $1"
# Store the notarization credentials so that we can prevent a UI password dialog
# from blocking the CI
echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" \
--apple-id "$MACOS_NOTARIZATION_APPLE_ID" \
--team-id "$MACOS_NOTARIZATION_TEAM_ID" \
--password "$MACOS_NOTARIZATION_PWD"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "Creating temp notarization archive"
ditto -c -k --keepParent "$1" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "Attach staple"
xcrun stapler staple "$1"

165
CHANGELOG.md

@ -5,6 +5,171 @@ 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.8.0
### Added
- Added Image to Video project type
- Added CLIP Skip setting to inference, toggleable from the model settings button
- Added image and model details in model selection boxes
- Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer)
- Added native desktop push notifications for some events (i.e. Downloads, Package installs, Inference generation)
- Currently available on Windows and Linux, macOS support is pending
- Added Package Extensions (Plugins) management - accessible from the Packages' 3-dot menu. Currently supports ComfyUI and Automatic1111.
- Added new launch argument options for Fooocus
- Added "Config" Shared Model Folder option for Fooocus
- Added Recommended Models dialog after one-click installer
- Added "Copy Details" button to Unexpected Error dialog
- Added German language option, thanks to Mario da Graca for the translation
- Added Portuguese language options, thanks to nextosai for the translation
- Added base model filter to Checkpoints page
- Added "Compatible Images" category when selecting images for Inference projects
- Added "Find in Model Browser" option to the right-click menu on the Checkpoints page
- Added `--use-directml` launch argument for SDWebUI DirectML fork
- Added release builds for macOS (Apple Silicon)
- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
- Added Delete button to the CivitAI Model Browser details dialog
- Added "Copy Link to Clipboard" for connected models in the Checkpoints page
- Added support for webp files to the Output Browser
- Added "Send to Image to Image" and "Send to Image to Video" options to the context menu
### Changed
- New package installation flow
- Changed one-click installer to match the new package installation style
- Automatic1111 packages will now use PyTorch v2.1.2. Upgrade will occur during the next package update or upon fresh installation.
- Search box on Checkpoints page now searches tags and trigger words
- Changed the Close button on the package install dialog to "Hide"
- Functionality remains the same, just a name change
- Updated translations for the following languages:
- Spanish
- French
- Japanese
- Turkish
- Inference file name patterns with directory separator characters will now have the subdirectories created automatically
- Changed how settings file is written to disk to reduce potential data loss risk
- (Internal) Updated to Avalonia 11.0.7
### Fixed
- Fixed error when ControlNet module image paths are not found, even if the module is disabled
- Fixed error when finding metadata for archived models
- Fixed error when extensions folder is missing
- Fixed crash when model was not selected in Inference
- Fixed Fooocus Config shared folder mode overwriting unknown config keys
- Fixed potential SD.Next update issues by moving to shared update process
- Fixed crash on startup when Outputs page failed to load categories properly
- Fixed image gallery arrow key navigation requiring clicking before responding
- Fixed crash when loading extensions list with no internet connection
- Fixed crash when invalid launch arguments are passed
- Fixed missing up/downgrade buttons on the Python Packages dialog when the version was not semver compatible
## v2.8.0-pre.5
### Fixed
- Fixed error when ControlNet module image paths are not found, even if the module is disabled
- Fixed error when finding metadata for archived models
- Fixed error when extensions folder is missing
- Fixed error when webp files have incorrect metadata
- Fixed crash when model was not selected in Inference
- Fixed Fooocus Config shared folder mode overwriting unknown config keys
## v2.8.0-pre.4
### Added
- Added Recommended Models dialog after one-click installer
- Added native desktop push notifications for some events (i.e. Downloads, Package installs, Inference generation)
- Currently available on Windows and Linux, macOS support is pending
- Added settings options for notifications
- Added new launch argument options for Fooocus
- Added Automatic1111 & Stable Diffusion WebUI-UX to the compatible macOS packages
### Changed
- Changed one-click installer to match the new package installation style
- Automatic1111 packages will now use PyTorch v2.1.2. Upgrade will occur during the next package update or upon fresh installation.
- Updated French translation with the latest changes
### Fixed
- Fixed [#413](https://github.com/LykosAI/StabilityMatrix/issues/413) - Environment Variables are editable again
- Fixed potential SD.Next update issues by moving to shared update process
- Fixed Invoke install trying to use system nodejs
- Fixed crash on startup when Outputs page failed to load categories properly
## v2.8.0-pre.3
### Added
- Added "Config" Shared Model Folder option for Fooocus
- Added "Copy Details" button to Unexpected Error dialog
### Changed
- (Internal) Updated to Avalonia 11.0.7
- Changed the Close button on the package install dialog to "Hide"
- Functionality remains the same, just a name change
- Updated French translation (thanks Greg!)
### Fixed
- Webp static images can now be shown alongside existing webp animation support
- Fixed image gallery arrow key navigation requiring clicking before responding
- Fixed crash when loading extensions list with no internet connection
- Fixed crash when invalid launch arguments are passed
- Fixed "must give at least one requirement to install" error when installing extensions with empty requirements.txt
## v2.8.0-pre.2
### Added
- Added German language option, thanks to Mario da Graca for the translation
- Added Portuguese language options, thanks to nextosai for the translation
### Changed
- Updated translations for the following languages:
- Spanish
- French
- Japanese
- Turkish
### Fixed
- Fixed Auto-update failing to start new version on Windows and Linux when path contains spaces
- Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI
- Install button will now be properly disabled when the duplicate warning is shown
## v2.8.0-pre.1
### Added
- Added Package Extensions (Plugins) management - accessible from the Packages' 3-dot menu. Currently supports ComfyUI and A1111.
- Added base model filter to Checkpoints page
- Search box on Checkpoints page now searches tags and trigger words
- Added "Compatible Images" category when selecting images for Inference projects
- Added "Find in Model Browser" option to the right-click menu on the Checkpoints page
### Changed
- Removed "Failed to load image" notification when loading some images on the Checkpoints page
- Installed models will no longer be selectable on the Hugging Face tab of the model browser
### Fixed
- Inference file name patterns with directory separator characters will now have the subdirectories created automatically
- Fixed missing up/downgrade buttons on the Python Packages dialog when the version was not semver compatible
- Automatic1111 package installs will now install the missing `jsonmerge` package
## v2.8.0-dev.4
### Added
- Auto-update support for macOS
- New package installation flow
- Added `--use-directml` launch argument for SDWebUI DirectML fork
### Changed
- Changed default Period to "AllTime" in the Model Browser
### Fixed
- Fixed SDTurboScheduler's missing denoise parameter
## v2.8.0-dev.3
### Added
- Added release builds for macOS (Apple Silicon)
- Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer)
- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
- Added Delete button to the CivitAI Model Browser details dialog
- Added "Copy Link to Clipboard" for connected models in the Checkpoints page
### Changed
- Python Packages install dialog now allows entering multiple arguments or option flags
### Fixed
- Fixed environment variables grid not being editable related to [Avalonia #13843](https://github.com/AvaloniaUI/Avalonia/issues/13843)
## v2.8.0-dev.2
### Added
#### Inference
- Added Image to Video project type
#### Output Browser
- Added support for webp files
- Added "Send to Image to Image" and "Send to Image to Video" options to the context menu
### Changed
- Changed how settings file is written to disk to reduce potential data loss risk
## v2.8.0-dev.1
### Added
#### Inference
- Added image and model details in model selection boxes
- Added CLIP Skip setting, toggleable from the model settings button
## v2.7.9
### Fixed
- Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI

10
README.md

@ -6,7 +6,7 @@
[release]: https://github.com/LykosAI/StabilityMatrix/releases/latest
[download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip
[download-linux-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-linux-x64.zip
[download-macos]: https://github.com/LykosAI/StabilityMatrix/issues/45
[download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg
[auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui
[comfy]: https://github.com/comfyanonymous/ComfyUI
@ -44,9 +44,7 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion
[![Windows](https://img.shields.io/badge/Windows-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64]
[![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-x64]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos]
> macOS builds are currently pending: [#45][download-macos]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64]
### Inference - A reimagined built-in Stable Diffusion experience
- Powerful auto-completion and syntax highlighting using a formal language grammar
@ -106,6 +104,10 @@ Stability Matrix is now available in the following languages, thanks to our comm
- aolko
- 🇹🇷 Türkçe
- Progesor
- 🇩🇪 Deutsch
- Mario da Graca
- 🇵🇹 Português
- nextosai
If you would like to contribute a translation, please create an issue or contact us on Discord. Include an email where we'll send an invite to our [POEditor](https://poeditor.com/) project.

13
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,12 +19,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="Avalonia" Version="11.0.7" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="NLog" Version="5.2.8" />
</ItemGroup>
<ItemGroup>

5
StabilityMatrix.Avalonia/App.axaml

@ -4,6 +4,7 @@
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
Name="Stability Matrix"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -23,6 +24,7 @@
<ResourceInclude Source="Controls/Scroll/BetterScrollViewer.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/HyperlinkIconButtonStyles.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/BetterComboBoxStyles.axaml"/>
<ResourceInclude Source="Styles/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/FAComboBoxStyles.axaml"/>
<ResourceInclude Source="Controls/Inference/ImageFolderCard.axaml"/>
@ -51,6 +53,7 @@
<StyleInclude Source="Styles/ToggleButtonStyles.axaml"/>
<StyleInclude Source="Styles/DockStyles.axaml"/>
<StyleInclude Source="Styles/BorderStyles.axaml"/>
<StyleInclude Source="Styles/TextBoxStyles.axaml"/>
<StyleInclude Source="Controls/AdvancedImageBox.axaml"/>
<StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>
@ -58,6 +61,8 @@
<StyleInclude Source="Controls/SelectableImageCard/SelectableImageButton.axaml"/>
<StyleInclude Source="Controls/SettingsAccountLinkExpander.axaml"/>
<StyleInclude Source="Controls/StarsRating.axaml"/>
<StyleInclude Source="Controls/VideoGenerationSettingsCard.axaml"/>
<StyleInclude Source="Controls/VideoOutputSettingsCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackEditableCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackExpander.axaml"/>

333
StabilityMatrix.Avalonia/App.axaml.cs

@ -17,11 +17,13 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Interop;
using FluentAvalonia.UI.Controls;
using MessagePipe;
using Microsoft.Extensions.Configuration;
@ -80,7 +82,8 @@ public sealed class App : Application
public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!;
internal static bool IsHeadlessMode => TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
internal static bool IsHeadlessMode =>
TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
[NotNull]
public static IStorageProvider? StorageProvider { get; internal set; }
@ -96,6 +99,8 @@ public sealed class App : Application
public IClassicDesktopStyleApplicationLifetime? DesktopLifetime =>
ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
public static new App? Current => (App?)Application.Current;
/// <summary>
/// Called before <see cref="Services"/> is built.
/// Can be used by UI tests to override services.
@ -106,6 +111,8 @@ public sealed class App : Application
{
AvaloniaXamlLoader.Load(this);
SetFontFamily(GetPlatformDefaultFontFamily());
// Set design theme
if (Design.IsDesignMode)
{
@ -117,8 +124,7 @@ public sealed class App : Application
{
// Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit
var dataValidationPluginsToRemove = BindingPlugins
.DataValidators
.OfType<DataAnnotationsValidationPlugin>()
.DataValidators.OfType<DataAnnotationsValidationPlugin>()
.ToArray();
foreach (var plugin in dataValidationPluginsToRemove)
@ -161,22 +167,19 @@ public sealed class App : Application
DesktopLifetime.MainWindow = setupWindow;
setupWindow
.ShowAsyncCts
.Token
.Register(() =>
setupWindow.ShowAsyncCts.Token.Register(() =>
{
if (setupWindow.Result == ContentDialogResult.Primary)
{
if (setupWindow.Result == ContentDialogResult.Primary)
{
settingsManager.SetEulaAccepted();
ShowMainWindow();
DesktopLifetime.MainWindow.Show();
}
else
{
Shutdown();
}
});
settingsManager.SetEulaAccepted();
ShowMainWindow();
DesktopLifetime.MainWindow.Show();
}
else
{
Shutdown();
}
});
}
else
{
@ -185,6 +188,58 @@ public sealed class App : Application
}
}
/// <summary>
/// Set the default font family for the application.
/// </summary>
private void SetFontFamily(FontFamily fontFamily)
{
Resources["ContentControlThemeFontFamily"] = fontFamily;
}
/// <summary>
/// Get the default font family for the current platform and language.
/// </summary>
public FontFamily GetPlatformDefaultFontFamily()
{
try
{
var fonts = new List<string>();
if (Cultures.Current?.Name == "ja-JP")
{
return Resources["NotoSansJP"] as FontFamily
?? throw new ApplicationException("Font NotoSansJP not found");
}
if (Compat.IsWindows)
{
fonts.Add(OSVersionHelper.IsWindows11() ? "Segoe UI Variable Text" : "Segoe UI");
}
else if (Compat.IsMacOS)
{
// Use Segoe fonts if installed, but we can't distribute them
fonts.Add("Segoe UI Variable");
fonts.Add("Segoe UI");
fonts.Add("San Francisco");
fonts.Add("Helvetica Neue");
fonts.Add("Helvetica");
}
else
{
return FontFamily.Default;
}
return new FontFamily(string.Join(",", fonts));
}
catch (Exception e)
{
LogManager.GetCurrentClassLogger().Error(e);
return FontFamily.Default;
}
}
/// <summary>
/// Setup tasks to be run shortly before any window is shown
/// </summary>
@ -223,17 +278,13 @@ public sealed class App : Application
mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
mainWindow.Closing += OnMainWindowClosing;
mainWindow.Closed += (_, _) => Shutdown();
mainWindow.SetDefaultFonts();
VisualRoot = mainWindow;
StorageProvider = mainWindow.StorageProvider;
Clipboard = mainWindow.Clipboard ?? throw new NullReferenceException("Clipboard is null");
DesktopLifetime.MainWindow = mainWindow;
DesktopLifetime.Exit += OnExit;
DesktopLifetime.ShutdownRequested += OnShutdownRequested;
}
private static void ConfigureServiceProvider()
@ -246,7 +297,10 @@ public sealed class App : Application
var settingsManager = Services.GetRequiredService<ISettingsManager>();
settingsManager.LibraryDirOverride = Program.Args.DataDirectoryOverride;
if (Program.Args.DataDirectoryOverride is not null)
{
settingsManager.SetLibraryDirOverride(Program.Args.DataDirectoryOverride);
}
if (settingsManager.TryFindLibrary())
{
@ -276,7 +330,7 @@ public sealed class App : Application
{
provider.GetRequiredService<LaunchPageViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<NewPackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
@ -297,7 +351,9 @@ public sealed class App : Application
var serviceManager = new ServiceManager<ViewModelBase>();
var serviceManagedTypes = exportedTypes
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) })
.Select(
t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) }
)
.Where(t1 => t1.attributes is { Length: > 0 })
.Select(t1 => t1.t)
.ToList();
@ -322,8 +378,7 @@ public sealed class App : Application
services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix");
var exportedTypes = AppDomain
.CurrentDomain
.GetAssemblies()
.CurrentDomain.GetAssemblies()
.Where(a => a.FullName?.StartsWith("StabilityMatrix") == true)
.SelectMany(a => a.GetExportedTypes())
.ToArray();
@ -332,7 +387,8 @@ public sealed class App : Application
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) })
.Where(
t1 =>
t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] });
@ -352,9 +408,12 @@ public sealed class App : Application
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) })
.Where(
t1 =>
t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray() });
.Select(
t1 => new { Type = t1.t, Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray() }
);
foreach (var typePair in singletonTypes)
{
@ -386,7 +445,9 @@ public sealed class App : Application
// Rich presence
services.AddSingleton<IDiscordRichPresenceService, DiscordRichPresenceService>();
services.AddSingleton<IDisposable>(provider => provider.GetRequiredService<IDiscordRichPresenceService>());
services.AddSingleton<IDisposable>(
provider => provider.GetRequiredService<IDiscordRichPresenceService>()
);
Config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@ -512,7 +573,7 @@ public sealed class App : Application
.AddRefitClient<ILykosAuthApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://stableauthentication.azurewebsites.net");
c.BaseAddress = new Uri("https://auth.lykos.ai");
c.Timeout = TimeSpan.FromSeconds(15);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
@ -557,7 +618,11 @@ public sealed class App : Application
#if DEBUG
builder.AddNLog(
ConfigureLogging(),
new NLogProviderOptions { IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy }
new NLogProviderOptions
{
IgnoreEmptyEventId = false,
CaptureEventId = EventIdCaptureType.Legacy
}
);
#else
builder.AddNLog(ConfigureLogging());
@ -577,91 +642,79 @@ public sealed class App : Application
{
if (Current is null)
throw new NullReferenceException("Current Application was null when Shutdown called");
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Shutdown(exitCode);
try
{
var result = lifetime.TryShutdown(exitCode);
Debug.WriteLine($"Shutdown: {result}");
if (result)
{
Environment.Exit(exitCode);
}
}
catch (InvalidOperationException)
{
// Ignore in case already shutting down
}
}
else
{
Environment.Exit(exitCode);
}
}
/// <summary>
/// Handle shutdown requests (happens before <see cref="OnExit"/>)
/// </summary>
private static void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
private static void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
if (e.Cancel)
return;
var mainWindow = (MainWindow)sender!;
// Show confirmation if package running
var launchPageViewModel = Services.GetRequiredService<LaunchPageViewModel>();
launchPageViewModel.OnMainWindowClosing(e);
Debug.WriteLine("Start OnShutdownRequested");
if (e.Cancel)
return;
// Check if we need to dispose IAsyncDisposables
if (
!isAsyncDisposeComplete
&& Services.GetServices<IAsyncDisposable>().ToList() is { Count: > 0 } asyncDisposables
isAsyncDisposeComplete
|| Services.GetServices<IAsyncDisposable>().ToList() is not { Count: > 0 } asyncDisposables
)
{
// Cancel shutdown for now
e.Cancel = true;
isAsyncDisposeComplete = true;
return;
// Cancel shutdown for now
e.Cancel = true;
isAsyncDisposeComplete = true;
Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
Task.Run(async () =>
Dispatcher
.UIThread.InvokeAsync(async () =>
{
foreach (var disposable in asyncDisposables)
{
foreach (var disposable in asyncDisposables)
Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})");
try
{
Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})");
try
{
await disposable.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
Debug.Fail(ex.ToString());
}
await disposable.DisposeAsync().ConfigureAwait(false);
}
})
.ContinueWith(_ =>
catch (Exception ex)
{
Debug.Fail(ex.ToString());
}
}
})
.ContinueWith(_ =>
{
// Shutdown again
Debug.WriteLine("Finished disposing IAsyncDisposables, shutting down");
if (Dispatcher.UIThread.SupportsRunLoops)
{
// Shutdown again
Dispatcher.UIThread.Invoke(() => Shutdown());
})
.SafeFireAndForget();
return;
}
OnMainWindowClosingTerminal(mainWindow);
}
/// <summary>
/// Called at the end of <see cref="OnMainWindowClosing"/> before the main window is closed.
/// </summary>
private static void OnMainWindowClosingTerminal(Window sender)
{
var settingsManager = Services.GetRequiredService<ISettingsManager>();
// Save window position
var validWindowPosition = sender.Screens.All.Any(screen => screen.Bounds.Contains(sender.Position));
}
settingsManager.Transaction(
s =>
{
s.WindowSettings = new WindowSettings(
sender.Width,
sender.Height,
validWindowPosition ? sender.Position.X : 0,
validWindowPosition ? sender.Position.Y : 0
);
},
ignoreMissingLibraryDir: true
);
Environment.Exit(0);
})
.SafeFireAndForget();
}
private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args)
@ -714,10 +767,12 @@ public sealed class App : Application
.WriteTo(
new FileTarget
{
Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
Layout =
"${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
ArchiveOldFileOnStartup = true,
FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveFileName =
"${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveNumbering = ArchiveNumberingMode.Rolling,
MaxArchiveFiles = 2
}
@ -730,7 +785,9 @@ public sealed class App : Application
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
// Disable console trace logging by default
builder.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel").WriteToNil(NLog.LogLevel.Debug);
builder
.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
.WriteToNil(NLog.LogLevel.Debug);
// Disable LoadableViewModelBase trace logging by default
builder
@ -751,20 +808,18 @@ public sealed class App : Application
// Sentry
if (SentrySdk.IsEnabled)
{
LogManager
.Configuration
.AddSentry(o =>
{
o.InitializeSdk = false;
o.Layout = "${message}";
o.ShutdownTimeoutSeconds = 5;
o.IncludeEventDataOnBreadcrumbs = true;
o.BreadcrumbLayout = "${logger}: ${message}";
// Debug and higher are stored as breadcrumbs (default is Info)
o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
// Error and higher is sent as event (default is Error)
o.MinimumEventLevel = NLog.LogLevel.Error;
});
LogManager.Configuration.AddSentry(o =>
{
o.InitializeSdk = false;
o.Layout = "${message}";
o.ShutdownTimeoutSeconds = 5;
o.IncludeEventDataOnBreadcrumbs = true;
o.BreadcrumbLayout = "${logger}: ${message}";
// Debug and higher are stored as breadcrumbs (default is Info)
o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
// Error and higher is sent as event (default is Error)
o.MinimumEventLevel = NLog.LogLevel.Error;
});
}
LogManager.ReconfigExistingLoggers();
@ -803,34 +858,36 @@ public sealed class App : Application
results.Add(ms);
}
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
var dest = await StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true }
);
Dispatcher.UIThread.InvokeAsync(async () =>
{
var dest = await StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions()
{
SuggestedFileName = "screenshot.png",
ShowOverwritePrompt = true
}
);
if (dest?.TryGetLocalPath() is { } localPath)
if (dest?.TryGetLocalPath() is { } localPath)
{
var localFile = new FilePath(localPath);
foreach (var (i, stream) in results.Enumerate())
{
var localFile = new FilePath(localPath);
foreach (var (i, stream) in results.Enumerate())
var name = localFile.NameWithoutExtension;
if (results.Count > 1)
{
var name = localFile.NameWithoutExtension;
if (results.Count > 1)
{
name += $"_{i + 1}";
}
localFile = localFile.Directory!.JoinFile(name + ".png");
localFile.Create();
await using var fileStream = localFile.Info.OpenWrite();
stream.Seek(0, SeekOrigin.Begin);
await stream.CopyToAsync(fileStream);
name += $"_{i + 1}";
}
localFile = localFile.Directory!.JoinFile(name + ".png");
localFile.Create();
await using var fileStream = localFile.Info.OpenWrite();
stream.Seek(0, SeekOrigin.Begin);
await stream.CopyToAsync(fileStream);
}
});
}
});
}
[Conditional("DEBUG")]

28
StabilityMatrix.Avalonia/Assets.cs

@ -11,16 +11,19 @@ namespace StabilityMatrix.Avalonia;
internal static class Assets
{
public static AvaloniaResource AppIcon { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIcon { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIconPng { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.png");
public static AvaloniaResource AppIconPng { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.png");
/// <summary>
/// Fixed image for models with no images.
/// </summary>
public static Uri NoImage { get; } = new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
public static AvaloniaResource LicensesJson => new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
public static AvaloniaResource LicensesJson =>
new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
public static AvaloniaResource ImagePromptLanguageJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json");
@ -28,7 +31,8 @@ internal static class Assets
public static AvaloniaResource ThemeMatrixDarkJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json");
public static AvaloniaResource HfPackagesJson => new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json");
public static AvaloniaResource HfPackagesJson =>
new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json");
private const UnixFileMode Unix755 =
UnixFileMode.UserRead
@ -44,7 +48,10 @@ internal static class Assets
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipExecutable =>
Compat.Switch(
(PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")),
(
PlatformKind.Windows,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")
),
(
PlatformKind.Linux | PlatformKind.X64,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", Unix755)
@ -112,10 +119,9 @@ internal static class Assets
PlatformKind.MacOS | PlatformKind.Arm,
new RemoteResource
{
Url = new Uri(
"https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"
),
HashSha256 = "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f"
// Requires our distribution with signed dylib for gatekeeper
Url = new Uri("https://cdn.lykos.ai/cpython-3.10.11-macos-arm64.zip"),
HashSha256 = "83c00486e0af9c460604a425e519d58e4b9604fbe7a4448efda0f648f86fb6e3"
}
)
);
@ -148,7 +154,9 @@ internal static class Assets
/// <summary>
/// Yield AvaloniaResources given a relative directory path within the 'Assets' folder.
/// </summary>
public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(string relativeAssetPath)
public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(
string relativeAssetPath
)
{
var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/");
var targetUri = new Uri(baseUri, relativeAssetPath);

BIN
StabilityMatrix.Avalonia/Assets/AppIcon.icns

Binary file not shown.

141
StabilityMatrix.Avalonia/Collections/SearchCollection.cs

@ -0,0 +1,141 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
using JetBrains.Annotations;
namespace StabilityMatrix.Avalonia.Collections;
[PublicAPI]
public class SearchCollection<TObject, TKey, TQuery> : AbstractNotifyPropertyChanged, IDisposable
where TObject : notnull
where TKey : notnull
{
private readonly IDisposable cleanUp;
private Func<TQuery?, Func<TObject, bool>>? PredicateSelector { get; }
private Func<TQuery?, Func<TObject, (bool, int)>>? ScorerSelector { get; }
private Func<TObject, (bool, int)>? Scorer { get; set; }
private TQuery? _query;
public TQuery? Query
{
get => _query;
set => SetAndRaise(ref _query, value);
}
private SortExpressionComparer<TObject> _sortComparer = [];
public SortExpressionComparer<TObject> SortComparer
{
get => _sortComparer;
set => SetAndRaise(ref _sortComparer, value);
}
/// <summary>
/// Converts <see cref="SortComparer"/> to <see cref="SortExpressionComparer{SearchItem}"/>.
/// </summary>
private SortExpressionComparer<SearchItem> SearchItemSortComparer =>
[
..SortComparer
.Select(sortExpression => new SortExpression<SearchItem>(
item => sortExpression.Expression(item.Item),
sortExpression.Direction
)).Prepend(new SortExpression<SearchItem>(item => item.Score, SortDirection.Descending))
];
public IObservableCollection<TObject> Items { get; } = new ObservableCollectionExtended<TObject>();
public IObservableCollection<TObject> FilteredItems { get; } =
new ObservableCollectionExtended<TObject>();
public SearchCollection(
IObservable<IChangeSet<TObject, TKey>> source,
Func<TQuery?, Func<TObject, bool>> predicateSelector,
SortExpressionComparer<TObject>? sortComparer = null
)
{
PredicateSelector = predicateSelector;
if (sortComparer is not null)
{
SortComparer = sortComparer;
}
// Observable which creates a new predicate whenever Query property changes
var dynamicPredicate = this.WhenValueChanged(@this => @this.Query).Select(predicateSelector);
cleanUp = source
.Bind(Items)
.Filter(dynamicPredicate)
.Sort(SortComparer)
.Bind(FilteredItems)
.Subscribe();
}
public SearchCollection(
IObservable<IChangeSet<TObject, TKey>> source,
Func<TQuery?, Func<TObject, (bool, int)>> scorerSelector,
SortExpressionComparer<TObject>? sortComparer = null
)
{
ScorerSelector = scorerSelector;
if (sortComparer is not null)
{
SortComparer = sortComparer;
}
// Monitor Query property for changes
var queryChanged = this.WhenValueChanged(@this => @this.Query).Select(_ => Unit.Default);
cleanUp = new CompositeDisposable(
// Update Scorer property whenever Query property changes
queryChanged.Subscribe(_ => Scorer = scorerSelector(Query)),
// Transform source items into SearchItems
source
.Transform(
obj =>
{
var (isMatch, score) = Scorer?.Invoke(obj) ?? (true, 0);
return new SearchItem
{
Item = obj,
IsMatch = isMatch,
Score = score
};
},
forceTransform: queryChanged
)
.Filter(item => item.IsMatch)
.Sort(SearchItemSortComparer, SortOptimisations.ComparesImmutableValuesOnly)
.Transform(searchItem => searchItem.Item)
.Bind(FilteredItems)
.Subscribe()
);
}
/// <summary>
/// Clears <see cref="Query"/> property by setting it to default value.
/// </summary>
public void ClearQuery()
{
Query = default;
}
public void Dispose()
{
cleanUp.Dispose();
GC.SuppressFinalize(this);
}
private readonly record struct SearchItem
{
public TObject Item { get; init; }
public int Score { get; init; }
public bool IsMatch { get; init; }
}
}

59
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml

@ -8,30 +8,53 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:gif="clr-namespace:Avalonia.Gif;assembly=Avalonia.Gif"
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="models:ImageSource"
mc:Ignorable="d">
<Grid>
<controls:AdvancedImageBox
Name="ImageBox"
CornerRadius="4"
Image="{Binding BitmapAsync^}"
SizeMode="Fit">
<controls:AdvancedImageBox.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
x:Name="CopyMenuItem"
CommandParameter="{Binding #ImageBox.Image}"
HotKey="Ctrl+C"
IconSource="Copy"
IsEnabled="{OnPlatform Windows=True,
Default=False}"
Text="Copy" />
</ui:FAMenuFlyout>
</controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox>
<!-- Tag is not used but sets TemplateKey which is used to select the DataTemplate later -->
<ContentPresenter
Tag="{Binding TemplateKeyAsync^}"
Content="{Binding}">
<ContentPresenter.ContentTemplate>
<controls:DataTemplateSelector x:TypeArguments="models:ImageSourceTemplateType">
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.WebpAnimation}" DataType="models:ImageSource">
<gif:GifImage
Stretch="Uniform"
SourceUri="{Binding LocalFile.FullPath}"/>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Image}" DataType="models:ImageSource">
<controls:AdvancedImageBox
CornerRadius="4"
Image="{Binding BitmapAsync^}"
SizeMode="Fit">
<controls:AdvancedImageBox.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
x:Name="CopyMenuItem"
CommandParameter="{Binding $parent[controls:AdvancedImageBox].Image}"
HotKey="Ctrl+C"
IconSource="Copy"
IsEnabled="{OnPlatform Windows=True, Default=False}"
Text="Copy" />
</ui:FAMenuFlyout>
</controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Default}" DataType="models:ImageSource">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Unsupported Format"/>
</DataTemplate>
</controls:DataTemplateSelector>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
<!-- Label pill card -->
<Border

13
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs

@ -20,14 +20,17 @@ public partial class AdvancedImageBoxView : UserControl
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var copyMenuItem = this.FindControl<MenuFlyoutItem>("CopyMenuItem")!;
copyMenuItem.Command = new AsyncRelayCommand<Bitmap?>(FlyoutCopy);
if (this.FindControl<MenuFlyoutItem>("CopyMenuItem") is { } copyMenuItem)
{
copyMenuItem.Command = new AsyncRelayCommand<Bitmap?>(FlyoutCopy);
}
}
private static async Task FlyoutCopy(Bitmap? image)
{
if (image is null || !Compat.IsWindows) return;
if (image is null || !Compat.IsWindows)
return;
await Task.Run(() =>
{

38
StabilityMatrix.Avalonia/Controls/BetterComboBox.cs

@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
namespace StabilityMatrix.Avalonia.Controls;
public class BetterComboBox : ComboBox
{
public static readonly DirectProperty<BetterComboBox, IDataTemplate?> SelectionBoxItemTemplateProperty =
AvaloniaProperty.RegisterDirect<BetterComboBox, IDataTemplate?>(
nameof(SelectionBoxItemTemplate),
v => v.SelectionBoxItemTemplate,
(x, v) => x.SelectionBoxItemTemplate = v
);
private IDataTemplate? _selectionBoxItemTemplate;
public IDataTemplate? SelectionBoxItemTemplate
{
get => _selectionBoxItemTemplate;
set => SetAndRaise(SelectionBoxItemTemplateProperty, ref _selectionBoxItemTemplate, value);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<ContentControl>("ContentPresenter") is { } contentPresenter)
{
if (SelectionBoxItemTemplate is { } template)
{
contentPresenter.ContentTemplate = template;
}
}
}
}

64
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -122,8 +122,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentVerticalScrollBarVisibilityProperty, value);
}
public static readonly StyledProperty<double> MinDialogWidthProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MinDialogWidth");
public static readonly StyledProperty<double> MinDialogWidthProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MinDialogWidth");
public double MinDialogWidth
{
@ -131,8 +133,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MinDialogWidthProperty, value);
}
public static readonly StyledProperty<double> MaxDialogWidthProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogWidth");
public static readonly StyledProperty<double> MaxDialogWidthProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MaxDialogWidth");
public double MaxDialogWidth
{
@ -140,8 +144,21 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogWidthProperty, value);
}
public static readonly StyledProperty<double> MaxDialogHeightProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogHeight");
public static readonly StyledProperty<double> MinDialogHeightProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MinDialogHeight");
public double MinDialogHeight
{
get => GetValue(MaxDialogHeightProperty);
set => SetValue(MaxDialogHeightProperty, value);
}
public static readonly StyledProperty<double> MaxDialogHeightProperty = AvaloniaProperty.Register<
BetterContentDialog,
double
>("MaxDialogHeight");
public double MaxDialogHeight
{
@ -149,8 +166,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogHeightProperty, value);
}
public static readonly StyledProperty<Thickness> ContentMarginProperty =
AvaloniaProperty.Register<BetterContentDialog, Thickness>("ContentMargin");
public static readonly StyledProperty<Thickness> ContentMarginProperty = AvaloniaProperty.Register<
BetterContentDialog,
Thickness
>("ContentMargin");
public Thickness ContentMargin
{
@ -158,8 +177,10 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentMarginProperty, value);
}
public static readonly StyledProperty<bool> CloseOnClickOutsideProperty =
AvaloniaProperty.Register<BetterContentDialog, bool>("CloseOnClickOutside");
public static readonly StyledProperty<bool> CloseOnClickOutsideProperty = AvaloniaProperty.Register<
BetterContentDialog,
bool
>("CloseOnClickOutside");
/// <summary>
/// Whether to close the dialog when clicking outside of it (on the blurred background)
@ -187,12 +208,17 @@ public class BetterContentDialog : ContentDialog
var point = e.GetPosition(this);
if (
!backgroundPart.Bounds.Contains(point)
&& (Content as Control)?.DataContext is ContentDialogViewModelBase vm
)
if (!backgroundPart.Bounds.Contains(point))
{
vm.OnCloseButtonClick();
// Use vm if available
if ((Content as Control)?.DataContext is ContentDialogViewModelBase vm)
{
vm.OnCloseButtonClick();
}
else
{
Hide(ContentDialogResult.None);
}
}
}
}
@ -211,10 +237,7 @@ public class BetterContentDialog : ContentDialog
viewModelDirect.SecondaryButtonClick += OnDialogButtonClick;
viewModelDirect.CloseButtonClick += OnDialogButtonClick;
}
else if (
(Content as Control)?.DataContext
is ContentDialogProgressViewModelBase progressViewModel
)
else if ((Content as Control)?.DataContext is ContentDialogProgressViewModelBase progressViewModel)
{
progressViewModel.PrimaryButtonClick += OnDialogButtonClick;
progressViewModel.SecondaryButtonClick += OnDialogButtonClick;
@ -234,8 +257,7 @@ public class BetterContentDialog : ContentDialog
}
else
{
PrimaryButton.IsVisible =
IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
}
}

46
StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
using JetBrains.Annotations;
using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.Controls;
/// <summary>
/// Selector for objects implementing <see cref="ITemplateKey{T}"/>
/// </summary>
[PublicAPI]
public class DataTemplateSelector<TKey> : IDataTemplate
where TKey : notnull
{
/// <summary>
/// Key that is used when no other key matches
/// </summary>
public TKey? DefaultKey { get; set; }
[Content]
public Dictionary<TKey, IDataTemplate> Templates { get; } = new();
public bool Match(object? data) => data is ITemplateKey<TKey>;
/// <inheritdoc />
public Control Build(object? data)
{
if (data is not ITemplateKey<TKey> key)
throw new ArgumentException(null, nameof(data));
if (Templates.TryGetValue(key.TemplateKey, out var template))
{
return template.Build(data)!;
}
if (DefaultKey is not null && Templates.TryGetValue(DefaultKey, out var defaultTemplate))
{
return defaultTemplate.Build(data)!;
}
throw new ArgumentException(null, nameof(data));
}
}

113
StabilityMatrix.Avalonia/Controls/HyperlinkIconButton.cs

@ -1,13 +1,122 @@
using System;
using System.Diagnostics;
using System.IO;
using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Logging;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Core.Processes;
using Symbol = FluentIcons.Common.Symbol;
namespace StabilityMatrix.Avalonia.Controls;
/// <summary>
/// Like <see cref="HyperlinkButton"/>, but with a link icon left of the text content.
/// </summary>
public class HyperlinkIconButton : HyperlinkButton
public class HyperlinkIconButton : Button
{
/// <inheritdoc />
private Uri? _navigateUri;
/// <summary>
/// Defines the <see cref="NavigateUri"/> property
/// </summary>
public static readonly DirectProperty<HyperlinkIconButton, Uri?> NavigateUriProperty =
AvaloniaProperty.RegisterDirect<HyperlinkIconButton, Uri?>(
nameof(NavigateUri),
x => x.NavigateUri,
(x, v) => x.NavigateUri = v
);
/// <summary>
/// Gets or sets the Uri that the button should navigate to upon clicking. In assembly paths are not supported, (e.g., avares://...)
/// </summary>
public Uri? NavigateUri
{
get => _navigateUri;
set => SetAndRaise(NavigateUriProperty, ref _navigateUri, value);
}
public static readonly StyledProperty<Symbol> IconProperty = AvaloniaProperty.Register<
HyperlinkIconButton,
Symbol
>("Icon", Symbol.Link);
public Symbol Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
protected override Type StyleKeyOverride => typeof(HyperlinkIconButton);
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
// Update icon
if (change.Property == NavigateUriProperty)
{
var uri = change.GetNewValue<Uri?>();
if (uri is not null && uri.IsFile && Icon == Symbol.Link)
{
Icon = Symbol.Open;
}
}
}
protected override void OnClick()
{
base.OnClick();
if (NavigateUri is null)
return;
// File or Folder URIs
if (NavigateUri.IsFile)
{
var path = NavigateUri.LocalPath;
if (Directory.Exists(path))
{
ProcessRunner
.OpenFolderBrowser(path)
.SafeFireAndForget(ex =>
{
Logger.TryGet(LogEventLevel.Error, $"Unable to open directory Uri {NavigateUri}");
});
}
else if (File.Exists(path))
{
ProcessRunner
.OpenFileBrowser(path)
.SafeFireAndForget(ex =>
{
Logger.TryGet(LogEventLevel.Error, $"Unable to open file Uri {NavigateUri}");
});
}
}
// Web
else
{
try
{
Process.Start(
new ProcessStartInfo(NavigateUri.ToString()) { UseShellExecute = true, Verb = "open" }
);
}
catch
{
Logger.TryGet(LogEventLevel.Error, $"Unable to open Uri {NavigateUri}");
}
}
}
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
return presenter.Name == "ContentPresenter";
}
}

2
StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml

@ -2,7 +2,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"

159
StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml

@ -7,11 +7,14 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}" />
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ImgToVidModelCardViewModel}" />
</StackPanel>
</Panel>
</Design.PreviewWith>
@ -21,126 +24,39 @@
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<sg:SpacedGrid
ColumnSpacing="8"
RowSpacing="0"
ColumnDefinitions="Auto,*,Auto"
RowDefinitions="*,*,*,*">
<!-- Model -->
<TextBlock
Grid.Column="0"
MinWidth="60"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
Padding="8,6,4,6"
Theme="{StaticResource BetterComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedModel}">
<!--<ui:FAComboBox.Styles>
<Style Selector="ui|FAComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
~1~ Image @1@
<controls:BetterAdvancedImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Grid RowDefinitions="Auto,Auto,*" VerticalAlignment="Stretch" MaxWidth="300">
~1~ Title @1@
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
~1~ Version @1@
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
~1~ Path @1@
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding RelativePath}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</Template>
</Setter>
</Style>
<Style Selector="ui|FAComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:FAComboBox.Styles>
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<StackPanel ToolTip.Placement="RightEdgeAlignedTop">
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>-->
<!--<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
<controls:BetterAdvancedImage
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Margin="0,0,8,0"
Height="32"
Width="32"
Stretch="UniformToFill"
StretchDirection="Both"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,32,32" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Text="{Binding Local.ConnectedModelInfo.VersionName}"/>
<TextBlock
FontSize="11"
Grid.Row="1"
Grid.Column="1"
Text="{Binding Local.ConnectedModelInfo.ModelName}"/>
</Grid>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>-->
</ui:FAComboBox>
SelectedItem="{Binding SelectedModel}"/>
<Button
Grid.Row="0"
Grid.Column="2"
Margin="8,0,0,0">
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Margin="0,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsRefinerSelectionEnabled}" Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsVaeSelectionEnabled}" Text="{x:Static lang:Resources.Label_VAE}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsClipSkipEnabled}" Text="{x:Static lang:Resources.Label_CLIPSkip}" />
</ui:FAMenuFlyout>
</Button.Flyout>
</Button>
@ -149,30 +65,28 @@
<TextBlock
Grid.Row="1"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"
TextAlignment="Left" />
<ui:FAComboBox
<controls:BetterComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
Padding="8,6,4,6"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedRefiner}" />
SelectedItem="{Binding SelectedRefiner}"/>
<!-- VAE -->
<TextBlock
Grid.Row="2"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsVaeSelectionEnabled}"
@ -190,8 +104,33 @@
IsVisible="{Binding IsVaeSelectionEnabled}"
ItemsSource="{Binding ClientManager.VaeModels}"
SelectedItem="{Binding SelectedVae}" />
<!-- CLIP Skip -->
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsClipSkipEnabled}"
Text="{x:Static lang:Resources.Label_CLIPSkip}"
TextAlignment="Left" />
<NumericUpDown
Grid.Row="3"
Grid.Column="1"
Grid.ColumnSpan="2"
IsVisible="{Binding IsClipSkipEnabled}"
Watermark="1"
HorizontalAlignment="Stretch"
Margin="0,8,0,0"
Minimum="1"
Maximum="24"
Increment="1"
ParsingNumberStyle="Integer"
Value="{Binding ClipSkip, Converter={x:Static converters:NullableDefaultNumericConverters.IntToDecimal}}"
ClipValueToMinMax="True"/>
</Grid>
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>

2
StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml

@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"

2
StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml

@ -4,7 +4,7 @@
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:modules="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Modules"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"

6
StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml

@ -2,11 +2,11 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
x:DataType="vmInference:StackExpanderViewModel">
<Design.PreviewWith>
@ -66,7 +66,7 @@
Classes="transparent-full"
Command="{Binding SettingsCommand}"
IsVisible="{Binding IsSettingsEnabled}">
<fluentIcons:SymbolIcon FontSize="17" Symbol="Settings" />
<fluent:SymbolIcon FontSize="17" Symbol="Settings" />
</Button>
<!-- Delete button for StackEditableCard -->
<Button
@ -76,7 +76,7 @@
Classes="transparent-red"
Command="{Binding RemoveFromParentListCommand}"
IsVisible="{Binding IsEditEnabled}">
<fluentIcons:SymbolIcon FontSize="16" Symbol="Delete" />
<fluent:SymbolIcon FontSize="16" Symbol="Delete" />
</Button>
</Grid>
</Expander.Header>

2
StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml

@ -8,7 +8,7 @@
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
x:DataType="vmInference:UpscalerCardViewModel">
<Design.PreviewWith>
<Grid MinWidth="300">

4
StabilityMatrix.Avalonia/Controls/StarsRating.axaml

@ -1,8 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:icons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia">
xmlns:controls="using:StabilityMatrix.Avalonia.Controls">
<Design.PreviewWith>
<StackPanel Width="400" Height="400" Spacing="4">
<StackPanel.Styles>

15
StabilityMatrix.Avalonia/Controls/StarsRating.axaml.cs

@ -3,17 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.VisualTree;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using FluentIcons.FluentAvalonia;
using SpacedGridControl.Avalonia;
using StabilityMatrix.Avalonia.Styles;
namespace StabilityMatrix.Avalonia.Controls;
@ -36,10 +31,10 @@ public class StarsRating : TemplatedControl
set => SetValue(IsEditableProperty, value);
}
public static readonly StyledProperty<int> MaximumProperty = AvaloniaProperty.Register<
StarsRating,
int
>(nameof(Maximum), 5);
public static readonly StyledProperty<int> MaximumProperty = AvaloniaProperty.Register<StarsRating, int>(
nameof(Maximum),
5
);
public int Maximum
{

122
StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml

@ -0,0 +1,122 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
x:DataType="video:SvdImgToVidConditioningViewModel"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:video="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Video"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages">
<Design.PreviewWith>
<Grid MinWidth="400">
<controls:VideoGenerationSettingsCard DataContext="{x:Static mocks:DesignData.SvdImgToVidConditioningViewModel}" />
</Grid>
</Design.PreviewWith>
<Style Selector="controls|VideoGenerationSettingsCard">
<!-- Set Defaults -->
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="8" HorizontalAlignment="{TemplateBinding HorizontalAlignment}">
<Grid Margin="4" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, *">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Frames}" />
<controls1:NumberBox
Grid.Row="0"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding NumFrames}"
Margin="8,0,0,0"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Fps}" />
<controls1:NumberBox
Grid.Row="1"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding Fps}"
Margin="8,8,0,0"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_MinCfg}" />
<controls1:NumberBox
Margin="8,8,0,0"
Grid.Row="2"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding MinCfg}"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<TextBlock
Margin="0,8,8,0"
Grid.Row="3"
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_MotionBucketId}" />
<controls1:NumberBox
Margin="8,8,0,0"
Grid.Row="3"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding MotionBucketId}"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline"/>
<StackPanel Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Margin="0,16,0,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_AugmentationLevel}" />
<controls1:NumberBox
Grid.Column="1"
Margin="4,0,0,0"
ValidationMode="InvalidInputOverwritten"
SmallChange="0.01"
SimpleNumberFormat="F2"
Value="{Binding AugmentationLevel}"
HorizontalAlignment="Stretch"
MinWidth="100"
SpinButtonPlacementMode="Compact"/>
</Grid>
<Slider
Minimum="0"
Maximum="10"
Value="{Binding AugmentationLevel}"
TickFrequency="1"
Margin="0,0,0,-4"
TickPlacement="BottomRight"/>
</StackPanel>
</Grid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml.cs

@ -0,0 +1,7 @@
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class VideoGenerationSettingsCard : TemplatedControl { }

98
StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml

@ -0,0 +1,98 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
x:DataType="video:VideoOutputSettingsCardViewModel"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:video="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Video"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages">
<Design.PreviewWith>
<Grid MinWidth="400">
<controls:VideoOutputSettingsCard
DataContext="{x:Static mocks:DesignData.SvdImgToVidConditioningViewModel}" />
</Grid>
</Design.PreviewWith>
<Style Selector="controls|VideoOutputSettingsCard">
<!-- Set Defaults -->
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="8" HorizontalAlignment="{TemplateBinding HorizontalAlignment}">
<Grid Margin="4" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, *">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="Video Output Settings"
FontSize="16"
FontWeight="DemiBold"
Margin="0,0,0,16"
/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Fps}" />
<controls1:NumberBox
Grid.Row="1"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding Fps}"
Margin="8,0,0,0"
SimpleNumberFormat="F0"
SmallChange="1"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Lossless}" />
<CheckBox
Grid.Row="2"
Grid.Column="1"
IsChecked="{Binding Lossless}"
Margin="8,8,0,0"
HorizontalAlignment="Stretch" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VideoQuality}" />
<controls1:NumberBox
Margin="8,8,0,0"
Grid.Row="3"
Grid.Column="1"
SelectionHighlightColor="Transparent"
Value="{Binding Quality}"
SimpleNumberFormat="F0"
SmallChange="1"
Maximum="100"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Inline" />
<TextBlock
Margin="0,8,8,0"
Grid.Row="4"
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VideoOutputMethod}" />
<ComboBox
Grid.Row="4"
Grid.Column="1"
Margin="8,8,0,0"
MinWidth="100"
ItemsSource="{Binding AvailableMethods}"
SelectedIndex="{Binding SelectedMethod}" />
</Grid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml.cs

@ -0,0 +1,7 @@
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class VideoOutputSettingsCard : TemplatedControl { }

40
StabilityMatrix.Avalonia/Converters/EnumAttributeConverter.cs

@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
/// <summary>
/// Converts an enum value to an attribute
/// </summary>
/// <typeparam name="TAttribute">Type of attribute</typeparam>
public class EnumAttributeConverter<TAttribute>(Func<TAttribute, object?>? accessor = null) : IValueConverter
where TAttribute : Attribute
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null)
return null;
if (value is not Enum @enum)
throw new ArgumentException("Value must be an enum");
var field = @enum.GetType().GetField(@enum.ToString());
if (field is null)
throw new ArgumentException("Value must be an enum");
if (field.GetCustomAttributes<TAttribute>().FirstOrDefault() is not { } attribute)
throw new ArgumentException($"Enum value {@enum} does not have attribute {typeof(TAttribute)}");
return accessor is not null ? accessor(attribute) : attribute;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

13
StabilityMatrix.Avalonia/Converters/EnumAttributeConverters.cs

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace StabilityMatrix.Avalonia.Converters;
internal static class EnumAttributeConverters
{
public static EnumAttributeConverter<DisplayAttribute> Display => new();
public static EnumAttributeConverter<DisplayAttribute> DisplayName => new(attribute => attribute.Name);
public static EnumAttributeConverter<DisplayAttribute> DisplayDescription =>
new(attribute => attribute.Description);
}

36
StabilityMatrix.Avalonia/Converters/FileUriConverter.cs

@ -0,0 +1,36 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.Converters;
public class FileUriConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType != typeof(Uri))
{
return null;
}
return value switch
{
string str => new Uri("file://" + str),
IFormattable formattable => new Uri("file://" + formattable.ToString(null, culture)),
_ => null
};
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(string) && value is Uri uri)
{
return uri.ToString().StripStart("file://");
}
return null;
}
}

53
StabilityMatrix.Avalonia/Converters/FuncCommandConverter.cs

@ -0,0 +1,53 @@
using System;
using System.Globalization;
using System.Windows.Input;
using Avalonia.Data.Converters;
using PropertyModels.ComponentModel;
namespace StabilityMatrix.Avalonia.Converters;
/// <summary>
/// Converts an object's named <see cref="Func{TResult}"/> to a <see cref="ICommand"/>.
/// </summary>
public class FuncCommandConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null || parameter is null)
{
return null;
}
// Parameter is the name of the Func<T> to convert.
if (parameter is not string funcName)
{
// ReSharper disable once LocalizableElement
throw new ArgumentException("Parameter must be a string.", nameof(parameter));
}
// Find the Func<T> on the object.
if (value.GetType().GetMethod(funcName) is not { } methodInfo)
{
// ReSharper disable once LocalizableElement
throw new ArgumentException(
$"Method {funcName} not found on {value.GetType().Name}.",
nameof(parameter)
);
}
// Create a delegate from the method info.
var func = (Action)methodInfo.CreateDelegate(typeof(Action), value);
// Create ICommand
var command = ReactiveCommand.Create(func);
return command;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

250
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,15 +4,14 @@ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Semver;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models;
@ -24,8 +23,9 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Avalonia.ViewModels.Inference.Video;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
@ -37,8 +37,10 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Python;
@ -175,12 +177,23 @@ public static class DesignData
};
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = new ObservableCollectionExtended<BasePackage>(
packageFactory.GetAllAvailablePackages()
NewInstallerDialogViewModel = Services.GetRequiredService<PackageInstallBrowserViewModel>();
// NewInstallerDialogViewModel.InferencePackages = new ObservableCollectionExtended<BasePackage>(
// packageFactory.GetPackagesByType(PackageType.SdInference).OrderBy(p => p.InstallerSortOrder)
// );
// NewInstallerDialogViewModel.TrainingPackages = new ObservableCollection<BasePackage>(
// packageFactory.GetPackagesByType(PackageType.SdTraining).OrderBy(p => p.InstallerSortOrder)
// );
PackageInstallDetailViewModel = new PackageInstallDetailViewModel(
packageFactory.GetAllAvailablePackages().FirstOrDefault() as BaseGitPackage,
settingsManager,
notificationService,
null,
null,
null,
null
);
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
ObservableCacheEx.AddOrUpdate(
CheckpointsPageViewModel.CheckpointFoldersCache,
@ -198,6 +211,7 @@ public static class DesignData
PreviewImagePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
UpdateAvailable = true,
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
@ -373,25 +387,27 @@ public static class DesignData
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }
};
ProgressManagerViewModel
.ProgressItems
.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
ProgressManagerViewModel.ProgressItems.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(
Guid.NewGuid(),
"Test File.exe",
new ProgressReport(0.5f, "Downloading...")
)
}
);
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
}
);
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.CurrentVersionText = "v2.0.0";
@ -402,7 +418,10 @@ public static class DesignData
}
[NotNull]
public static InstallerViewModel? InstallerViewModel { get; private set; }
public static PackageInstallBrowserViewModel? NewInstallerDialogViewModel { get; private set; }
[NotNull]
public static PackageInstallDetailViewModel? PackageInstallDetailViewModel { get; private set; }
[NotNull]
public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; }
@ -413,16 +432,64 @@ public static class DesignData
public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService<MainWindowViewModel>();
public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService<LaunchPageViewModel>();
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static HuggingFacePageViewModel HuggingFacePageViewModel =>
Services.GetRequiredService<HuggingFacePageViewModel>();
public static NewOneClickInstallViewModel NewOneClickInstallViewModel =>
Services.GetRequiredService<NewOneClickInstallViewModel>();
public static RecommendedModelsViewModel RecommendedModelsViewModel =>
DialogFactory.Get<RecommendedModelsViewModel>(vm =>
{
vm.Sd15Models = new ObservableCollectionExtended<RecommendedModelItemViewModel>()
{
new()
{
ModelVersion = new CivitModelVersion
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
Images =
[
new CivitImage
{
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
],
},
Author = "bb95"
},
new()
{
ModelVersion = new CivitModelVersion
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
Images =
[
new CivitImage
{
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
],
},
Author = "bb95"
}
};
});
public static OutputsPageViewModel OutputsPageViewModel
{
get
@ -452,7 +519,10 @@ public static class DesignData
vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(
new InstalledPackage[] { UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), }
new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
}
);
vm.PackageCards[0].IsUpdateAvailable = true;
@ -461,6 +531,39 @@ public static class DesignData
}
}
public static PackageExtensionBrowserViewModel PackageExtensionBrowserViewModel =>
DialogFactory.Get<PackageExtensionBrowserViewModel>(vm =>
{
vm.AddExtensions(
[
new PackageExtension
{
Author = "123",
Title = "Cool Extension",
Description = "This is an interesting extension",
Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"),
Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")]
},
new PackageExtension
{
Author = "123",
Title = "Cool Extension",
Description = "This is an interesting extension",
Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"),
Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")]
}
],
[
new InstalledPackageExtension
{
GitRepositoryUrl = "https://github.com/LykosAI/StabilityMatrix",
Paths = [new DirectoryPath("example-dir")]
},
new InstalledPackageExtension { Paths = [new DirectoryPath("example-dir-2")] }
]
);
});
public static CheckpointsPageViewModel CheckpointsPageViewModel =>
Services.GetRequiredService<CheckpointsPageViewModel>();
@ -469,14 +572,21 @@ public static class DesignData
public static SettingsViewModel SettingsViewModel => Services.GetRequiredService<SettingsViewModel>();
public static NewPackageManagerViewModel NewPackageManagerViewModel =>
Services.GetRequiredService<NewPackageManagerViewModel>();
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService<InferenceSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService<MainSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel =>
Services.GetRequiredService<MainSettingsViewModel>();
public static AccountSettingsViewModel AccountSettingsViewModel =>
Services.GetRequiredService<AccountSettingsViewModel>();
public static NotificationSettingsViewModel NotificationSettingsViewModel =>
Services.GetRequiredService<NotificationSettingsViewModel>();
public static UpdateSettingsViewModel UpdateSettingsViewModel
{
get
@ -556,7 +666,10 @@ The gallery images are often inpainted, but you will get something very similar
}
}
};
var sampleViewModel = new ModelVersionViewModel(new HashSet<string> { "ABCD" }, sampleCivitVersions[0]);
var sampleViewModel = new ModelVersionViewModel(
new HashSet<string> { "ABCD" },
sampleCivitVersions[0]
);
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> { sampleViewModel };
@ -630,13 +743,22 @@ The gallery images are often inpainted, but you will get something very similar
vm.OutputProgress.Text = "Sampler 10/30";
});
public static InferenceImageToVideoViewModel InferenceImageToVideoViewModel =>
DialogFactory.Get<InferenceImageToVideoViewModel>(vm =>
{
vm.OutputProgress.Value = 10;
vm.OutputProgress.Maximum = 30;
vm.OutputProgress.Text = "Sampler 10/30";
});
public static InferenceImageToImageViewModel InferenceImageToImageViewModel =>
DialogFactory.Get<InferenceImageToImageViewModel>();
public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel =>
DialogFactory.Get<InferenceImageUpscaleViewModel>();
public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get<PackageImportViewModel>();
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success };
@ -652,6 +774,8 @@ The gallery images are often inpainted, but you will get something very similar
});
public static SeedCardViewModel SeedCardViewModel => new();
public static SvdImgToVidConditioningViewModel SvdImgToVidConditioningViewModel => new();
public static VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel => new();
public static SamplerCardViewModel SamplerCardViewModel =>
DialogFactory.Get<SamplerCardViewModel>(vm =>
@ -681,6 +805,9 @@ The gallery images are often inpainted, but you will get something very similar
public static ModelCardViewModel ModelCardViewModel => DialogFactory.Get<ModelCardViewModel>();
public static ImgToVidModelCardViewModel ImgToVidModelCardViewModel =>
DialogFactory.Get<ImgToVidModelCardViewModel>();
public static ImageGalleryCardViewModel ImageGalleryCardViewModel =>
DialogFactory.Get<ImageGalleryCardViewModel>(vm =>
{
@ -706,7 +833,8 @@ The gallery images are often inpainted, but you will get something very similar
);
});
public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get<ImageFolderCardViewModel>();
public static ImageFolderCardViewModel ImageFolderCardViewModel =>
DialogFactory.Get<ImageFolderCardViewModel>();
public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get<FreeUCardViewModel>();
@ -759,7 +887,8 @@ The gallery images are often inpainted, but you will get something very similar
public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get<UpscalerCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel =>
DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption =>
DialogFactory.Get<BatchSizeCardViewModel>(vm =>
@ -790,6 +919,42 @@ The gallery images are often inpainted, but you will get something very similar
}
}
public static IEnumerable<HybridModelFile> SampleHybridModels { get; } =
[
HybridModelFile.FromLocal(
new LocalModelFile
{
SharedFolderType = SharedFolderType.StableDiffusion,
RelativePath = "art_shaper_v8.safetensors",
PreviewImageFullPath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512",
ConnectedModelInfo = new ConnectedModelInfo
{
ModelName = "Art Shaper (very long name example)",
VersionName = "Style v8 (very long name)"
}
}
),
HybridModelFile.FromLocal(
new LocalModelFile
{
SharedFolderType = SharedFolderType.StableDiffusion,
RelativePath = "background_arts.safetensors",
PreviewImageFullPath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/71c81ddf-d8c3-46b4-843d-9f8f20a9254a/width=512",
ConnectedModelInfo = new ConnectedModelInfo
{
ModelName = "Background Arts",
VersionName = "Anime Style v10"
}
}
),
HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
HybridModelFile.FromRemote("sample-model.pt"),
];
public static HybridModelFile SampleHybridModel => SampleHybridModels.First();
public static ImageViewerViewModel ImageViewerViewModel =>
DialogFactory.Get<ImageViewerViewModel>(vm =>
{
@ -816,7 +981,8 @@ The gallery images are often inpainted, but you will get something very similar
public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel =>
DialogFactory.Get<InferenceConnectionHelpViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel =>
DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel_WithImage =>
DialogFactory.Get<SelectImageCardViewModel>(vm =>
@ -829,12 +995,17 @@ The gallery images are often inpainted, but you will get something very similar
});
public static ImageSource SampleImageSource =>
new(new Uri("https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"))
new(
new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
)
{
Label = "Test Image"
};
public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get<ControlNetCardViewModel>();
public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get<ControlNetCardViewModel>();
public static Indexer Types { get; } = new();
@ -844,7 +1015,8 @@ The gallery images are often inpainted, but you will get something very similar
{
get
{
var type = Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found");
var type =
Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found");
try
{
return Services.GetService(type);

22
StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
@ -25,9 +26,26 @@ public class MockModelIndexService : IModelIndexService
}
/// <inheritdoc />
public Task<IReadOnlyList<LocalModelFile>> GetModelsOfType(SharedFolderType type)
public Task<IEnumerable<LocalModelFile>> FindAsync(SharedFolderType type)
{
return Task.FromResult<IReadOnlyList<LocalModelFile>>(new List<LocalModelFile>());
return Task.FromResult(Enumerable.Empty<LocalModelFile>());
}
/// <inheritdoc />
public Task<IEnumerable<LocalModelFile>> FindByHashAsync(string hashBlake3)
{
return Task.FromResult(Enumerable.Empty<LocalModelFile>());
}
/// <inheritdoc />
public Task<bool> RemoveModelAsync(LocalModelFile model)
{
return Task.FromResult(false);
}
public Task CheckModelsForUpdates()
{
return Task.CompletedTask;
}
/// <inheritdoc />

49
StabilityMatrix.Avalonia/DialogHelper.cs

@ -23,15 +23,15 @@ using NLog;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Services;
using TextMateSharp.Grammars;
using Process = FuzzySharp.Process;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia;
@ -48,11 +48,7 @@ public static class DialogHelper
IReadOnlyList<TextBoxField> textFields
)
{
return CreateTextEntryDialog(
title,
new MarkdownScrollViewer { Markdown = description },
textFields
);
return CreateTextEntryDialog(title, new MarkdownScrollViewer { Markdown = description }, textFields);
}
/// <summary>
@ -80,11 +76,7 @@ public static class DialogHelper
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Star),
new RowDefinition(GridLength.Auto)
},
RowDefinitions = { new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto) },
Children = { markdown, image }
};
@ -109,18 +101,14 @@ public static class DialogHelper
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star)
},
RowDefinitions = { new RowDefinition(GridLength.Auto), new RowDefinition(GridLength.Star) },
Children = { content, stackPanel }
};
grid.Loaded += (_, _) =>
{
// Focus first TextBox
var firstTextBox = stackPanel.Children
.OfType<StackPanel>()
var firstTextBox = stackPanel
.Children.OfType<StackPanel>()
.FirstOrDefault()
.FindDescendantOfType<TextBox>();
firstTextBox!.Focus();
@ -254,16 +242,16 @@ public static class DialogHelper
Content = viewer,
CloseButtonText = Resources.Action_Close,
IsPrimaryButtonEnabled = false,
MinDialogWidth = 800,
MaxDialogHeight = 1000,
MaxDialogWidth = 1000
};
}
/// <summary>
/// Create a dialog for displaying an ApiException
/// </summary>
public static BetterContentDialog CreateApiExceptionDialog(
ApiException exception,
string? title = null
)
public static BetterContentDialog CreateApiExceptionDialog(ApiException exception, string? title = null)
{
Dispatcher.UIThread.VerifyAccess();
@ -275,9 +263,7 @@ public static class DialogHelper
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
@ -354,9 +340,7 @@ public static class DialogHelper
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
@ -437,8 +421,7 @@ public static class DialogHelper
{
Dispatcher.UIThread.VerifyAccess();
var title =
exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
var title = exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
// Get the index of the error
var errorIndex = exception.TextOffset;
@ -526,7 +509,7 @@ public static class DialogHelper
models.Select(m => m.FileNameWithoutExtension)
);
if (result.Score > 40)
if (result is { Score: > 40 })
{
mainGrid.Children.Add(
new InfoBar

67
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -20,15 +20,17 @@ public static class ComfyNodeBuilderExtensions
int? batchIndex = null
)
{
var emptyLatent = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.EmptyLatentImage
{
Name = "EmptyLatentImage",
BatchSize = batchSize,
Height = height,
Width = width
}
);
var emptyLatent = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.EmptyLatentImage
{
Name = "EmptyLatentImage",
BatchSize = batchSize,
Height = height,
Width = width
}
);
builder.Connections.Primary = emptyLatent.Output;
builder.Connections.PrimarySize = new Size(width, height);
@ -36,7 +38,8 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder.Nodes
builder.Connections.Primary = builder
.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
@ -64,9 +67,9 @@ public static class ComfyNodeBuilderExtensions
var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached());
// Load source
var loadImage = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath }
);
var loadImage = builder
.Nodes
.AddTypedNode(new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath });
builder.Connections.Primary = loadImage.Output1;
builder.Connections.PrimarySize = imageSize;
@ -74,7 +77,8 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder.Nodes
builder.Connections.Primary = builder
.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
@ -93,24 +97,25 @@ public static class ComfyNodeBuilderExtensions
if (builder.Connections.Primary is null)
throw new ArgumentException("No Primary");
var image = builder.Connections.Primary.Match(
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.RefinerVAE
?? builder.Connections.BaseVAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
);
var image = builder
.Connections
.Primary
.Match(
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.Refiner.VAE
?? builder.Connections.Base.VAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
);
var previewImage = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = builder.Nodes.GetUniqueName("SaveImage"),
Images = image
}
);
var previewImage = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.PreviewImage { Name = builder.Nodes.GetUniqueName("SaveImage"), Images = image }
);
builder.Connections.OutputNodes.Add(previewImage);

20
StabilityMatrix.Avalonia/Extensions/NotificationLevelExtensions.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Controls.Notifications;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Extensions;
public static class NotificationLevelExtensions
{
public static NotificationType ToNotificationType(this NotificationLevel level)
{
return level switch
{
NotificationLevel.Information => NotificationType.Information,
NotificationLevel.Success => NotificationType.Success,
NotificationLevel.Warning => NotificationType.Warning,
NotificationLevel.Error => NotificationType.Error,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
};
}
}

54
StabilityMatrix.Avalonia/Extensions/NotificationServiceExtensions.cs

@ -0,0 +1,54 @@
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using DesktopNotifications;
using NLog;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Extensions;
public static class NotificationServiceExtensions
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public static void OnPackageInstallCompleted(
this INotificationService notificationService,
IPackageModificationRunner runner
)
{
OnPackageInstallCompletedAsync(notificationService, runner)
.SafeFireAndForget(ex => Logger.Error(ex, "Error Showing Notification"));
}
private static async Task OnPackageInstallCompletedAsync(
this INotificationService notificationService,
IPackageModificationRunner runner
)
{
if (runner.Failed)
{
Logger.Error(runner.Exception, "Error Installing Package");
await notificationService.ShowAsync(
NotificationKey.Package_Install_Failed,
new Notification
{
Title = runner.ModificationFailedTitle,
Body = runner.ModificationFailedMessage
}
);
}
else
{
await notificationService.ShowAsync(
NotificationKey.Package_Install_Completed,
new Notification
{
Title = runner.ModificationCompleteTitle,
Body = runner.ModificationCompleteMessage
}
);
}
}
}

76
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
@ -10,7 +11,9 @@ using NLog;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
@ -61,6 +64,55 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
return isGitInstalled == true;
}
public Task InstallPackageRequirements(BasePackage package, IProgress<ProgressReport>? progress = null) =>
InstallPackageRequirements(package.Prerequisites.ToList(), progress);
public async Task InstallPackageRequirements(
List<PackagePrerequisite> prerequisites,
IProgress<ProgressReport>? progress = null
)
{
await UnpackResourcesIfNecessary(progress);
if (prerequisites.Contains(PackagePrerequisite.Python310))
{
await InstallPythonIfNecessary(progress);
await InstallVirtualenvIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Git))
{
await InstallGitIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Node))
{
await InstallNodeIfNecessary(progress);
}
}
private async Task InstallVirtualenvIfNecessary(IProgress<ProgressReport>? progress = null)
{
// python stuff
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled)
{
progress?.Report(
new ProgressReport(-1f, "Installing Python prerequisites...", isIndeterminate: true)
);
await pyRunner.Initialize().ConfigureAwait(false);
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip().ConfigureAwait(false);
}
if (!PyRunner.VenvInstalled)
{
await pyRunner.InstallPackage("virtualenv").ConfigureAwait(false);
}
}
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await UnpackResourcesIfNecessary(progress);
@ -70,15 +122,9 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
{
// Array of (asset_uri, extract_to)
var assets = new[]
{
(Assets.SevenZipExecutable, AssetsDir),
(Assets.SevenZipLicense, AssetsDir),
};
var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir), };
progress?.Report(
new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)
);
progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true));
Directory.CreateDirectory(AssetsDir);
foreach (var (asset, extractDir) in assets)
@ -86,9 +132,7 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
await asset.ExtractToDir(extractDir);
}
progress?.Report(
new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)
);
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false));
}
public async Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null)
@ -150,8 +194,7 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
if (result.ExitCode != 0)
{
Logger.Error(
"Git command [{Command}] failed with exit code "
+ "{ExitCode}:\n{StdOut}\n{StdErr}",
"Git command [{Command}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
command,
result.ExitCode,
result.StandardOutput,
@ -239,9 +282,9 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
progress?.Report(new ProgressReport(1, "Installing Python", isIndeterminate: false));
}
public Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
public Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null)
{
throw new NotImplementedException();
return ProcessRunner.RunBashCommand(args.Prepend("git").ToArray(), workingDirectory ?? "");
}
[SupportedOSPlatform("Linux")]
@ -249,7 +292,8 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
public async Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null
)
{
var command = args.Prepend([NpmPath]);

17
StabilityMatrix.Avalonia/Helpers/UriHandler.cs

@ -58,17 +58,19 @@ public class UriHandler
public void RegisterUriScheme()
{
// Not supported on macos
if (Compat.IsWindows)
{
RegisterUriSchemeWin();
}
else
else if (Compat.IsLinux)
{
// Try to register on unix but ignore errors
// Try to register on linux but ignore errors
// Library does not support some distros
try
{
RegisterUriSchemeUnix();
RegisterUriSchemeLinux();
}
catch (Exception e)
{
@ -98,9 +100,14 @@ public class UriHandler
}
}
private void RegisterUriSchemeUnix()
[SupportedOSPlatform("linux")]
private void RegisterUriSchemeLinux()
{
var service = URISchemeServiceFactory.GetURISchemeSerivce(Scheme, Description, Compat.AppCurrentPath.FullPath);
var service = URISchemeServiceFactory.GetURISchemeSerivce(
Scheme,
Description,
Compat.AppCurrentPath.FullPath
);
service.Set();
}
}

113
StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs

@ -1,15 +1,20 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
using NLog;
using Octokit;
using PropertyModels.Extensions;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Helpers;
@ -22,6 +27,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
private readonly IGitHubClient gitHubClient;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe";
private const string TkinterDownloadUrl =
@ -59,12 +65,14 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
public WindowsPrerequisiteHelper(
IGitHubClient gitHubClient,
IDownloadService downloadService,
ISettingsManager settingsManager
ISettingsManager settingsManager,
IPyRunner pyRunner
)
{
this.gitHubClient = gitHubClient;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.pyRunner = pyRunner;
}
public async Task RunGit(
@ -99,29 +107,28 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
result.EnsureSuccessExitCode();
}
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
public Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null)
{
var process = await ProcessRunner.GetProcessOutputAsync(
return ProcessRunner.GetProcessResultAsync(
GitExePath,
string.Join(" ", args),
args,
workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string>
{
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
}
);
return process;
}
public async Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null
)
{
var result = await ProcessRunner
.GetProcessResultAsync(NodeExistsPath, args, workingDirectory)
.GetProcessResultAsync(NodeExistsPath, args, workingDirectory, envVars)
.ConfigureAwait(false);
result.EnsureSuccessExitCode();
@ -129,6 +136,38 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
onProcessOutput?.Invoke(ProcessOutput.FromStdErrLine(result.StandardError));
}
public Task InstallPackageRequirements(BasePackage package, IProgress<ProgressReport>? progress = null) =>
InstallPackageRequirements(package.Prerequisites.ToList(), progress);
public async Task InstallPackageRequirements(
List<PackagePrerequisite> prerequisites,
IProgress<ProgressReport>? progress = null
)
{
await UnpackResourcesIfNecessary(progress);
if (prerequisites.Contains(PackagePrerequisite.Python310))
{
await InstallPythonIfNecessary(progress);
await InstallVirtualenvIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Git))
{
await InstallGitIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.VcRedist))
{
await InstallVcRedistIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Node))
{
await InstallNodeIfNecessary(progress);
}
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await InstallVcRedistIfNecessary(progress);
@ -141,15 +180,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
{
// Array of (asset_uri, extract_to)
var assets = new[]
{
(Assets.SevenZipExecutable, AssetsDir),
(Assets.SevenZipLicense, AssetsDir),
};
var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir), };
progress?.Report(
new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)
);
progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true));
Directory.CreateDirectory(AssetsDir);
foreach (var (asset, extractDir) in assets)
@ -157,9 +190,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
await asset.ExtractToDir(extractDir);
}
progress?.Report(
new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)
);
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false));
}
public async Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null)
@ -275,17 +306,35 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
}
}
private async Task InstallVirtualenvIfNecessary(IProgress<ProgressReport>? progress = null)
{
// python stuff
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled)
{
progress?.Report(
new ProgressReport(-1f, "Installing Python prerequisites...", isIndeterminate: true)
);
await pyRunner.Initialize().ConfigureAwait(false);
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip().ConfigureAwait(false);
}
if (!PyRunner.VenvInstalled)
{
await pyRunner.InstallPackage("virtualenv").ConfigureAwait(false);
}
}
}
[SupportedOSPlatform("windows")]
public async Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (!Directory.Exists(TkinterExistsPath))
{
Logger.Info("Downloading Tkinter");
await downloadService.DownloadToFileAsync(
TkinterDownloadUrl,
TkinterZipPath,
progress: progress
);
await downloadService.DownloadToFileAsync(TkinterDownloadUrl, TkinterZipPath, progress: progress);
progress?.Report(
new ProgressReport(
progress: 1f,
@ -300,11 +349,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
}
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Tkinter install complete",
type: ProgressType.Generic
)
new ProgressReport(progress: 1f, message: "Tkinter install complete", type: ProgressType.Generic)
);
}
@ -338,10 +383,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
public async Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null)
{
var registry = Registry.LocalMachine;
var key = registry.OpenSubKey(
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64",
false
);
var key = registry.OpenSubKey(@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false);
if (key != null)
{
var buildId = Convert.ToUInt32(key.GetValue("Bld"));
@ -375,10 +417,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
message: "Installing prerequisites..."
)
);
var process = ProcessRunner.StartAnsiProcess(
VcRedistDownloadPath,
"/install /quiet /norestart"
);
var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart");
await process.WaitForExitAsync();
progress?.Report(
new ProgressReport(

12
StabilityMatrix.Avalonia/Languages/Cultures.cs

@ -13,7 +13,10 @@ public static class Cultures
public static CultureInfo? Current => Resources.Culture;
public static readonly Dictionary<string, CultureInfo> SupportedCulturesByCode = new Dictionary<string, CultureInfo>
public static readonly Dictionary<string, CultureInfo> SupportedCulturesByCode = new Dictionary<
string,
CultureInfo
>
{
["en-US"] = Default,
["ja-JP"] = new("ja-JP"),
@ -23,10 +26,13 @@ public static class Cultures
["fr-FR"] = new("fr-FR"),
["es"] = new("es"),
["ru-RU"] = new("ru-RU"),
["tr-TR"] = new("tr-TR")
["tr-TR"] = new("tr-TR"),
["de"] = new("de"),
["pt-PT"] = new("pt-PT")
};
public static IReadOnlyList<CultureInfo> SupportedCultures => SupportedCulturesByCode.Values.ToImmutableList();
public static IReadOnlyList<CultureInfo> SupportedCultures =>
SupportedCulturesByCode.Values.ToImmutableList();
public static CultureInfo GetSupportedCultureOrDefault(string? cultureCode)
{

191
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -167,6 +167,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Copy Details.
/// </summary>
public static string Action_CopyDetails {
get {
return ResourceManager.GetString("Action_CopyDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy Trigger Words.
/// </summary>
@ -203,6 +212,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Download.
/// </summary>
public static string Action_Download {
get {
return ResourceManager.GetString("Action_Download", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
@ -221,6 +239,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Hide.
/// </summary>
public static string Action_Hide {
get {
return ResourceManager.GetString("Action_Hide", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import.
/// </summary>
@ -707,6 +734,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Augmentation Level.
/// </summary>
public static string Label_AugmentationLevel {
get {
return ResourceManager.GetString("Label_AugmentationLevel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Auto Completion.
/// </summary>
@ -833,6 +869,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to CLIP Skip.
/// </summary>
public static string Label_CLIPSkip {
get {
return ResourceManager.GetString("Label_CLIPSkip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Close dialog when finished.
/// </summary>
@ -1166,6 +1211,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Find in Model Browser.
/// </summary>
public static string Label_FindInModelBrowser {
get {
return ResourceManager.GetString("Label_FindInModelBrowser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to First Page.
/// </summary>
@ -1184,6 +1238,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Frames Per Second.
/// </summary>
public static string Label_Fps {
get {
return ResourceManager.GetString("Label_Fps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Frames.
/// </summary>
public static string Label_Frames {
get {
return ResourceManager.GetString("Label_Frames", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to General.
/// </summary>
@ -1229,6 +1301,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Image to Video.
/// </summary>
public static string Label_ImageToVideo {
get {
return ResourceManager.GetString("Label_ImageToVideo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image Viewer.
/// </summary>
@ -1328,6 +1409,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Installed.
/// </summary>
public static string Label_Installed {
get {
return ResourceManager.GetString("Label_Installed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installing.
/// </summary>
@ -1427,6 +1517,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Lossless.
/// </summary>
public static string Label_Lossless {
get {
return ResourceManager.GetString("Label_Lossless", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Min CFG.
/// </summary>
public static string Label_MinCfg {
get {
return ResourceManager.GetString("Label_MinCfg", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Missing Image File.
/// </summary>
@ -1481,6 +1589,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Motion Bucket ID.
/// </summary>
public static string Label_MotionBucketId {
get {
return ResourceManager.GetString("Label_MotionBucketId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Networks (Lora / LyCORIS).
/// </summary>
@ -1526,6 +1643,33 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to No extensions found..
/// </summary>
public static string Label_NoExtensionsFound {
get {
return ResourceManager.GetString("Label_NoExtensionsFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string Label_NotificationOption_None {
get {
return ResourceManager.GetString("Label_NotificationOption_None", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Notifications.
/// </summary>
public static string Label_Notifications {
get {
return ResourceManager.GetString("Label_Notifications", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} images selected.
/// </summary>
@ -1769,6 +1913,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Recommended Models.
/// </summary>
public static string Label_RecommendedModels {
get {
return ResourceManager.GetString("Label_RecommendedModels", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to While your package is installing, here are some models we recommend to help you get started..
/// </summary>
public static string Label_RecommendedModelsSubText {
get {
return ResourceManager.GetString("Label_RecommendedModelsSubText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Refiner.
/// </summary>
@ -2156,6 +2318,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Method.
/// </summary>
public static string Label_VideoOutputMethod {
get {
return ResourceManager.GetString("Label_VideoOutputMethod", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Quality.
/// </summary>
public static string Label_VideoQuality {
get {
return ResourceManager.GetString("Label_VideoQuality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Waiting to connect....
/// </summary>
@ -2291,6 +2471,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Check the progress of your package installations and model downloads here..
/// </summary>
public static string TeachingTip_DownloadsExplanation {
get {
return ResourceManager.GetString("TeachingTip_DownloadsExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here.
/// </summary>
@ -2310,7 +2499,7 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to Choose your preferred interface and click Install to get started.
/// Looks up a localized string similar to Choose your preferred interface to get started.
/// </summary>
public static string Text_OneClickInstaller_SubHeader {
get {

933
StabilityMatrix.Avalonia/Languages/Resources.de.resx

@ -0,0 +1,933 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Action_Launch" xml:space="preserve">
<value>Starten</value>
</data>
<data name="Action_Quit" xml:space="preserve">
<value>Beenden</value>
</data>
<data name="Action_Save" xml:space="preserve">
<value>Speichern</value>
</data>
<data name="Action_Cancel" xml:space="preserve">
<value>Abbrechen</value>
</data>
<data name="Label_Language" xml:space="preserve">
<value>Sprache</value>
</data>
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve">
<value>Ein Neustart ist nötig, um Sprachänderungen vorzunehmen</value>
</data>
<data name="Action_Relaunch" xml:space="preserve">
<value>Neustarten</value>
</data>
<data name="Action_RelaunchLater" xml:space="preserve">
<value>Später Neustarten</value>
</data>
<data name="Label_RelaunchRequired" xml:space="preserve">
<value>Neustart benötigt</value>
</data>
<data name="Label_UnknownPackage" xml:space="preserve">
<value>Unbekanntes Paket</value>
</data>
<data name="Action_Import" xml:space="preserve">
<value>Importieren</value>
</data>
<data name="Label_PackageType" xml:space="preserve">
<value>Pakettyp</value>
</data>
<data name="Label_Version" xml:space="preserve">
<value>Version</value>
</data>
<data name="Label_VersionType" xml:space="preserve">
<value>Versionstyp</value>
</data>
<data name="Label_Releases" xml:space="preserve">
<value>Veröffentlichungen</value>
</data>
<data name="Label_Branches" xml:space="preserve">
<value>Zweige</value>
</data>
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>Checkpoints hierherziehen zum importieren</value>
</data>
<data name="Label_Emphasis" xml:space="preserve">
<value>Betonung</value>
</data>
<data name="Label_Deemphasis" xml:space="preserve">
<value>Negativ Betonung</value>
</data>
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve">
<value>Embeddings</value>
</data>
<data name="Label_NetworksLoraOrLycoris" xml:space="preserve">
<value>Netzwerk (Lora / LyCORIS)</value>
</data>
<data name="Label_Comments" xml:space="preserve">
<value>Kommentare</value>
</data>
<data name="Label_ShowPixelGridAtHighZoomLevels" xml:space="preserve">
<value>Pixel-Gitter bei hohen Zoomstufen anzeigen</value>
</data>
<data name="Label_Steps" xml:space="preserve">
<value>Schritte</value>
</data>
<data name="Label_StepsBase" xml:space="preserve">
<value>Schritte - Base</value>
</data>
<data name="Label_StepsRefiner" xml:space="preserve">
<value>Schritte - Refiner</value>
</data>
<data name="Label_CFGScale" xml:space="preserve">
<value>CFG Wert</value>
</data>
<data name="Label_DenoisingStrength" xml:space="preserve">
<value>Entrauschungsstärke</value>
</data>
<data name="Label_Width" xml:space="preserve">
<value>Breite</value>
</data>
<data name="Label_Height" xml:space="preserve">
<value>Höhe</value>
</data>
<data name="Label_Refiner" xml:space="preserve">
<value>Refiner</value>
</data>
<data name="Label_VAE" xml:space="preserve">
<value>VAE</value>
</data>
<data name="Label_Model" xml:space="preserve">
<value>Modell</value>
</data>
<data name="Action_Connect" xml:space="preserve">
<value>Verbinden</value>
</data>
<data name="Label_ConnectingEllipsis" xml:space="preserve">
<value>Verbindet...</value>
</data>
<data name="Action_Close" xml:space="preserve">
<value>Schließen</value>
</data>
<data name="Label_WaitingToConnectEllipsis" xml:space="preserve">
<value>Warten auf die Verbindung...</value>
</data>
<data name="Label_UpdateAvailable" xml:space="preserve">
<value>Updates verfügbar</value>
</data>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>Werde ein Patreon</value>
</data>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Trete dem Discord Server bei</value>
</data>
<data name="Label_Downloads" xml:space="preserve">
<value>Downloads</value>
</data>
<data name="Action_Install" xml:space="preserve">
<value>Installieren</value>
</data>
<data name="Label_SkipSetup" xml:space="preserve">
<value>Erstmalige Einstellung überspringen</value>
</data>
<data name="Label_UnexpectedErrorOccurred" xml:space="preserve">
<value>Ein unerwarteter Fehler ist aufgetreten</value>
</data>
<data name="Action_ExitApplication" xml:space="preserve">
<value>Beende die Applikation</value>
</data>
<data name="Label_DisplayName" xml:space="preserve">
<value>Anzeigename</value>
</data>
<data name="Label_InstallationWithThisNameExists" xml:space="preserve">
<value>Eine Installation mit diesem namen existiert bereits.</value>
</data>
<data name="Label_PleaseChooseDifferentName" xml:space="preserve">
<value>Bitte wähle einen anderen namen oder ändere den Installationsordner.</value>
</data>
<data name="Label_AdvancedOptions" xml:space="preserve">
<value>Erweiterte Optionen</value>
</data>
<data name="Label_Commit" xml:space="preserve">
<value>Commit</value>
</data>
<data name="Label_SharedModelFolderStrategy" xml:space="preserve">
<value>Geteilte Modell Ordnerstrategie</value>
</data>
<data name="Label_PyTorchVersion" xml:space="preserve">
<value>PyTorch Version</value>
</data>
<data name="Label_CloseDialogWhenFinished" xml:space="preserve">
<value>Dialog nach Beendigung schließen</value>
</data>
<data name="Label_DataDirectory" xml:space="preserve">
<value>Daten Ordner</value>
</data>
<data name="Label_DataDirectoryExplanation" xml:space="preserve">
<value>Hier werden die Applikationsdaten (Modell Checkpoints, Web UIs, etc.) installiert.</value>
</data>
<data name="Label_FatWarning" xml:space="preserve">
<value>Bei der Verwendung eines FAT32- oder exFAT-Laufwerks können Fehler auftreten. Wählen Sie ein anderes Laufwerk, um ein reibungsloseres Arbeiten zu ermöglichen.</value>
</data>
<data name="Label_PortableMode" xml:space="preserve">
<value>Tragbarer Modus</value>
</data>
<data name="Label_PortableModeExplanation" xml:space="preserve">
<value>Im portablen Modus werden alle Daten und Einstellungen in demselben Verzeichnis wie die Anwendung gespeichert. Sie können die Anwendung mit ihrem &apos;Daten&apos;-Ordner an einen anderen Ort oder auf einen anderen Computer verschieben.</value>
</data>
<data name="Action_Continue" xml:space="preserve">
<value>Weiter</value>
</data>
<data name="Label_PreviousImage" xml:space="preserve">
<value>Vorheriges Bild</value>
</data>
<data name="Label_NextImage" xml:space="preserve">
<value>Nächstes Bild</value>
</data>
<data name="Label_ModelDescription" xml:space="preserve">
<value>Modellbeschreibung</value>
</data>
<data name="Label_NewVersionAvailable" xml:space="preserve">
<value>Eine neue Version von Stability Matrix ist verfügbar!</value>
</data>
<data name="Label_ImportLatest" xml:space="preserve">
<value>Neueste Importieren -</value>
</data>
<data name="Label_AllVersions" xml:space="preserve">
<value>Alle Versionen</value>
</data>
<data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Suche modelle, #tags, or @nutzer</value>
</data>
<data name="Action_Search" xml:space="preserve">
<value>Suchen</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>Sortieren</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Periode</value>
</data>
<data name="Label_ModelType" xml:space="preserve">
<value>Modelltyp</value>
</data>
<data name="Label_BaseModel" xml:space="preserve">
<value>Basis Modell</value>
</data>
<data name="Label_ShowNsfwContent" xml:space="preserve">
<value>Zeige NSFW Inhalte</value>
</data>
<data name="Label_DataProvidedByCivitAi" xml:space="preserve">
<value>Daten bereitgestellt von CivitAI</value>
</data>
<data name="Label_Page" xml:space="preserve">
<value>Seite</value>
</data>
<data name="Label_FirstPage" xml:space="preserve">
<value>Erste Seite</value>
</data>
<data name="Label_PreviousPage" xml:space="preserve">
<value>Vorherige Seite</value>
</data>
<data name="Label_NextPage" xml:space="preserve">
<value>Nächste Seite</value>
</data>
<data name="Label_LastPage" xml:space="preserve">
<value>Letzte Seite</value>
</data>
<data name="Action_Rename" xml:space="preserve">
<value>Umbenennen</value>
</data>
<data name="Action_Delete" xml:space="preserve">
<value>Löschen</value>
</data>
<data name="Action_OpenOnCivitAi" xml:space="preserve">
<value>Öffnen in CivitAI</value>
</data>
<data name="Label_ConnectedModel" xml:space="preserve">
<value>Verbundenes Modell</value>
</data>
<data name="Label_LocalModel" xml:space="preserve">
<value>Lokales Modell</value>
</data>
<data name="Action_ShowInExplorer" xml:space="preserve">
<value>Im Explorer anzeigen</value>
</data>
<data name="Action_New" xml:space="preserve">
<value>Neu</value>
</data>
<data name="Label_Folder" xml:space="preserve">
<value>Ordner</value>
</data>
<data name="Label_DropFileToImport" xml:space="preserve">
<value>Datei zum Importieren hier ablegen</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Importieren mit Metadaten</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Suche nach verbundenen Metadaten bei neuen lokalen Importen</value>
</data>
<data name="Label_Indexing" xml:space="preserve">
<value>Indizierung...</value>
</data>
<data name="Label_ModelsFolder" xml:space="preserve">
<value>Modellordner</value>
</data>
<data name="Label_Categories" xml:space="preserve">
<value>Kategorien</value>
</data>
<data name="Label_LetsGetStarted" xml:space="preserve">
<value>Lass uns starten</value>
</data>
<data name="Label_ReadAndAgree" xml:space="preserve">
<value>Ich habe gelesen und akzeptiere die</value>
</data>
<data name="Label_LicenseAgreement" xml:space="preserve">
<value>Lizensbestimmungen.</value>
</data>
<data name="Label_FindConnectedMetadata" xml:space="preserve">
<value>Finde verbundene Metadaten</value>
</data>
<data name="Label_ShowModelImages" xml:space="preserve">
<value>Zeige Modell Bilder</value>
</data>
<data name="Label_Appearance" xml:space="preserve">
<value>Aussehen</value>
</data>
<data name="Label_Theme" xml:space="preserve">
<value>Thema</value>
</data>
<data name="Label_CheckpointManager" xml:space="preserve">
<value>Checkpoint Manager</value>
</data>
<data name="Label_RemoveSymlinksOnShutdown" xml:space="preserve">
<value>Symbolische Links auf gemeinsame Checkpoints beim Herunterfahren entfernen</value>
</data>
<data name="Label_RemoveSymlinksOnShutdown_Details" xml:space="preserve">
<value>Wählen Sie diese Option, wenn Sie Probleme beim Verschieben von Stability Matrix auf ein anderes Laufwerk haben</value>
</data>
<data name="Label_ResetCheckpointsCache" xml:space="preserve">
<value>Checkpoint-Cache zurücksetzen</value>
</data>
<data name="Label_ResetCheckpointsCache_Details" xml:space="preserve">
<value>Stellt den installierten Checkpoint-Cache wieder her. Wird verwendet, wenn Prüfpunkte im Modell-Browser falsch beschriftet sind</value>
</data>
<data name="Label_PackageEnvironment" xml:space="preserve">
<value>Paket Umgebung</value>
</data>
<data name="Action_Edit" xml:space="preserve">
<value>Bearbeiten</value>
</data>
<data name="Label_EnvironmentVariables" xml:space="preserve">
<value>Umgebungsvariablen</value>
</data>
<data name="Label_EmbeddedPython" xml:space="preserve">
<value>Eingebettetes Python</value>
</data>
<data name="Action_CheckVersion" xml:space="preserve">
<value>Version überprüfen</value>
</data>
<data name="Label_Integrations" xml:space="preserve">
<value>Integrationen</value>
</data>
<data name="Label_DiscordRichPresence" xml:space="preserve">
<value>Discord Rich Presence</value>
</data>
<data name="Label_System" xml:space="preserve">
<value>System</value>
</data>
<data name="Label_AddToStartMenu" xml:space="preserve">
<value>Füge Stability Matrix zum Startmenü hinzu</value>
</data>
<data name="Label_AddToStartMenu_Details" xml:space="preserve">
<value>Verwendet den aktuellen Standort der Anwendung, Sie können dies erneut ausführen, wenn Sie die Anwendung verschieben</value>
</data>
<data name="Label_OnlyAvailableOnWindows" xml:space="preserve">
<value>Nur auf Windows verfügbar</value>
</data>
<data name="Action_AddForCurrentUser" xml:space="preserve">
<value>Hinzufügen für aktuellen nutzer</value>
</data>
<data name="Action_AddForAllUsers" xml:space="preserve">
<value>Hinzufügen für alle Nutzer</value>
</data>
<data name="Label_SelectNewDataDirectory" xml:space="preserve">
<value>Wähle einen neuen Daten Ordner aus</value>
</data>
<data name="Label_SelectNewDataDirectory_Details" xml:space="preserve">
<value>Verschiebt keine existierenden Daten</value>
</data>
<data name="Action_SelectDirectory" xml:space="preserve">
<value>Ordner auswählen</value>
</data>
<data name="Label_About" xml:space="preserve">
<value>Über</value>
</data>
<data name="Label_StabilityMatrix" xml:space="preserve">
<value>Stability Matrix</value>
</data>
<data name="Label_LicenseAndOpenSourceNotices" xml:space="preserve">
<value>Lizenz und Open Source Notizen</value>
</data>
<data name="TeachingTip_ClickLaunchToGetStarted" xml:space="preserve">
<value>Klicken Sie auf Start, um loszulegen!</value>
</data>
<data name="Action_Stop" xml:space="preserve">
<value>Stopp</value>
</data>
<data name="Action_SendInput" xml:space="preserve">
<value>Eingaben senden</value>
</data>
<data name="Label_Input" xml:space="preserve">
<value>Eingaben</value>
</data>
<data name="Action_Send" xml:space="preserve">
<value>Senden</value>
</data>
<data name="Label_InputRequired" xml:space="preserve">
<value>Eingaben benötigt</value>
</data>
<data name="Label_ConfirmQuestion" xml:space="preserve">
<value>Bestätigen?</value>
</data>
<data name="Action_Yes" xml:space="preserve">
<value>Ja</value>
</data>
<data name="Label_No" xml:space="preserve">
<value>Nein</value>
</data>
<data name="Action_OpenWebUI" xml:space="preserve">
<value>Web UI öffnen</value>
</data>
<data name="Text_WelcomeToStabilityMatrix" xml:space="preserve">
<value>Willkommen zu Stability Matrix!</value>
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Wählen Sie Ihre bevorzugte Schnittstelle und klicken Sie auf Installieren, um loszulegen.</value>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Installiert</value>
</data>
<data name="Text_ProceedingToLaunchPage" xml:space="preserve">
<value>Weiter zur Seite Start</value>
</data>
<data name="Progress_DownloadingPackage" xml:space="preserve">
<value>Herunterladen des Pakets...</value>
</data>
<data name="Progress_DownloadComplete" xml:space="preserve">
<value>Herunterladen abgeschlossen</value>
</data>
<data name="Progress_InstallationComplete" xml:space="preserve">
<value>Installation abgeschlossen</value>
</data>
<data name="Progress_InstallingPrerequisites" xml:space="preserve">
<value>Voraussetzungen installieren...</value>
</data>
<data name="Progress_InstallingPackageRequirements" xml:space="preserve">
<value>Paket-Voraussetzungen installieren...</value>
</data>
<data name="Action_OpenInExplorer" xml:space="preserve">
<value>Öffnen im Explorer</value>
</data>
<data name="Action_OpenInFinder" xml:space="preserve">
<value>Öffnen in Finder</value>
</data>
<data name="Action_Uninstall" xml:space="preserve">
<value>Deinstallieren</value>
</data>
<data name="Action_CheckForUpdates" xml:space="preserve">
<value>Auf Updates überprüfen</value>
</data>
<data name="Action_Update" xml:space="preserve">
<value>Update</value>
</data>
<data name="Action_AddPackage" xml:space="preserve">
<value>Paket hinzufügen</value>
</data>
<data name="TeachingTip_AddPackageToGetStarted" xml:space="preserve">
<value>Füge ein Paket hinzu, um loszulegen!</value>
</data>
<data name="Label_EnvVarsTable_Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Label_EnvVarsTable_Value" xml:space="preserve">
<value>Wert</value>
</data>
<data name="Action_Remove" xml:space="preserve">
<value>Entfernen</value>
</data>
<data name="Label_Details" xml:space="preserve">
<value>Details</value>
</data>
<data name="Label_Callstack" xml:space="preserve">
<value>Callstack</value>
</data>
<data name="Label_InnerException" xml:space="preserve">
<value>Inner Exception</value>
</data>
<data name="Label_SearchEllipsis" xml:space="preserve">
<value>Suchen...</value>
</data>
<data name="Action_OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Action_Retry" xml:space="preserve">
<value>Erneut versuchen</value>
</data>
<data name="Label_PythonVersionInfo" xml:space="preserve">
<value>Python Versionsinfo</value>
</data>
<data name="Action_Restart" xml:space="preserve">
<value>Neustarten</value>
</data>
<data name="Label_ConfirmDelete" xml:space="preserve">
<value>Löschen bestätigen</value>
</data>
<data name="Text_PackageUninstall_Details" xml:space="preserve">
<value>Dadurch werden der Paketordner und sein gesamter Inhalt gelöscht, einschließlich aller generierten Bilder und Dateien, die Sie hinzugefügt haben.</value>
</data>
<data name="Progress_UninstallingPackage" xml:space="preserve">
<value>Paket deinstallieren...</value>
</data>
<data name="Label_PackageUninstalled" xml:space="preserve">
<value>Paket deinstalliert</value>
</data>
<data name="Text_SomeFilesCouldNotBeDeleted" xml:space="preserve">
<value>Einige Dateien konnten nicht gelöscht werden. Bitte schließen Sie alle offenen Dateien im Paketverzeichnis und versuchen Sie es erneut.</value>
</data>
<data name="Label_InvalidPackageType" xml:space="preserve">
<value>Ungültiger Pakettyp</value>
</data>
<data name="TextTemplate_UpdatingPackage" xml:space="preserve">
<value>Aktualisiert {0}</value>
</data>
<data name="Progress_UpdateComplete" xml:space="preserve">
<value>Aktualisierung abgeschlossen</value>
</data>
<data name="TextTemplate_PackageUpdatedToLatest" xml:space="preserve">
<value>{0} wurde auf die neueste Version aktualisiert</value>
</data>
<data name="TextTemplate_ErrorUpdatingPackage" xml:space="preserve">
<value>Fehler beim Aktualisieren {0}</value>
</data>
<data name="Progress_UpdateFailed" xml:space="preserve">
<value>Aktualisierung fehlgeschlagen</value>
</data>
<data name="Action_OpenInBrowser" xml:space="preserve">
<value>Öffnen im Browser</value>
</data>
<data name="Label_ErrorInstallingPackage" xml:space="preserve">
<value>Fehler bei der Installation des Pakets</value>
</data>
<data name="Label_Branch" xml:space="preserve">
<value>Zweig</value>
</data>
<data name="Label_AutoScrollToEnd" xml:space="preserve">
<value>Automatisch zum Ende der Konsolenausgabe blättern</value>
</data>
<data name="Label_License" xml:space="preserve">
<value>Lizenz</value>
</data>
<data name="Label_SharedModelStrategyShort" xml:space="preserve">
<value>Modell-Sharing</value>
</data>
<data name="Label_PleaseSelectDataDirectory" xml:space="preserve">
<value>Bitte wählen Sie ein Datenverzeichnis</value>
</data>
<data name="Label_DataFolderName" xml:space="preserve">
<value>Name des Datenordners</value>
</data>
<data name="Label_CurrentDirectory" xml:space="preserve">
<value>Aktuelles Verzeichnis:</value>
</data>
<data name="Text_AppWillRelaunchAfterUpdate" xml:space="preserve">
<value>Die App wird nach der Aktualisierung neu gestartet</value>
</data>
<data name="Action_RemindMeLater" xml:space="preserve">
<value>Erinnern Sie mich später</value>
</data>
<data name="Action_InstallNow" xml:space="preserve">
<value>Jetzt installieren</value>
</data>
<data name="Label_ReleaseNotes" xml:space="preserve">
<value>Anmerkungen zur Veröffentlichung</value>
</data>
<data name="Action_OpenProjectEllipsis" xml:space="preserve">
<value>Projekt öffnen...</value>
</data>
<data name="Action_SaveAsEllipsis" xml:space="preserve">
<value>Speichern als...</value>
</data>
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Standard-Layout wiederherstellen</value>
</data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Gemeinsame Nutzung des Outputs</value>
</data>
<data name="Label_BatchIndex" xml:space="preserve">
<value>Batch Index</value>
</data>
<data name="Action_Copy" xml:space="preserve">
<value>Kopieren</value>
</data>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Öffnen im Bildbetrachter</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} Bilder ausgewählt</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Ausgabe-Ordner</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Ausgabetyp</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Auswahl löschen</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>Alle auswählen</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>An Inference senden</value>
</data>
<data name="Label_TextToImage" xml:space="preserve">
<value>Text zu Bild</value>
</data>
<data name="Label_ImageToImage" xml:space="preserve">
<value>Bild zu Bild</value>
</data>
<data name="Label_Inpainting" xml:space="preserve">
<value>Inpainting</value>
</data>
<data name="Label_Upscale" xml:space="preserve">
<value>Hochskalieren</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Ausgabe-Browser</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 Bild ausgewählt</value>
</data>
<data name="Label_PythonPackages" xml:space="preserve">
<value>Python Pakete</value>
</data>
<data name="Action_Consolidate" xml:space="preserve">
<value>Konsolidieren</value>
</data>
<data name="Label_AreYouSure" xml:space="preserve">
<value>Sind Sie sicher?</value>
</data>
<data name="Label_ConsolidateExplanation" xml:space="preserve">
<value>Dadurch werden alle generierten Bilder aus den ausgewählten Paketen in das konsolidierte Verzeichnis des gemeinsamen Ausgabeordners verschoben. Diese Aktion kann nicht rückgängig gemacht werden.</value>
</data>
<data name="Action_Refresh" xml:space="preserve">
<value>Aktualisieren</value>
</data>
<data name="Action_Upgrade" xml:space="preserve">
<value>Upgrade</value>
</data>
<data name="Action_Downgrade" xml:space="preserve">
<value>Downgrade</value>
</data>
<data name="Action_OpenGithub" xml:space="preserve">
<value>Öffnen in GitHub</value>
</data>
<data name="Label_Connected" xml:space="preserve">
<value>Verbunden</value>
</data>
<data name="Action_Disconnect" xml:space="preserve">
<value>Nicht verbunden</value>
</data>
<data name="Label_Email" xml:space="preserve">
<value>Email</value>
</data>
<data name="Label_Username" xml:space="preserve">
<value>Nutzername</value>
</data>
<data name="Label_Password" xml:space="preserve">
<value>Password</value>
</data>
<data name="Action_Login" xml:space="preserve">
<value>Einloggen</value>
</data>
<data name="Action_Signup" xml:space="preserve">
<value>Anmelden</value>
</data>
<data name="Label_ConfirmPassword" xml:space="preserve">
<value>Passwort bestätigen</value>
</data>
<data name="Label_ApiKey" xml:space="preserve">
<value>API Schlüssel</value>
</data>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Preprocessor</value>
</data>
<data name="Label_Strength" xml:space="preserve">
<value>Stärke</value>
</data>
<data name="Label_ControlWeight" xml:space="preserve">
<value>Kontrollgewicht</value>
</data>
<data name="Label_ControlSteps" xml:space="preserve">
<value>Kontrollschritte</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>Sie müssen eingeloggt sein, um diesen Checkpoint herunterladen zu können. Bitte geben Sie einen CivitAI API Schlüssel in den Einstellungen ein.</value>
</data>
<data name="Label_DownloadFailed" xml:space="preserve">
<value>Download fehlgeschlagen</value>
</data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Automatische Updates</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Für Early Adopters. Preview-Builds sind zuverlässiger als die aus dem Dev-Channel und werden näher an den stabilen Versionen verfügbar sein. Ihr Feedback hilft uns sehr dabei, Probleme zu entdecken und Designelemente zu verbessern.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Für technische Benutzer. Seien Sie der Erste, der auf unsere Entwicklungs-Builds aus den Funktionszweigen zugreift, sobald sie verfügbar sind. Da wir mit neuen Funktionen experimentieren, kann es noch einige Ecken und Kanten und Bugs geben.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Updates</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>Sie sind auf dem aktuellsten Stand</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Zuletzt überprüft: {0}</value>
</data>
<data name="Action_CopyTriggerWords" xml:space="preserve">
<value>Trigger Wörter kopieren</value>
</data>
<data name="Label_TriggerWords" xml:space="preserve">
<value>Trigger Wörter:</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Zusätzliche Ordner wie IPAdapter und TextualInversions (Einbettungen) können hier aktiviert werden</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Öffnen in Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Vorhandene Metadaten aktualisieren</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>Generell</value>
<comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inference</value>
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Prompt</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Ausgabe von Bilddateien</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Bildbetrachter</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Automatische Vervollständigung</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Ersetzen von Unterstrichen durch Leerzeichen beim Einfügen von Vervollständigungen</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Prompt Tags</value>
<comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Importiere Prompt Tags</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Tags-Datei, die zum Vorschlagen von Vervollständigungen verwendet wird (unterstützt das Format a1111-sd-webui-tagcomplete .csv)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Systeminformationen</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Zusätze</value>
<comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Zwischenbild speichern</value>
<comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="Action_SelectFile" xml:space="preserve">
<value>Datei auswählen</value>
</data>
<data name="Action_ReplaceContents" xml:space="preserve">
<value>Inhalt ersetzen</value>
</data>
<data name="Label_WipFeature" xml:space="preserve">
<value>Noch nicht verfügbar</value>
</data>
<data name="Label_WipFeatureDescription" xml:space="preserve">
<value>Die Funktion wird in einem zukünftigen Update verfügbar sein</value>
</data>
<data name="Label_MissingImageFile" xml:space="preserve">
<value>Fehlende Bilddatei</value>
</data>
<data name="Label_HolidayMode" xml:space="preserve">
<value>Urlaubsmodus</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>CLIP überspringen</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Bild zu Video</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Bilder pro Sekunde </value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>Min CFG</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Fehlerfrei</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Bilder</value>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Augmentierungslevel</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Methode</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Qualität</value>
</data>
</root>

103
StabilityMatrix.Avalonia/Languages/Resources.es.resx

@ -680,7 +680,106 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restablecer Diseño Predeterminado</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Compartir Resultado</value>
</data>
<data name="Label_BatchIndex" xml:space="preserve">
<value>Índice de lotes</value>
</data>
<data name="Action_Copy" xml:space="preserve">
<value>Copiar</value>
</data>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Abrir en el Visor de imágenes</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} imágenes seleccionadas</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Carpeta de Resultados</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Tipo de Resultados</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Borrar Selección</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>Seleccionar Todo</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>Enviar a Inference</value>
</data>
<data name="Label_TextToImage" xml:space="preserve">
<value>Texto a Imagen</value>
</data>
<data name="Label_ImageToImage" xml:space="preserve">
<value>Imagen a Imagen</value>
</data>
<data name="Label_Inpainting" xml:space="preserve">
<value>Pintar Región</value>
</data>
<data name="Label_Upscale" xml:space="preserve">
<value>Escalar</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Navegador de Resultados</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 imagen seleccionada</value>
</data>
<data name="Label_PythonPackages" xml:space="preserve">
<value>Paquetes Python</value>
</data>
<data name="Action_Consolidate" xml:space="preserve">
<value>Agrupar</value>
</data>
<data name="Label_AreYouSure" xml:space="preserve">
<value>¿Estás seguro?</value>
</data>
<data name="Label_ConsolidateExplanation" xml:space="preserve">
<value>Esto moverá todas las imágenes generadas desde los paquetes seleccionados, al directorio &apos;Grupo&apos; de la carpeta de resultados compartidos. Esta acción no se puede deshacer.</value>
</data>
<data name="Action_Refresh" xml:space="preserve">
<value>Refrescar</value>
</data>
<data name="Action_Upgrade" xml:space="preserve">
<value>Mejorar</value>
</data>
<data name="Action_Downgrade" xml:space="preserve">
<value>Degradar</value>
</data>
<data name="Action_OpenGithub" xml:space="preserve">
<value>Abrir en GitHub</value>
</data>
<data name="Label_Connected" xml:space="preserve">
<value>Conectado</value>
</data>
<data name="Action_Disconnect" xml:space="preserve">
<value>Desconectar</value>
</data>
<data name="Label_Email" xml:space="preserve">
<value>Email</value>
</data>
<data name="Label_Username" xml:space="preserve">
<value>Usuario</value>
</data>
<data name="Label_Password" xml:space="preserve">
<value>Contraseña</value>
</data>
<data name="Action_Login" xml:space="preserve">
<value>Entrar</value>
</data>
<data name="Action_Signup" xml:space="preserve">
<value>Registrarme</value>
</data>
<data name="Label_ConfirmPassword" xml:space="preserve">
<value>Confirmar Contraseña</value>
</data>
<data name="Label_ApiKey" xml:space="preserve">
<value>Clave API</value>
</data>
<data name="Label_Accounts" xml:space="preserve">
<value>Cuentas</value>
</data>
</root>

245
StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Action_Launch" xml:space="preserve">
<value>Lancement</value>
<value>Lancer</value>
</data>
<data name="Action_Quit" xml:space="preserve">
<value>Quitter</value>
@ -142,7 +142,7 @@
<value>Relancer plus tard</value>
</data>
<data name="Label_RelaunchRequired" xml:space="preserve">
<value>Relance nécessaire</value>
<value>Relancement nécessaire</value>
</data>
<data name="Label_UnknownPackage" xml:space="preserve">
<value>Paquet inconnu</value>
@ -169,10 +169,10 @@
<value>Glisser-déposer les checkpoints ici pour les importer</value>
</data>
<data name="Label_Emphasis" xml:space="preserve">
<value>Accentuation</value>
<value>Emphase</value>
</data>
<data name="Label_Deemphasis" xml:space="preserve">
<value>Désaccentuation</value>
<value>Emphase inverse</value>
</data>
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve">
<value>Emebeddings / Inversion textuelle</value>
@ -250,7 +250,7 @@
<value>Une erreur inattendue s&apos;est produite</value>
</data>
<data name="Action_ExitApplication" xml:space="preserve">
<value>Demande de sortie</value>
<value>Quitter l&apos;application</value>
</data>
<data name="Label_DisplayName" xml:space="preserve">
<value>Nom d&apos;affichage</value>
@ -379,10 +379,10 @@
<value>Déposer le fichier ici pour l&apos;importer</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Importer en tant que connecté</value>
<value>Importer avec les métadonnées</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Recherche de métadonnées connectées sur les nouvelles importations locales</value>
<value>Recherche de métadonnées en ligne sur les nouvelles importations locales</value>
</data>
<data name="Label_Indexing" xml:space="preserve">
<value>Indexation...</value>
@ -403,10 +403,10 @@
<value>Accord de licence.</value>
</data>
<data name="Label_FindConnectedMetadata" xml:space="preserve">
<value>Trouver des métadonnées connectées</value>
<value>Trouver des métadonnées en ligne</value>
</data>
<data name="Label_ShowModelImages" xml:space="preserve">
<value>Afficher les images du modèle</value>
<value>Afficher les vignettes</value>
</data>
<data name="Label_Appearance" xml:space="preserve">
<value>Apparence</value>
@ -448,13 +448,14 @@
<value>Intégrations</value>
</data>
<data name="Label_DiscordRichPresence" xml:space="preserve">
<value>Présence riche en discorde</value>
<value>Activer Statut Discord</value>
</data>
<data name="Label_System" xml:space="preserve">
<value>Système</value>
</data>
<data name="Label_AddToStartMenu" xml:space="preserve">
<value>Ajouter la matrice de stabilité au menu de démarrage</value>
<value>Ajouter Stability Matrix au menu de démarrage</value>
<comment>updated because &quot;Stability Matrix&quot; was translated in french here haha</comment>
</data>
<data name="Label_AddToStartMenu_Details" xml:space="preserve">
<value>Utilise l&apos;emplacement actuel de l&apos;application, vous pouvez relancer cette opération si vous déplacez l&apos;application.</value>
@ -521,6 +522,7 @@
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Choisissez votre interface préférée et cliquez sur Installer pour commencer.</value>
<comment>Fuzzy</comment>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Installation</value>
@ -556,7 +558,8 @@
<value>Vérifier les mises à jour</value>
</data>
<data name="Action_Update" xml:space="preserve">
<value>Mise à jour</value>
<value>Mettre à jour</value>
<comment>switched to verb as it&apos;s action label</comment>
</data>
<data name="Action_AddPackage" xml:space="preserve">
<value>Ajouter un paquet</value>
@ -598,7 +601,7 @@
<value>Redémarrage</value>
</data>
<data name="Label_ConfirmDelete" xml:space="preserve">
<value>Confirmer Supprimer</value>
<value>Confirmer la suppression</value>
</data>
<data name="Text_PackageUninstall_Details" xml:space="preserve">
<value>Cette opération va supprimer le dossier du paquet et tout son contenu, y compris les images générées et les fichiers que vous avez éventuellement ajoutés.</value>
@ -640,7 +643,7 @@
<value>Branche</value>
</data>
<data name="Label_AutoScrollToEnd" xml:space="preserve">
<value>Défilement automatique jusqu&apos;à la fin de la sortie de la console</value>
<value>Défilement automatique jusqu&apos;à la dernière sortie de la console</value>
</data>
<data name="Label_License" xml:space="preserve">
<value>Licence</value>
@ -667,10 +670,10 @@
<value>Installer maintenant</value>
</data>
<data name="Label_ReleaseNotes" xml:space="preserve">
<value>Notes de mise à jour</value>
<value>Notes de version</value>
</data>
<data name="Action_OpenProjectEllipsis" xml:space="preserve">
<value>Projet ouvert...</value>
<value>Ouvrir projet...</value>
</data>
<data name="Action_SaveAsEllipsis" xml:space="preserve">
<value>Enregistrer sous...</value>
@ -678,7 +681,217 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Rétablir la présentation par défaut</value>
</data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Partager des générations</value>
</data>
<data name="Action_Copy" xml:space="preserve">
<value>Copier</value>
</data>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Ouvrir dans la visionneuse d&apos;images</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} images sélectionnées</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Dossier</value>
<comment>Hey hey :) is this used somewhere else ? if it&apos;s only inside the &quot;output browser&quot;, I suggest to remove &quot;output&quot; from translation, but you&apos;re the boss, lmk :)</comment>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Type</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Annuler la sélection</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>Sélectionner tout</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>Envoyer à l&apos;inférence</value>
</data>
<data name="Label_TextToImage" xml:space="preserve">
<value>Texte vers image</value>
</data>
<data name="Label_ImageToImage" xml:space="preserve">
<value>Image vers image</value>
</data>
<data name="Label_Upscale" xml:space="preserve">
<value>Amélioration de qualité</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Explorateur de génération</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 image sélectionnée</value>
</data>
<data name="Label_PythonPackages" xml:space="preserve">
<value>Paquets Python</value>
</data>
<data name="Action_Consolidate" xml:space="preserve">
<value>Consolider</value>
</data>
<data name="Label_AreYouSure" xml:space="preserve">
<value>Êtes-vous sure ?</value>
</data>
<data name="Label_ConsolidateExplanation" xml:space="preserve">
<value>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.</value>
</data>
<data name="Action_Refresh" xml:space="preserve">
<value>Rafraichir</value>
</data>
<data name="Action_Upgrade" xml:space="preserve">
<value>Passer à la version supérieure</value>
<comment>What is this related to ?</comment>
</data>
<data name="Action_Downgrade" xml:space="preserve">
<value>Passer à la version inférieure</value>
<comment>What is this related to ?</comment>
</data>
<data name="Action_OpenGithub" xml:space="preserve">
<value>Ouvrir sur GitHub</value>
</data>
<data name="Label_Connected" xml:space="preserve">
<value>Connecté</value>
</data>
<data name="Action_Disconnect" xml:space="preserve">
<value>Se déconnecter</value>
</data>
<data name="Label_Email" xml:space="preserve">
<value>Email</value>
</data>
<data name="Label_Username" xml:space="preserve">
<value>Nom d&apos;utilisateur</value>
</data>
<data name="Label_Password" xml:space="preserve">
<value>Mot de passe</value>
</data>
<data name="Action_Login" xml:space="preserve">
<value>Ouvrir une session</value>
</data>
<data name="Action_Signup" xml:space="preserve">
<value>S&apos;enregistrer</value>
</data>
<data name="Label_ConfirmPassword" xml:space="preserve">
<value>Confirmer le mot de passe</value>
</data>
<data name="Label_ApiKey" xml:space="preserve">
<value>Clé d&apos;API</value>
</data>
<data name="Label_Accounts" xml:space="preserve">
<value>Comptes</value>
</data>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Préprocesseur</value>
</data>
<data name="Label_Strength" xml:space="preserve">
<value>Force</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>Vous devez être loggué pour télécharger ce checkpoint. Veuillez entrer une clé d&apos;API CivitAI dans les paramètres.</value>
</data>
<data name="Label_DownloadFailed" xml:space="preserve">
<value>Echec du téléchargement</value>
</data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Mises à jour automatiques</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Pour les early adopters. Version de preview, plus stable que les versions de développement et plus proche des versions stables. Vos retours vont grandement nous aider à identifier des problèmes et ajuster les éléments de design.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Pour les utilisateurs expérimentés. Soyez parmis les premiers à accéder aux builds développement incluants les branches de fonctionnalités dès qu&apos;elles sont disponibles. Il peut y avoir quelques disfonctionnements et bugs au fur et à mesure que nous expérimentons de nouvelles fonctionnalités.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Mises à jour</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>Vous êtes à jour</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Dernière vérification: {0}</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Ouvrir sur Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Mettre à jour les métadonnées existantes</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>Générale</value>
<comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inférence</value>
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Instruction</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Visionneuse d&apos;images</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto-complétion</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Informations système</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Modules complémentaires</value>
<comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Sauvegarder les images intermédiaires</value>
<comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Paramètres</value>
</data>
<data name="Action_SelectFile" xml:space="preserve">
<value>Sélectionner un fichier</value>
</data>
<data name="Action_ReplaceContents" xml:space="preserve">
<value>Remplacer des contenus</value>
</data>
<data name="Label_WipFeature" xml:space="preserve">
<value>Pas encore disponible</value>
</data>
<data name="Label_WipFeatureDescription" xml:space="preserve">
<value>Fonctionnalité disponible dans une mise à jour future</value>
</data>
<data name="Label_MissingImageFile" xml:space="preserve">
<value>Fichier image manquant</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Image vers vidéo</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Images par seconde</value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>CFG Min</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Sans perte</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Images</value>
<comment>What is the context on this one ?</comment>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Méthode</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Qualité</value>
</data>
</root>

57
StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx

@ -236,6 +236,7 @@
</data>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>Patreonになる</value>
<comment>fix platform name</comment>
</data>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Discordに参加</value>
@ -308,7 +309,7 @@
<value>Modelの説明</value>
</data>
<data name="Label_NewVersionAvailable" xml:space="preserve">
<value>Stability Matrixがバージョンアップ!</value>
<value>Stability Matrixを最新版に更新中!</value>
</data>
<data name="Label_ImportLatest" xml:space="preserve">
<value>最新版DL</value>
@ -425,6 +426,7 @@
</data>
<data name="Label_RemoveSymlinksOnShutdown" xml:space="preserve">
<value>checkpointフォルダ内のシンボリックリンクをシャットダウンか再起動時に削除</value>
<comment>I had mistranslated and rewrite now. I thought it was &quot;when the software exits,&quot; but then I realized, given the .net source, that this was to be executed at OS shutdown.</comment>
</data>
<data name="Label_RemoveSymlinksOnShutdown_Details" xml:space="preserve">
<value>Stability Matrix を別のドライブに移動する際に問題が起きた場合、ここにチェック</value>
@ -680,7 +682,56 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>レイアウトを初期状態に戻す</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>共有画像フォルダ</value>
</data>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Image Viewerで開く</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Outputフォルダ</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>全て選択</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>Inferenceに送る</value>
</data>
<data name="Label_PythonPackages" xml:space="preserve">
<value>Pythonパッケージ</value>
</data>
<data name="Label_ConsolidateExplanation" xml:space="preserve">
<value>これにより、選択したパッケージから生成されたすべてのイメージが、共有出力フォルダの Consolidated ディレクトリに移動します。この操作は元に戻せません。</value>
</data>
<data name="Action_Refresh" xml:space="preserve">
<value>更新</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>ダウンロードにはCivitAIのログインが必要です。SettingからAPIキーを入力してください。</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Previewビルドはアーリーアダプター向けです。Devビルドよりも信頼性が高く、安定版に近い状態で利用できます。ぜひ意見や感想を送ってください。問題とデザインの改善に大いに役立ちます。</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Devビルドはテクニカルなユーザ向けです。新機能をいち早く利用できます。荒削りな部分やバグがあるかもしれません。</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Hugging Faceで開く</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Metadataをアップデートする</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>入力補完でアンダースコアをスペースに置き換える</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>入力補完に使用するタグリスト(拡張機能a1111-sd-webui-tagcomplete内にあるcsvファイルが使えます)</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>フレームレート(FPS)</value>
<comment>for jp, &quot;frame rate&quot; is easier to understand, and its better to append FPS. no one can mistake it for a genre of games except nerds</comment>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>非圧縮</value>
</data>
</root>

933
StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx

@ -0,0 +1,933 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Action_Launch" xml:space="preserve">
<value>Executar</value>
</data>
<data name="Action_Quit" xml:space="preserve">
<value>Encerrar</value>
</data>
<data name="Action_Save" xml:space="preserve">
<value>Salvar</value>
</data>
<data name="Action_Cancel" xml:space="preserve">
<value>Cancelar</value>
</data>
<data name="Label_Language" xml:space="preserve">
<value>Idioma</value>
</data>
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve">
<value>Reiniciar</value>
</data>
<data name="Action_Relaunch" xml:space="preserve">
<value>Reiniciar</value>
</data>
<data name="Action_RelaunchLater" xml:space="preserve">
<value>Reiniciar mais tarde</value>
</data>
<data name="Label_RelaunchRequired" xml:space="preserve">
<value>Necessário Reiniciar</value>
</data>
<data name="Label_UnknownPackage" xml:space="preserve">
<value>Pacote Desconhecido</value>
</data>
<data name="Action_Import" xml:space="preserve">
<value>Importar</value>
</data>
<data name="Label_PackageType" xml:space="preserve">
<value>Tipo do Pacote</value>
</data>
<data name="Label_Version" xml:space="preserve">
<value>Versão</value>
</data>
<data name="Label_VersionType" xml:space="preserve">
<value>Tipo da Versão</value>
</data>
<data name="Label_Releases" xml:space="preserve">
<value>Versões</value>
</data>
<data name="Label_Branches" xml:space="preserve">
<value>Ramificações</value>
</data>
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>Arraste e Solte os Checkpoints aqui para Importar</value>
</data>
<data name="Label_Emphasis" xml:space="preserve">
<value>Enfatizar</value>
</data>
<data name="Label_Deemphasis" xml:space="preserve">
<value>Desenfatizar</value>
</data>
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve">
<value>Emebeddings / Inversão Textual</value>
</data>
<data name="Label_NetworksLoraOrLycoris" xml:space="preserve">
<value>Networks (Lora / LyCORIS)</value>
</data>
<data name="Label_Comments" xml:space="preserve">
<value>Comentários</value>
</data>
<data name="Label_ShowPixelGridAtHighZoomLevels" xml:space="preserve">
<value>Exibir Pixels Grid com níveis de Zoom</value>
</data>
<data name="Label_Steps" xml:space="preserve">
<value>Passos</value>
</data>
<data name="Label_StepsBase" xml:space="preserve">
<value>Passos - Base</value>
</data>
<data name="Label_StepsRefiner" xml:space="preserve">
<value>Passos - Refinamento</value>
</data>
<data name="Label_CFGScale" xml:space="preserve">
<value>Escale de CFG</value>
</data>
<data name="Label_DenoisingStrength" xml:space="preserve">
<value>Nível de Denoising</value>
</data>
<data name="Label_Width" xml:space="preserve">
<value>Largura</value>
</data>
<data name="Label_Height" xml:space="preserve">
<value>Altura</value>
</data>
<data name="Label_Refiner" xml:space="preserve">
<value>Refinamento</value>
</data>
<data name="Label_VAE" xml:space="preserve">
<value>VAE</value>
</data>
<data name="Label_Model" xml:space="preserve">
<value>Modelo</value>
</data>
<data name="Action_Connect" xml:space="preserve">
<value>Conectar</value>
</data>
<data name="Label_ConnectingEllipsis" xml:space="preserve">
<value>Conectando...</value>
</data>
<data name="Action_Close" xml:space="preserve">
<value>Fechar</value>
</data>
<data name="Label_WaitingToConnectEllipsis" xml:space="preserve">
<value>Aguardando pela conecção</value>
</data>
<data name="Label_UpdateAvailable" xml:space="preserve">
<value>Novo Update disponível</value>
</data>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>Torne-se um colaborador no Patreon</value>
</data>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Junte-se ao nosso canal no Discord</value>
</data>
<data name="Label_Downloads" xml:space="preserve">
<value>Downloads</value>
</data>
<data name="Action_Install" xml:space="preserve">
<value>Instalação</value>
</data>
<data name="Label_SkipSetup" xml:space="preserve">
<value>Pular as configurações iniciais</value>
</data>
<data name="Label_UnexpectedErrorOccurred" xml:space="preserve">
<value>Um erro inesperado ocorreu</value>
</data>
<data name="Action_ExitApplication" xml:space="preserve">
<value>Sair da Aplicação</value>
</data>
<data name="Label_DisplayName" xml:space="preserve">
<value>Nome de Exibição</value>
</data>
<data name="Label_InstallationWithThisNameExists" xml:space="preserve">
<value>Uma instalação com este nome já existe</value>
</data>
<data name="Label_PleaseChooseDifferentName" xml:space="preserve">
<value>Por favor escolha um novo nome ou escolha um novo local para a instalação</value>
</data>
<data name="Label_AdvancedOptions" xml:space="preserve">
<value>Opções Avançadas</value>
</data>
<data name="Label_Commit" xml:space="preserve">
<value>Salvar alterações</value>
</data>
<data name="Label_SharedModelFolderStrategy" xml:space="preserve">
<value>Estratégia de Modelo de Pasta compartilhado</value>
</data>
<data name="Label_PyTorchVersion" xml:space="preserve">
<value>Versão to PyTorch</value>
</data>
<data name="Label_CloseDialogWhenFinished" xml:space="preserve">
<value>Fechar diálogo quando finalizar</value>
</data>
<data name="Label_DataDirectory" xml:space="preserve">
<value>DIretório de Dados</value>
</data>
<data name="Label_DataDirectoryExplanation" xml:space="preserve">
<value>Este local é onde os dados do aplicativo (modelos de Checkpoints, Interfaces Web, etc.) serão instaldos</value>
</data>
<data name="Label_FatWarning" xml:space="preserve">
<value>Você poderá encontrar erros quando usar discos rígidos com formatação FAT32 ou exFAT. Escolha outro tipo de HD para uma experiência melhor.</value>
</data>
<data name="Label_PortableMode" xml:space="preserve">
<value>Modo Portátil</value>
</data>
<data name="Label_PortableModeExplanation" xml:space="preserve">
<value>Com o Modo Portátil, todos os dados e configurações serão salvos no mesmo diretório da aplicação. Você poderá mover a aplicação com suas pastas de dados para um local diferente em seu computador.</value>
</data>
<data name="Action_Continue" xml:space="preserve">
<value>Continuar</value>
</data>
<data name="Label_PreviousImage" xml:space="preserve">
<value>Imagem Anterior</value>
</data>
<data name="Label_NextImage" xml:space="preserve">
<value>Próxima Imagem</value>
</data>
<data name="Label_ModelDescription" xml:space="preserve">
<value>Descrição do Modelo</value>
</data>
<data name="Label_NewVersionAvailable" xml:space="preserve">
<value>Uma nova versão do Stability Matrix está disponível</value>
</data>
<data name="Label_ImportLatest" xml:space="preserve">
<value>Importar última versão</value>
</data>
<data name="Label_AllVersions" xml:space="preserve">
<value>Todas as Versões</value>
</data>
<data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Pesquisar Modelos, #tags, ou @users</value>
</data>
<data name="Action_Search" xml:space="preserve">
<value>Pesquisar</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>Filtrar</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Período</value>
</data>
<data name="Label_ModelType" xml:space="preserve">
<value>Tipo do Modelo</value>
</data>
<data name="Label_BaseModel" xml:space="preserve">
<value>Modelo Base</value>
</data>
<data name="Label_ShowNsfwContent" xml:space="preserve">
<value>Mostar Conteúdo NSFW (adulto)</value>
</data>
<data name="Label_DataProvidedByCivitAi" xml:space="preserve">
<value>Dados fornecidos pela CivitAI</value>
</data>
<data name="Label_Page" xml:space="preserve">
<value>Página</value>
</data>
<data name="Label_FirstPage" xml:space="preserve">
<value>Primeira Página</value>
</data>
<data name="Label_PreviousPage" xml:space="preserve">
<value>Página Anterior</value>
</data>
<data name="Label_NextPage" xml:space="preserve">
<value>Próxima Página</value>
</data>
<data name="Label_LastPage" xml:space="preserve">
<value>Última Página</value>
</data>
<data name="Action_Rename" xml:space="preserve">
<value>Renomear</value>
</data>
<data name="Action_Delete" xml:space="preserve">
<value>Apagar</value>
</data>
<data name="Action_OpenOnCivitAi" xml:space="preserve">
<value>Abrir em CivitAI</value>
</data>
<data name="Label_ConnectedModel" xml:space="preserve">
<value>Modelo Conectado</value>
</data>
<data name="Label_LocalModel" xml:space="preserve">
<value>Modelo Local</value>
</data>
<data name="Action_ShowInExplorer" xml:space="preserve">
<value>Exibir no Explorer</value>
</data>
<data name="Action_New" xml:space="preserve">
<value>Novo</value>
</data>
<data name="Label_Folder" xml:space="preserve">
<value>Pasta</value>
</data>
<data name="Label_DropFileToImport" xml:space="preserve">
<value>Coloque o arquivo aqui para Importar</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Importar com Metadados</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Procurar por Metadados conectados em novos locais de importação</value>
</data>
<data name="Label_Indexing" xml:space="preserve">
<value>Indexando...</value>
</data>
<data name="Label_ModelsFolder" xml:space="preserve">
<value>Pasta de Modelos</value>
</data>
<data name="Label_Categories" xml:space="preserve">
<value>Catagorias</value>
</data>
<data name="Label_LetsGetStarted" xml:space="preserve">
<value>Vamos começar!</value>
</data>
<data name="Label_ReadAndAgree" xml:space="preserve">
<value>Ei lí e concordei com o</value>
</data>
<data name="Label_LicenseAgreement" xml:space="preserve">
<value>Acordo de Licenciamento</value>
</data>
<data name="Label_FindConnectedMetadata" xml:space="preserve">
<value>Procurar Metadados Conectados</value>
</data>
<data name="Label_ShowModelImages" xml:space="preserve">
<value>Exibir imagens dos Modelos</value>
</data>
<data name="Label_Appearance" xml:space="preserve">
<value>Aparencia</value>
</data>
<data name="Label_Theme" xml:space="preserve">
<value>Tema</value>
</data>
<data name="Label_CheckpointManager" xml:space="preserve">
<value>Gerenciador de Checkpoint</value>
</data>
<data name="Label_RemoveSymlinksOnShutdown" xml:space="preserve">
<value>Remove os links simbólico compartilhados do diretorio de Checkpoints ao encerrar</value>
</data>
<data name="Label_RemoveSymlinksOnShutdown_Details" xml:space="preserve">
<value>Selecione esta opção caso você esteja encontrando problemas ao mover o Stability Matrix para outro HD</value>
</data>
<data name="Label_ResetCheckpointsCache" xml:space="preserve">
<value>Resetar o cache de Checkpoints</value>
</data>
<data name="Label_ResetCheckpointsCache_Details" xml:space="preserve">
<value>Recriar o cache de Checkpoints instalados. Use caso os Checkpoints estiverem com a descrição incorreta no Selecionador de Modelos</value>
</data>
<data name="Label_PackageEnvironment" xml:space="preserve">
<value>Ambiente do Pacote de Instalação</value>
</data>
<data name="Action_Edit" xml:space="preserve">
<value>Editar</value>
</data>
<data name="Label_EnvironmentVariables" xml:space="preserve">
<value>Variáveis de Ambiente</value>
</data>
<data name="Label_EmbeddedPython" xml:space="preserve">
<value>Python Embutido</value>
</data>
<data name="Action_CheckVersion" xml:space="preserve">
<value>Verificar Versão</value>
</data>
<data name="Label_Integrations" xml:space="preserve">
<value>Integrações</value>
</data>
<data name="Label_DiscordRichPresence" xml:space="preserve">
<value>Presença no Discord</value>
</data>
<data name="Label_System" xml:space="preserve">
<value>Sistema</value>
</data>
<data name="Label_AddToStartMenu" xml:space="preserve">
<value>Adicionar o Stability Matrix ao Menu Iniciar</value>
</data>
<data name="Label_AddToStartMenu_Details" xml:space="preserve">
<value>Utilizar o local atual do aplicativo. Você poderá executar esta opção novamente caso você mover o aplicativo</value>
</data>
<data name="Label_OnlyAvailableOnWindows" xml:space="preserve">
<value>Apenas disponível no Windows</value>
</data>
<data name="Action_AddForCurrentUser" xml:space="preserve">
<value>Adicionar para o Usuário Atual</value>
</data>
<data name="Action_AddForAllUsers" xml:space="preserve">
<value>Adicionar para todos os Usuários</value>
</data>
<data name="Label_SelectNewDataDirectory" xml:space="preserve">
<value>Selecionar novo Diretório de Dados</value>
</data>
<data name="Label_SelectNewDataDirectory_Details" xml:space="preserve">
<value>Não mover os dados atuais</value>
</data>
<data name="Action_SelectDirectory" xml:space="preserve">
<value>Selecionar o Diretório</value>
</data>
<data name="Label_About" xml:space="preserve">
<value>Sobre</value>
</data>
<data name="Label_StabilityMatrix" xml:space="preserve">
<value>Stability Matrix</value>
</data>
<data name="Label_LicenseAndOpenSourceNotices" xml:space="preserve">
<value>Avisos de licença e código aberto</value>
</data>
<data name="TeachingTip_ClickLaunchToGetStarted" xml:space="preserve">
<value>Clique em Iniciar para começar!</value>
</data>
<data name="Action_Stop" xml:space="preserve">
<value>Parar</value>
</data>
<data name="Action_SendInput" xml:space="preserve">
<value>Enviar comando</value>
</data>
<data name="Label_Input" xml:space="preserve">
<value>Input</value>
</data>
<data name="Action_Send" xml:space="preserve">
<value>Enviar</value>
</data>
<data name="Label_InputRequired" xml:space="preserve">
<value>Input necessário</value>
</data>
<data name="Label_ConfirmQuestion" xml:space="preserve">
<value>Confirma?</value>
</data>
<data name="Action_Yes" xml:space="preserve">
<value>Sim</value>
</data>
<data name="Label_No" xml:space="preserve">
<value>Não</value>
</data>
<data name="Action_OpenWebUI" xml:space="preserve">
<value>Abrir a Interface Web</value>
</data>
<data name="Text_WelcomeToStabilityMatrix" xml:space="preserve">
<value>Bem-vindo ao Stability Matrix!</value>
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Escolha sua interface preferida e clique em Instalar para começar</value>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Instalando</value>
</data>
<data name="Text_ProceedingToLaunchPage" xml:space="preserve">
<value>Prosseguindo para a Página de Inicial</value>
</data>
<data name="Progress_DownloadingPackage" xml:space="preserve">
<value>Baixando pacote...</value>
</data>
<data name="Progress_DownloadComplete" xml:space="preserve">
<value>Download completo</value>
</data>
<data name="Progress_InstallationComplete" xml:space="preserve">
<value>Instalação completa</value>
</data>
<data name="Progress_InstallingPrerequisites" xml:space="preserve">
<value>Instalando pré-requisitos...</value>
</data>
<data name="Progress_InstallingPackageRequirements" xml:space="preserve">
<value>Instalando requisitos de pacote...</value>
</data>
<data name="Action_OpenInExplorer" xml:space="preserve">
<value>Abrir no Explorer</value>
</data>
<data name="Action_OpenInFinder" xml:space="preserve">
<value>Abrir no Finder</value>
</data>
<data name="Action_Uninstall" xml:space="preserve">
<value>Desinstalar</value>
</data>
<data name="Action_CheckForUpdates" xml:space="preserve">
<value>Verificar se existem atualizações</value>
</data>
<data name="Action_Update" xml:space="preserve">
<value>Atualizar</value>
</data>
<data name="Action_AddPackage" xml:space="preserve">
<value>Adicionar um pacote</value>
</data>
<data name="TeachingTip_AddPackageToGetStarted" xml:space="preserve">
<value>Adicione um pacote para iniciar!</value>
</data>
<data name="Label_EnvVarsTable_Name" xml:space="preserve">
<value>Nome</value>
</data>
<data name="Label_EnvVarsTable_Value" xml:space="preserve">
<value>Valor</value>
</data>
<data name="Action_Remove" xml:space="preserve">
<value>Remover</value>
</data>
<data name="Label_Details" xml:space="preserve">
<value>Detalhes</value>
</data>
<data name="Label_Callstack" xml:space="preserve">
<value>Pilha de chamadas</value>
</data>
<data name="Label_InnerException" xml:space="preserve">
<value>Exceção interna</value>
</data>
<data name="Label_SearchEllipsis" xml:space="preserve">
<value>Procurar...</value>
</data>
<data name="Action_OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Action_Retry" xml:space="preserve">
<value>Tentar novamente</value>
</data>
<data name="Label_PythonVersionInfo" xml:space="preserve">
<value>Informações da versão do Python</value>
</data>
<data name="Action_Restart" xml:space="preserve">
<value>Reiniciar</value>
</data>
<data name="Label_ConfirmDelete" xml:space="preserve">
<value>Confirmar exclusão</value>
</data>
<data name="Text_PackageUninstall_Details" xml:space="preserve">
<value>Isso excluirá a pasta do pacote e todo o seu conteúdo, incluindo quaisquer imagens e arquivos gerados que você possa ter adicionado.</value>
</data>
<data name="Progress_UninstallingPackage" xml:space="preserve">
<value>Desinstalando pacote...</value>
</data>
<data name="Label_PackageUninstalled" xml:space="preserve">
<value>Pacote desinstalado</value>
</data>
<data name="Text_SomeFilesCouldNotBeDeleted" xml:space="preserve">
<value>Alguns arquivos não puderam ser excluídos. Feche todos os arquivos abertos no diretório do pacote e tente novamente.</value>
</data>
<data name="Label_InvalidPackageType" xml:space="preserve">
<value>Tipo de pacote inválido</value>
</data>
<data name="TextTemplate_UpdatingPackage" xml:space="preserve">
<value>Atualizando {0}</value>
</data>
<data name="Progress_UpdateComplete" xml:space="preserve">
<value>Atualização completa</value>
</data>
<data name="TextTemplate_PackageUpdatedToLatest" xml:space="preserve">
<value>{0} foi atualizado para a versão mais recente</value>
</data>
<data name="TextTemplate_ErrorUpdatingPackage" xml:space="preserve">
<value>Erro ao atualizar {0}</value>
</data>
<data name="Progress_UpdateFailed" xml:space="preserve">
<value>A Atualização falhou</value>
</data>
<data name="Action_OpenInBrowser" xml:space="preserve">
<value>Abrir no navegador</value>
</data>
<data name="Label_ErrorInstallingPackage" xml:space="preserve">
<value>Erro ao instalar o pacote</value>
</data>
<data name="Label_Branch" xml:space="preserve">
<value>Branch</value>
</data>
<data name="Label_AutoScrollToEnd" xml:space="preserve">
<value>Rolar automaticamente até o final da saída do console</value>
</data>
<data name="Label_License" xml:space="preserve">
<value>Licença</value>
</data>
<data name="Label_SharedModelStrategyShort" xml:space="preserve">
<value>Compartilhamento de modelo</value>
</data>
<data name="Label_PleaseSelectDataDirectory" xml:space="preserve">
<value>Selecione um diretório de dados</value>
</data>
<data name="Label_DataFolderName" xml:space="preserve">
<value>Nome da pasta de dados</value>
</data>
<data name="Label_CurrentDirectory" xml:space="preserve">
<value>Diretório atual:</value>
</data>
<data name="Text_AppWillRelaunchAfterUpdate" xml:space="preserve">
<value>O aplicativo será reiniciado após a atualização</value>
</data>
<data name="Action_RemindMeLater" xml:space="preserve">
<value>Lembre-me mais tarde</value>
</data>
<data name="Action_InstallNow" xml:space="preserve">
<value>instale agora</value>
</data>
<data name="Label_ReleaseNotes" xml:space="preserve">
<value>Notas de versão</value>
</data>
<data name="Action_OpenProjectEllipsis" xml:space="preserve">
<value>Abrir Projeto...</value>
</data>
<data name="Action_SaveAsEllipsis" xml:space="preserve">
<value>Salvar como...</value>
</data>
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restaurar layout padrão</value>
</data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Usar Pasta de saída Compartilhada </value>
</data>
<data name="Label_BatchIndex" xml:space="preserve">
<value>Índice de lote</value>
</data>
<data name="Action_Copy" xml:space="preserve">
<value>Copiar</value>
</data>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Abrir no Visualizador de Imagens</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} imagens selecionadas</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Pasta de saída</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Tipo de saída</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Limpar Seleção</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>Selecionar tudo</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>Enviar para inferência</value>
</data>
<data name="Label_TextToImage" xml:space="preserve">
<value>Texto para imagem</value>
</data>
<data name="Label_ImageToImage" xml:space="preserve">
<value>Imagem para imagem</value>
</data>
<data name="Label_Inpainting" xml:space="preserve">
<value>Inpainting (Pintar sobre a imagem)</value>
</data>
<data name="Label_Upscale" xml:space="preserve">
<value>Aumentar o tamanho da Imagem</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Navegador de Exibição</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 imagem selecionada</value>
</data>
<data name="Label_PythonPackages" xml:space="preserve">
<value>Pacotes Python</value>
</data>
<data name="Action_Consolidate" xml:space="preserve">
<value>Consolidar</value>
</data>
<data name="Label_AreYouSure" xml:space="preserve">
<value>Tem certeza?</value>
</data>
<data name="Label_ConsolidateExplanation" xml:space="preserve">
<value>Isso 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.</value>
</data>
<data name="Action_Refresh" xml:space="preserve">
<value>Atualizar</value>
</data>
<data name="Action_Upgrade" xml:space="preserve">
<value>Upgrade</value>
</data>
<data name="Action_Downgrade" xml:space="preserve">
<value>Downgrade</value>
</data>
<data name="Action_OpenGithub" xml:space="preserve">
<value>Abrir no GitHub</value>
</data>
<data name="Label_Connected" xml:space="preserve">
<value>Conectado</value>
</data>
<data name="Action_Disconnect" xml:space="preserve">
<value>Desconectar</value>
</data>
<data name="Label_Email" xml:space="preserve">
<value>E-mail</value>
</data>
<data name="Label_Username" xml:space="preserve">
<value>Nome de usuário</value>
</data>
<data name="Label_Password" xml:space="preserve">
<value>Senha</value>
</data>
<data name="Action_Login" xml:space="preserve">
<value>Conectar</value>
</data>
<data name="Action_Signup" xml:space="preserve">
<value>Cadastrar Conta</value>
</data>
<data name="Label_ConfirmPassword" xml:space="preserve">
<value>Confirme sua senha</value>
</data>
<data name="Label_ApiKey" xml:space="preserve">
<value>Chave API</value>
</data>
<data name="Label_Accounts" xml:space="preserve">
<value>Contas</value>
</data>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Pré-processador</value>
</data>
<data name="Label_Strength" xml:space="preserve">
<value>Força a aplicar</value>
</data>
<data name="Label_ControlWeight" xml:space="preserve">
<value>Controle de Potência a aplicar</value>
</data>
<data name="Label_ControlSteps" xml:space="preserve">
<value>Número de Etapas de controle</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>Você deve estar logado para baixar este ponto de verificação. Insira uma chave de API CivitAI nas configurações.</value>
</data>
<data name="Label_DownloadFailed" xml:space="preserve">
<value>O Download falhou</value>
</data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Atualizações automáticas</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Para os usuários Beta. Os Builds (compilações) de pré-visualização serão mais confiáveis do que as do canal Dev e estarão disponíveis mais próximas de versões estáveis. Seu feedback nos ajudará muito a descobrir problemas e aprimorar os elementos do design.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Para usuários técnicos. Seja o primeiro a acessar nossos Builds (compilações) de desenvolvimento a partir das versões com novos recursos assim que estes estiverem disponíveis. Podem haver alguns Bugs e pequenos problemas à medida que experimentamos novos recursos.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Atualizações</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>Você está atualizado</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Última verificação: {0}</value>
</data>
<data name="Action_CopyTriggerWords" xml:space="preserve">
<value>Copiar palavras que executam ações</value>
</data>
<data name="Label_TriggerWords" xml:space="preserve">
<value>Palavras que executam ações:</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Pastas adicionais como IPAdapters e TextualInversions (embeddings) podem ser habilitadas aqui</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Abrir no Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Atualizar metadados existentes</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>Em geral</value>
<comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inferência</value>
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Incitar</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Arquivos de imagem de saída</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Visualizador de imagens</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Preenchimento automático</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Substitua sublinhados por espaços ao inserir conclusões</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Etiquetas de prompt</value>
<comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Tags de prompt de importação</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Arquivo de tags a ser usado para sugerir conclusões (suporta o formato .csv a1111-sd-webui-tagcomplete)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Informação do sistema</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Abraçando o rosto</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Complementos</value>
<comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Salvar imagem intermediária</value>
<comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Configurações</value>
</data>
<data name="Action_SelectFile" xml:space="preserve">
<value>Selecione o arquivo</value>
</data>
<data name="Action_ReplaceContents" xml:space="preserve">
<value>Substituir conteúdo</value>
</data>
<data name="Label_WipFeature" xml:space="preserve">
<value>Não disponível ainda</value>
</data>
<data name="Label_WipFeatureDescription" xml:space="preserve">
<value>Este recurso estará disponível em uma atualização futura</value>
</data>
<data name="Label_MissingImageFile" xml:space="preserve">
<value>Arquivo de imagem não encontrado</value>
</data>
<data name="Label_HolidayMode" xml:space="preserve">
<value>Modo Férias</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>Pular CLIPE</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Imagem para vídeo</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Quadros por segundo</value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>CFG mínimo</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Sem perdas</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Frames</value>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>ID do Motion Bucket</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Nível de aumento</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Método</value>
</data>
</root>

65
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -520,7 +520,7 @@
<value>Welcome to Stability Matrix!</value>
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Choose your preferred interface and click Install to get started</value>
<value>Choose your preferred interface to get started</value>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Installing</value>
@ -897,4 +897,67 @@
<data name="Label_HolidayMode" xml:space="preserve">
<value>Holiday Mode</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>CLIP Skip</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Image to Video</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Frames Per Second</value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>Min CFG</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Lossless</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Frames</value>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Augmentation Level</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Method</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Quality</value>
</data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Find in Model Browser</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Installed</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>No extensions found.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Copy Details</value>
</data>
<data name="Label_Notifications" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value />
</data>
<data name="Action_Download" xml:space="preserve">
<value>Download</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Check the progress of your package installations and model downloads here.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Recommended Models</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>While your package is installing, here are some models we recommend to help you get started.</value>
</data>
</root>

187
StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx

@ -133,7 +133,7 @@
<value>Dil</value>
</data>
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve">
<value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekir.</value>
<value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekiyor</value>
</data>
<data name="Action_Relaunch" xml:space="preserve">
<value>Yeniden başlat</value>
@ -166,7 +166,7 @@
<value>Dallar</value>
</data>
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>İçe aktarmak için chekpoints&apos;leri buraya sürükleyip bırakın.</value>
<value>İçe aktarmak için chekpoints&apos;leri buraya sürükleyip bırakın</value>
</data>
<data name="Label_Emphasis" xml:space="preserve">
<value>Vurgu</value>
@ -235,7 +235,7 @@
<value>Sponsor Ol</value>
</data>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Discord Sunucumuza Katıl</value>
<value>Discord Sunucusuna Katıl</value>
</data>
<data name="Label_Downloads" xml:space="preserve">
<value>İndirmeler</value>
@ -313,22 +313,22 @@
<value>Tüm Sürümler</value>
</data>
<data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Modelleri,Tagları (#tag) veya Kullanıcıları (@user) Buradan Arayabilirsin</value>
<value>Modelleri, #etiketleri veya @kullanıcıları ara</value>
</data>
<data name="Action_Search" xml:space="preserve">
<value>Ara</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>Sıralama Tipi</value>
<value>Sırala</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Sıralama Tarihi</value>
<value>Süre</value>
</data>
<data name="Label_ModelType" xml:space="preserve">
<value>Model Türü</value>
</data>
<data name="Label_BaseModel" xml:space="preserve">
<value>Model Altyapısı</value>
<value>Temel Model</value>
</data>
<data name="Label_ShowNsfwContent" xml:space="preserve">
<value>NSFW İçerik Göster</value>
@ -376,10 +376,10 @@
<value>Klasör</value>
</data>
<data name="Label_DropFileToImport" xml:space="preserve">
<value>Dosyayı içeri aktarmak için lütfen sürükleyip işaretlenen alana bırakın.</value>
<value>İçe aktarma için dosyayı buraya bırakın</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Dosyaları metadata ile içeri aktar</value>
<value>Metadata ile içeri aktar</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Yeni yerel içe aktarmalar için bağlı meta veri arayın</value>
@ -448,7 +448,7 @@
<value>Entegrasyonlar</value>
</data>
<data name="Label_DiscordRichPresence" xml:space="preserve">
<value>Discord Etkinliğini StabilityMatrix Olarak Göster.</value>
<value>Discord Zengin Varlık</value>
</data>
<data name="Label_System" xml:space="preserve">
<value>Sistem</value>
@ -469,10 +469,10 @@
<value>Tüm Kullanıcılar İçin Ekle</value>
</data>
<data name="Label_SelectNewDataDirectory" xml:space="preserve">
<value>Yeni Dosya Dizinini Seç</value>
<value>Yeni Veri Dizini Seç</value>
</data>
<data name="Label_SelectNewDataDirectory_Details" xml:space="preserve">
<value>Bu işlem mevcut veriyi yeni dizine taşımaz.</value>
<value>Mevcut veriyi taşımaz</value>
</data>
<data name="Action_SelectDirectory" xml:space="preserve">
<value>Dizin Seçin</value>
@ -490,7 +490,7 @@
<value>Başlamak için Başlat&apos;a tıklayın!</value>
</data>
<data name="Action_Stop" xml:space="preserve">
<value>Durdur</value>
<value>Dur</value>
</data>
<data name="Action_SendInput" xml:space="preserve">
<value>Giriş Gönder</value>
@ -694,10 +694,10 @@
<value>{0} resim seçildi</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Çıktı Klasörü</value>
<value>Çıkış Klasörü</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Çıktı Türü</value>
<value>Çıkış Türü</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Seçimi Temizle</value>
@ -721,7 +721,7 @@
<value>Upscale</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Medya Kitaplığı</value>
<value>Çıkış Tarayıcısı</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 resim seçildi</value>
@ -780,4 +780,157 @@
<data name="Label_Accounts" xml:space="preserve">
<value>Hesaplar</value>
</data>
</root>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Önişlemci</value>
</data>
<data name="Label_Strength" xml:space="preserve">
<value>Kuvvet</value>
</data>
<data name="Label_ControlWeight" xml:space="preserve">
<value>Kontrol Ağırlığı</value>
</data>
<data name="Label_ControlSteps" xml:space="preserve">
<value>Kontrol Adımları</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>Bu kontrol noktasını indirmek için giriş yapmalısınız. Lütfen ayarlara bir CivitAI API Anahtarı girin.</value>
</data>
<data name="Label_DownloadFailed" xml:space="preserve">
<value>İndirme Başarısız</value>
</data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Otomatik Güncellemeler</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Erken benimseyenler için. Önizleme yapıları, Geliştirme kanalındakilerden daha güvenilir olacak ve kararlı sürümlere daha yakın olacak. Geri bildiriminiz sorunları keşfetmemizde ve tasarım öğelerini geliştirmemizde bize büyük ölçüde yardımcı olacaktır.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Teknik kullanıcılar için. Geliştirme yapılarımıza, kullanıma sunuldukları anda özellik dallarından ilk erişen siz olun. Yeni özellikleri denedikçe bazı pürüzlü noktalar ve hatalar olabilir.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Güncellemeler</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>Güncelsin</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Son kontrol: {0}</value>
</data>
<data name="Action_CopyTriggerWords" xml:space="preserve">
<value>Tetikleyici Kelimeleri Kopyala</value>
</data>
<data name="Label_TriggerWords" xml:space="preserve">
<value>Tetikleyici kelimeler:</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>IPAdapters ve TextualInversions (embeddings) gibi ek klasörler burada etkinleştirilebilir</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Hugging Face&apos;de aç</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Mevcut Meta Verileri Güncelle</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>Genel</value>
<comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Çıkarım</value>
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Komut</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Çıktı Resim Dosyaları</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Resim görüntüleyici</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Otomatik Tamamlama</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Tamamlamaları eklerken alt çizgileri boşluklarla değiştirin</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Komut Etiketleri</value>
<comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Komut etiketlerini içe aktar</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Tamamlamaları önermek için kullanılacak etiket dosyası (a1111-sd-webui-tagcomplete .csv formatını destekler)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Sistem bilgisi</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Eklentiler</value>
<comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Ara Resmi Kaydet</value>
<comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Ayarlar</value>
</data>
<data name="Action_SelectFile" xml:space="preserve">
<value>Dosya Seç</value>
</data>
<data name="Action_ReplaceContents" xml:space="preserve">
<value>İçeriği Değiştir</value>
</data>
<data name="Label_WipFeature" xml:space="preserve">
<value>Henüz uygun değil</value>
</data>
<data name="Label_WipFeatureDescription" xml:space="preserve">
<value>Özellik gelecekteki bir güncellemede kullanıma sunulacak</value>
</data>
<data name="Label_MissingImageFile" xml:space="preserve">
<value>Eksik Resim Dosyası</value>
</data>
<data name="Label_HolidayMode" xml:space="preserve">
<value>Tatil Modu</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>CLIP Atla</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Resimden Videoya</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Saniyedeki Kare Sayısı</value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>Min. CFG</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Kayıpsız</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Çerçeveler</value>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Arttırma Seviyesi</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Yöntem</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Kalite</value>
</data>
</root>

33
StabilityMatrix.Avalonia/Models/CommandItem.cs

@ -0,0 +1,33 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Windows.Input;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.Models;
public partial record CommandItem
{
public ICommand Command { get; init; }
public string DisplayName { get; init; }
public CommandItem(ICommand command, [CallerArgumentExpression("command")] string? commandName = null)
{
Command = command;
DisplayName = commandName == null ? "" : ProcessName(commandName);
}
[Pure]
private static string ProcessName(string name)
{
name = name.StripEnd("Command");
name = SpaceTitleCaseRegex().Replace(name, "$1 $2");
return name;
}
[GeneratedRegex("([a-z])_?([A-Z])")]
private static partial Regex SpaceTitleCaseRegex();
}

11
StabilityMatrix.Avalonia/Models/ITemplateKey.cs

@ -0,0 +1,11 @@
using StabilityMatrix.Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Models;
/// <summary>
/// Implements a template key for <see cref="DataTemplateSelector"/>
/// </summary>
public interface ITemplateKey<out T>
{
T TemplateKey { get; }
}

79
StabilityMatrix.Avalonia/Models/ImageSource.cs

@ -1,18 +1,21 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using AsyncImageLoader;
using Avalonia.Media.Imaging;
using Blake3;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Webp;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models;
public record ImageSource : IDisposable
public record ImageSource : IDisposable, ITemplateKey<ImageSourceTemplateType>
{
private Hash? contentHashBlake3;
@ -55,6 +58,80 @@ public record ImageSource : IDisposable
Bitmap = bitmap;
}
/// <inheritdoc />
public ImageSourceTemplateType TemplateKey { get; private set; }
private async Task<bool> TryRefreshTemplateKeyAsync()
{
if ((LocalFile?.Extension ?? Path.GetExtension(RemoteUrl?.ToString())) is not { } extension)
{
return false;
}
if (extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
{
if (LocalFile is not null && LocalFile.Exists)
{
await using var stream = LocalFile.Info.OpenRead();
using var reader = new WebpReader(stream);
try
{
TemplateKey = reader.GetIsAnimatedFlag()
? ImageSourceTemplateType.WebpAnimation
: ImageSourceTemplateType.Image;
}
catch (InvalidDataException)
{
return false;
}
return true;
}
if (RemoteUrl is not null)
{
var httpClientFactory = App.Services.GetRequiredService<IHttpClientFactory>();
using var client = httpClientFactory.CreateClient();
try
{
await using var stream = await client.GetStreamAsync(RemoteUrl);
using var reader = new WebpReader(stream);
TemplateKey = reader.GetIsAnimatedFlag()
? ImageSourceTemplateType.WebpAnimation
: ImageSourceTemplateType.Image;
}
catch (Exception)
{
return false;
}
return true;
}
return false;
}
TemplateKey = ImageSourceTemplateType.Image;
return true;
}
public async Task<ImageSourceTemplateType> GetOrRefreshTemplateKeyAsync()
{
if (TemplateKey is ImageSourceTemplateType.Default)
{
await TryRefreshTemplateKeyAsync();
}
return TemplateKey;
}
[JsonIgnore]
public Task<ImageSourceTemplateType> TemplateKeyAsync => GetOrRefreshTemplateKeyAsync();
[JsonIgnore]
public Task<Bitmap?> BitmapAsync => GetBitmapAsync();

8
StabilityMatrix.Avalonia/Models/ImageSourceTemplateType.cs

@ -0,0 +1,8 @@
namespace StabilityMatrix.Avalonia.Models;
public enum ImageSourceTemplateType
{
Default,
Image,
WebpAnimation
}

15
StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.Models.Inference;
@ -29,26 +29,19 @@ public class ModuleApplyStepEventArgs : EventArgs
/// <summary>
/// Generation overrides (like hires fix generate, current seed generate, etc.)
/// </summary>
public IReadOnlyDictionary<Type, bool> IsEnabledOverrides { get; init; } =
new Dictionary<Type, bool>();
public IReadOnlyDictionary<Type, bool> IsEnabledOverrides { get; init; } = new Dictionary<Type, bool>();
public class ModuleApplyStepTemporaryArgs
{
/// <summary>
/// Temporary conditioning apply step, used by samplers to apply control net.
/// </summary>
public (
ConditioningNodeConnection Positive,
ConditioningNodeConnection Negative
)? Conditioning { get; set; }
public ConditioningConnections? Conditioning { get; set; }
/// <summary>
/// Temporary refiner conditioning apply step, used by samplers to apply control net.
/// </summary>
public (
ConditioningNodeConnection Positive,
ConditioningNodeConnection Negative
)? RefinerConditioning { get; set; }
public ConditioningConnections? RefinerConditioning { get; set; }
/// <summary>
/// Temporary model apply step, used by samplers to apply control net.

11
StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Avalonia.Models.Inference;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum VideoOutputMethod
{
Fastest,
Default,
Slowest,
}

2
StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs

@ -33,7 +33,7 @@ public class InferenceProjectDocument : ICloneable
InferenceImageToImageViewModel => InferenceProjectType.ImageToImage,
InferenceTextToImageViewModel => InferenceProjectType.TextToImage,
InferenceImageUpscaleViewModel => InferenceProjectType.Upscale,
_ => throw new InvalidOperationException($"Unknown loadable model type: {loadableModel.GetType()}")
InferenceImageToVideoViewModel => InferenceProjectType.ImageToVideo,
},
State = loadableModel.SaveStateToJsonObject()
};

4
StabilityMatrix.Avalonia/Models/InferenceProjectType.cs

@ -9,7 +9,8 @@ public enum InferenceProjectType
TextToImage,
ImageToImage,
Inpainting,
Upscale
Upscale,
ImageToVideo
}
public static class InferenceProjectTypeExtensions
@ -22,6 +23,7 @@ public static class InferenceProjectTypeExtensions
InferenceProjectType.ImageToImage => typeof(InferenceImageToImageViewModel),
InferenceProjectType.Inpainting => null,
InferenceProjectType.Upscale => typeof(InferenceImageUpscaleViewModel),
InferenceProjectType.ImageToVideo => typeof(InferenceImageToVideoViewModel),
InferenceProjectType.Unknown => null,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};

10
StabilityMatrix.Avalonia/Models/PackageManagerNavigationOptions.cs

@ -0,0 +1,10 @@
using StabilityMatrix.Core.Models.Packages;
namespace StabilityMatrix.Avalonia.Models;
public record PackageManagerNavigationOptions
{
public bool OpenInstallerDialog { get; init; }
public BasePackage? InstallerSelectedPackage { get; init; }
}

52
StabilityMatrix.Avalonia/Models/SelectableItem.cs

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using JetBrains.Annotations;
namespace StabilityMatrix.Avalonia.Models;
[PublicAPI]
public class SelectableItem<T>(T item) : AbstractNotifyPropertyChanged, IEquatable<SelectableItem<T>>
{
public T Item { get; } = item;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => SetAndRaise(ref _isSelected, value);
}
public ICommand ToggleSelectedCommand => new RelayCommand(() => IsSelected = !IsSelected);
/// <inheritdoc />
public bool Equals(SelectableItem<T>? other)
{
if (ReferenceEquals(null, other))
return false;
if (ReferenceEquals(this, other))
return true;
return EqualityComparer<T>.Default.Equals(Item, other.Item);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
return Equals((SelectableItem<T>)obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(GetType().GetHashCode(), Item?.GetHashCode());
}
}

66
StabilityMatrix.Avalonia/Program.cs

@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia;
@ -55,8 +56,7 @@ public static class Program
SetDebugBuild();
var parseResult = Parser
.Default
.ParseArguments<AppArgs>(args)
.Default.ParseArguments<AppArgs>(args)
.WithNotParsed(errors =>
{
foreach (var error in errors)
@ -65,7 +65,7 @@ public static class Program
}
});
Args = parseResult.Value;
Args = parseResult.Value ?? new AppArgs();
if (Args.HomeDirectoryOverride is { } homeDir)
{
@ -147,7 +147,10 @@ public static class Program
if (Args.UseOpenGlRendering)
{
app = app.With(
new Win32PlatformOptions { RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software] }
new Win32PlatformOptions
{
RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software]
}
);
}
@ -156,7 +159,10 @@ public static class Program
app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } })
.With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } })
.With(
new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } }
new AvaloniaNativePlatformOptions
{
RenderingMode = new[] { AvaloniaNativeRenderingMode.Software }
}
);
}
@ -173,8 +179,6 @@ public static class Program
return;
// Copy our current file to the parent directory, overwriting the old app file
var currentExe = Compat.AppCurrentDir.JoinFile(Compat.GetExecutableName());
var targetExe = parentDir.JoinFile(Compat.GetExecutableName());
var isCopied = false;
@ -188,7 +192,27 @@ public static class Program
{
try
{
currentExe.CopyTo(targetExe, true);
if (Compat.IsMacOS)
{
var currentApp = Compat.AppBundleCurrentPath!;
var targetApp = parentDir.JoinDir(Compat.GetAppName());
// Since macOS has issues with signature caching, delete the target app first
if (targetApp.Exists)
{
targetApp.Delete(true);
}
currentApp.CopyTo(targetApp);
}
else
{
var currentExe = Compat.AppCurrentPath;
var targetExe = parentDir.JoinFile(Compat.GetExecutableName());
currentExe.CopyTo(targetExe, true);
}
isCopied = true;
break;
}
@ -204,11 +228,13 @@ public static class Program
Environment.Exit(1);
}
var targetAppOrBundle = Path.Combine(parentDir, Compat.GetAppName());
// Ensure permissions are set for unix
if (Compat.IsUnix)
{
File.SetUnixFileMode(
targetExe, // 0755
targetAppOrBundle, // 0755
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
@ -220,7 +246,10 @@ public static class Program
}
// Start the new app while passing our own PID to wait for exit
Process.Start(targetExe, $"--wait-for-exit-pid {Environment.ProcessId}");
ProcessRunner.StartApp(
targetAppOrBundle,
new[] { "--wait-for-exit-pid", $"{Environment.ProcessId}" }
);
// Shutdown the current app
Environment.Exit(0);
@ -274,7 +303,8 @@ public static class Program
{
SentrySdk.Init(o =>
{
o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.Dsn =
"https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.StackTraceMode = StackTraceMode.Enhanced;
o.TracesSampleRate = 1.0;
o.IsGlobalModeEnabled = true;
@ -301,7 +331,10 @@ public static class Program
});
}
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
private static void TaskScheduler_UnobservedTaskException(
object? sender,
UnobservedTaskExceptionEventArgs e
)
{
if (e.Exception is Exception ex)
{
@ -314,11 +347,13 @@ public static class Program
if (e.ExceptionObject is not Exception ex)
return;
SentryId? sentryId = null;
// Exception automatically logged by Sentry if enabled
if (SentrySdk.IsEnabled)
{
ex.SetSentryMechanism("AppDomain.UnhandledException", handled: false);
SentrySdk.CaptureException(ex);
sentryId = SentrySdk.CaptureException(ex);
SentrySdk.FlushAsync().SafeFireAndForget();
}
else
@ -328,7 +363,10 @@ public static class Program
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
var dialog = new ExceptionDialog { DataContext = new ExceptionViewModel { Exception = ex } };
var dialog = new ExceptionDialog
{
DataContext = new ExceptionViewModel { Exception = ex, SentryId = sentryId }
};
var mainWindow = lifetime.MainWindow;
// We can only show dialog if main window exists, and is visible

7
StabilityMatrix.Avalonia/Services/INavigationService.cs

@ -19,10 +19,7 @@ public interface INavigationService<[SuppressMessage("ReSharper", "UnusedTypePar
/// <summary>
/// Navigate to the view of the given view model type.
/// </summary>
void NavigateTo<TViewModel>(
NavigationTransitionInfo? transitionInfo = null,
object? param = null
)
void NavigateTo<TViewModel>(NavigationTransitionInfo? transitionInfo = null, object? param = null)
where TViewModel : ViewModelBase;
/// <summary>
@ -38,4 +35,6 @@ public interface INavigationService<[SuppressMessage("ReSharper", "UnusedTypePar
/// Navigate to the view of the given view model.
/// </summary>
void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null);
bool GoBack();
}

20
StabilityMatrix.Avalonia/Services/INotificationService.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Services;
@ -47,6 +48,20 @@ public interface INotificationService
NotificationType appearance = NotificationType.Error
);
/// <summary>
/// Show a keyed customizable persistent notification with the given parameters.
/// </summary>
Task ShowPersistentAsync(NotificationKey key, DesktopNotifications.Notification notification);
/// <summary>
/// Show a keyed customizable notification with the given parameters.
/// </summary>
Task ShowAsync(
NotificationKey key,
DesktopNotifications.Notification notification,
TimeSpan? expiration = null
);
/// <summary>
/// Show a notification with the given parameters.
/// </summary>
@ -77,4 +92,9 @@ public interface INotificationService
NotificationType appearance = NotificationType.Error,
LogLevel logLevel = LogLevel.Warning
);
/// <summary>
/// Get the native notification manager.
/// </summary>
Task<DesktopNotifications.INotificationManager?> GetNativeNotificationManagerAsync();
}

48
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -7,7 +7,9 @@ using StabilityMatrix.Avalonia.Animations;
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;
namespace StabilityMatrix.Avalonia.Services;
@ -20,6 +22,10 @@ namespace StabilityMatrix.Avalonia.Services;
ImplType = typeof(NavigationService<SettingsViewModel>),
InterfaceType = typeof(INavigationService<SettingsViewModel>)
)]
[Singleton(
ImplType = typeof(NavigationService<NewPackageManagerViewModel>),
InterfaceType = typeof(INavigationService<NewPackageManagerViewModel>)
)]
public class NavigationService<T> : INavigationService<T>
{
private Frame? _frame;
@ -33,10 +39,7 @@ public class NavigationService<T> : INavigationService<T>
}
/// <inheritdoc />
public void NavigateTo<TViewModel>(
NavigationTransitionInfo? transitionInfo = null,
object? param = null
)
public void NavigateTo<TViewModel>(NavigationTransitionInfo? transitionInfo = null, object? param = null)
where TViewModel : ViewModelBase
{
if (_frame is null)
@ -70,10 +73,7 @@ public class NavigationService<T> : INavigationService<T>
}
);
TypedNavigation?.Invoke(
this,
new TypedNavigationEventArgs { ViewModelType = typeof(TViewModel) }
);
TypedNavigation?.Invoke(this, new TypedNavigationEventArgs { ViewModelType = typeof(TViewModel) });
}
/// <inheritdoc />
@ -120,10 +120,7 @@ public class NavigationService<T> : INavigationService<T>
}
);
TypedNavigation?.Invoke(
this,
new TypedNavigationEventArgs { ViewModelType = viewModelType }
);
TypedNavigation?.Invoke(this, new TypedNavigationEventArgs { ViewModelType = viewModelType });
}
/// <inheritdoc />
@ -159,13 +156,36 @@ public class NavigationService<T> : INavigationService<T>
}
);
TypedNavigation?.Invoke(
this,
new TypedNavigationEventArgs { ViewModelType = viewModel.GetType(), ViewModel = viewModel }
);
}
public bool GoBack()
{
if (_frame?.Content is IHandleNavigation navigationHandler)
{
var wentBack = navigationHandler.GoBack();
if (wentBack)
{
return true;
}
}
if (_frame is not { CanGoBack: true })
return false;
TypedNavigation?.Invoke(
this,
new TypedNavigationEventArgs
{
ViewModelType = viewModel.GetType(),
ViewModel = viewModel
ViewModelType = _frame.BackStack.Last().SourcePageType,
ViewModel = _frame.BackStack.Last().Context
}
);
_frame.GoBack();
return true;
}
}

178
StabilityMatrix.Avalonia/Services/NotificationService.cs

@ -3,23 +3,32 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using DesktopNotifications.FreeDesktop;
using DesktopNotifications.Windows;
using Microsoft.Extensions.Logging;
using Nito.AsyncEx;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using INotificationManager = DesktopNotifications.INotificationManager;
namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(INotificationService))]
public class NotificationService : INotificationService
public class NotificationService(ILogger<NotificationService> logger, ISettingsManager settingsManager)
: INotificationService,
IDisposable
{
private readonly ILogger<NotificationService> logger;
private WindowNotificationManager? notificationManager;
public NotificationService(ILogger<NotificationService> logger)
{
this.logger = logger;
}
private readonly AsyncLock nativeNotificationManagerLock = new();
private volatile INotificationManager? nativeNotificationManager;
private volatile bool isNativeNotificationManagerInitialized;
public void Initialize(
Visual? visual,
@ -41,6 +50,115 @@ public class NotificationService : INotificationService
notificationManager?.Show(notification);
}
/// <inheritdoc />
public Task ShowPersistentAsync(NotificationKey key, DesktopNotifications.Notification notification)
{
return ShowAsyncCore(key, notification, null, true);
}
/// <inheritdoc />
public Task ShowAsync(
NotificationKey key,
DesktopNotifications.Notification notification,
TimeSpan? expiration = null
)
{
// Use default expiration if not specified
expiration ??= TimeSpan.FromSeconds(5);
return ShowAsyncCore(key, notification, expiration, false);
}
private async Task ShowAsyncCore(
NotificationKey key,
DesktopNotifications.Notification notification,
TimeSpan? expiration,
bool isPersistent
)
{
// If settings has option preference, use that, otherwise default
if (!settingsManager.Settings.NotificationOptions.TryGetValue(key, out var option))
{
option = key.DefaultOption;
}
switch (option)
{
case NotificationOption.None:
break;
case NotificationOption.NativePush:
{
// If native option is not supported, fallback to toast
if (await GetNativeNotificationManagerAsync() is not { } nativeManager)
{
// Show app toast
if (isPersistent)
{
Dispatcher.UIThread.Invoke(
() =>
ShowPersistent(
notification.Title ?? "",
notification.Body ?? "",
key.Level.ToNotificationType()
)
);
}
else
{
Dispatcher.UIThread.Invoke(
() =>
Show(
notification.Title ?? "",
notification.Body ?? "",
key.Level.ToNotificationType(),
expiration
)
);
}
return;
}
// Show native notification
await nativeManager.ShowNotification(
notification,
expiration is null ? null : DateTimeOffset.Now.Add(expiration.Value)
);
break;
}
case NotificationOption.AppToast:
// Show app toast
if (isPersistent)
{
Dispatcher.UIThread.Invoke(
() =>
ShowPersistent(
notification.Title ?? "",
notification.Body ?? "",
key.Level.ToNotificationType()
)
);
}
else
{
Dispatcher.UIThread.Invoke(
() =>
Show(
notification.Title ?? "",
notification.Body ?? "",
key.Level.ToNotificationType(),
expiration
)
);
}
break;
default:
logger.LogError("Unknown notification option {Option}", option);
break;
}
}
public void Show(
string title,
string message,
@ -111,4 +229,52 @@ public class NotificationService : INotificationService
return new TaskResult<bool>(false, e);
}
}
public void Dispose()
{
nativeNotificationManager?.Dispose();
GC.SuppressFinalize(this);
}
public async Task<INotificationManager?> GetNativeNotificationManagerAsync()
{
if (isNativeNotificationManagerInitialized)
return nativeNotificationManager;
using var _ = await nativeNotificationManagerLock.LockAsync();
if (isNativeNotificationManagerInitialized)
return nativeNotificationManager;
try
{
if (Compat.IsWindows)
{
var context = WindowsApplicationContext.FromCurrentProcess("Stability Matrix");
nativeNotificationManager = new WindowsNotificationManager(context);
await nativeNotificationManager.Initialize();
}
else if (Compat.IsLinux)
{
var context = FreeDesktopApplicationContext.FromCurrentProcess();
nativeNotificationManager = new FreeDesktopNotificationManager(context);
await nativeNotificationManager.Initialize();
}
else
{
logger.LogInformation("Native notifications are not supported on this platform");
}
}
catch (Exception e)
{
logger.LogError(e, "Failed to initialize native notification manager");
}
isNativeNotificationManagerInitialized = true;
return nativeNotificationManager;
}
}

92
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -1,19 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Condition="'$(RuntimeIdentifier)' != 'win-x64'">
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<CFBundleName>Stability Matrix</CFBundleName>
<CFBundleIconFile>Assets/AppIcon.icns</CFBundleIconFile>
<CFBundlePackageType>APPL</CFBundlePackageType>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.7.0-pre.999</Version>
<Version>2.8.0-dev.999</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<CFBundleURLTypes Include="dummy">
<CFBundleURLName>StabilityMatrix.URL</CFBundleURLName>
<CFBundleURLSchemes>stabilitymatrix;stabilitymatrix://</CFBundleURLSchemes>
</CFBundleURLTypes>
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StabilityMatrix.Tests" />
<InternalsVisibleTo Include="StabilityMatrix.UITests" />
@ -22,30 +38,35 @@
<ItemGroup>
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
<PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/>
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.5" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.7" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" />
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
<PackageReference Include="Avalonia" Version="11.0.7" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.7" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.7" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.7" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.5" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.5" />
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.0.5.1" />
<PackageReference Include="bodong.PropertyModels" Version="11.0.5.1" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.0.6.3" />
<PackageReference Include="bodong.PropertyModels" Version="11.0.6.2" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="DesktopNotifications" Version="1.3.1" />
<PackageReference Include="DesktopNotifications.Avalonia" Version="1.3.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Dock.Avalonia" Version="11.0.0.5" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.5" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.5" />
<PackageReference Include="DynamicData" Version="8.3.27" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.224" />
<PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.224" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.225" />
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.225" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
@ -56,17 +77,17 @@
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NLog" Version="5.2.7" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.7" />
<PackageReference Include="NLog" Version="5.2.8" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="Polly" Version="8.2.0" />
<PackageReference Include="Polly" Version="8.2.1" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.0.1" />
<PackageReference Include="RockLib.Reflection.Optimized" Version="2.0.0" />
<PackageReference Include="RockLib.Reflection.Optimized" Version="3.0.0" />
<PackageReference Include="Sentry" Version="3.41.3" />
<PackageReference Include="Sentry.NLog" Version="3.41.3" />
<PackageReference Include="SpacedGrid-Avalonia" Version="11.0.0" />
@ -91,6 +112,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Gif\Avalonia.Gif.csproj" />
<ProjectReference Include="..\StabilityMatrix.Core\StabilityMatrix.Core.csproj" />
</ItemGroup>
@ -128,6 +150,12 @@
<!-- Only for osx-arm64 -->
<AvaloniaResource Include="Assets\macos-arm64\**" Condition="'$(RuntimeIdentifier)' == 'osx-arm64'" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\AppIcon.icns" Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Languages\Resources.resx">
@ -142,6 +170,30 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Views\Inference\InferenceImageToVideoView.axaml.cs">
<DependentUpon>InferenceImageToVideoView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Controls\VideoGenerationSettingsCard.axaml.cs">
<DependentUpon>VideoGenerationSettingsCard.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Controls\VideoOutputSettingsCard.axaml.cs">
<DependentUpon>VideoOutputSettingsCard.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\NewPackageManagerPage.axaml.cs">
<DependentUpon>NewPackageManagerPage.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\PackageManager\PackageInstallDetailView.axaml.cs">
<DependentUpon>PackageInstallDetailView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\PackageManager\PackageInstallBrowserView.axaml.cs">
<DependentUpon>NewInstallerDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<!-- set HUSKY to 0 to disable, or opt-in during CI by setting HUSKY to 1 -->

254
StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml

@ -0,0 +1,254 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<Design.PreviewWith>
<Panel Width="450" Height="600">
<StackPanel Margin="8" Spacing="4" Width="250">
<controls:BetterComboBox
HorizontalAlignment="Stretch"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"
SelectedIndex="0" />
<controls:BetterComboBox
HorizontalAlignment="Stretch"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"
SelectedIndex="0"
Theme="{DynamicResource BetterComboBoxHybridModelTheme}" />
</StackPanel>
</Panel>
</Design.PreviewWith>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="BetterComboBoxItemHybridModelTheme"
BasedOn="{StaticResource {x:Type ComboBoxItem}}"
TargetType="ComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<sg:SpacedGrid
x:DataType="models:HybridModelFile"
ColumnDefinitions="Auto,*"
ColumnSpacing="6"
RowSpacing="0">
<!-- Image -->
<controls:BetterAdvancedImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<StackPanel
Grid.Column="1"
MaxWidth="300"
VerticalAlignment="Stretch">
<!-- Title -->
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Version -->
<TextBlock
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Path -->
<TextBlock
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding FileName}"
TextWrapping="Wrap" />
</StackPanel>
</sg:SpacedGrid>
</Template>
</Setter>
</ControlTheme>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="{x:Type controls:BetterComboBox}"
BasedOn="{StaticResource {x:Type ComboBox}}"
TargetType="controls:BetterComboBox" />
<ControlTheme
x:Key="BetterComboBoxHybridModelTheme"
BasedOn="{StaticResource {x:Type controls:BetterComboBox}}"
TargetType="controls:BetterComboBox">
<ControlTheme.Resources>
<controls:HybridModelTemplateSelector x:Key="HybridModelTemplateSelector">
<DataTemplate x:Key="{x:Static models:HybridModelType.Downloadable}" DataType="models:HybridModelFile">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Foreground="{DynamicResource ThemeGreyColor}" Text="{Binding ShortDisplayName}" />
<Button
Grid.Column="1"
Margin="8,0,0,0"
Padding="0"
Classes="transparent-full">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="18"
Foreground="{DynamicResource ThemeGreyColor}"
IsFilled="True"
Symbol="CloudArrowDown" />
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.Local}" DataType="models:HybridModelFile">
<sg:SpacedGrid
HorizontalAlignment="Stretch"
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
Width="42"
Height="42"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,42,42" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<!-- Text -->
<sg:SpacedGrid
Grid.Row="1"
Grid.Column="1"
RowDefinitions="Auto,Auto,Auto"
RowSpacing="1">
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="1"
FontSize="12"
FontWeight="Regular"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Local.DisplayModelVersion}"
TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="2"
FontSize="11"
FontWeight="Normal"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding Local.DisplayModelFileName}" />
</sg:SpacedGrid>
</sg:SpacedGrid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile">
<StackPanel>
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</controls:HybridModelTemplateSelector>
<controls:HybridModelTemplateSelector x:Key="HybridModelSelectionBoxTemplateSelector">
<DataTemplate x:Key="{x:Static models:HybridModelType.Downloadable}" DataType="models:HybridModelFile">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Foreground="{DynamicResource ThemeGreyColor}" Text="{Binding ShortDisplayName}" />
<Button
Grid.Column="1"
Margin="8,0,0,0"
Padding="0"
Classes="transparent-full">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="18"
Foreground="{DynamicResource ThemeGreyColor}"
IsFilled="True"
Symbol="CloudArrowDown" />
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.Local}" DataType="models:HybridModelFile">
<sg:SpacedGrid
HorizontalAlignment="Stretch"
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
Width="36"
Height="36"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,36,36" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<!-- Text -->
<sg:SpacedGrid
Grid.Row="1"
Grid.Column="1"
RowDefinitions="Auto,Auto"
RowSpacing="1">
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="1"
FontSize="12"
FontWeight="Regular"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Local.DisplayModelVersion}"
TextTrimming="CharacterEllipsis" />
</sg:SpacedGrid>
</sg:SpacedGrid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile">
<StackPanel>
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</controls:HybridModelTemplateSelector>
</ControlTheme.Resources>
<Setter Property="TextBlock.TextWrapping" Value="NoWrap" />
<Setter Property="SelectionBoxItemTemplate" Value="{StaticResource HybridModelSelectionBoxTemplateSelector}" />
<Setter Property="ItemTemplate" Value="{StaticResource HybridModelTemplateSelector}" />
<Setter Property="ItemContainerTheme" Value="{StaticResource BetterComboBoxItemHybridModelTheme}" />
<Style Selector="^ /template/ Popup#PART_Popup">
<Setter Property="Width" Value="400" />
<Setter Property="Placement" Value="Bottom" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="Effect">
<DropShadowEffect
BlurRadius="32"
Opacity="0.6"
Color="#FF000000" />
</Setter>
</Style>
</ControlTheme>
</ResourceDictionary>

48
StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml

@ -2,26 +2,51 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:ui="using:FluentAvalonia.UI.Controls">
<Design.PreviewWith>
<StackPanel Margin="8" Spacing="6">
<controls:HyperlinkIconButton Content="Link"/>
<controls:HyperlinkIconButton Content="Disabled" IsEnabled="False"/>
<controls:HyperlinkIconButton Content="File" Icon="Open"/>
</StackPanel>
</Design.PreviewWith>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="{x:Type controls:HyperlinkIconButton}"
BasedOn="{StaticResource {x:Type ui:HyperlinkButton}}"
TargetType="controls:HyperlinkIconButton">
<Setter Property="Background" Value="{DynamicResource HyperlinkButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource HyperlinkButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource HyperlinkButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Icon" Value="Link"/>
<Setter Property="ContentTemplate">
<DataTemplate DataType="x:String">
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
Margin="0,1,4,0"
FontSize="15"
Foreground="{DynamicResource HyperlinkButtonForeground}"
Symbol="Link" />
Symbol="{Binding $parent[controls:HyperlinkIconButton].Icon}" />
<TextBlock
Foreground="{DynamicResource HyperlinkButtonForeground}" Text="{Binding}" />
</StackPanel>
</DataTemplate>
</Setter>
<!-- Override template to not set Underline TextDecoration -->
<Setter Property="Template">
<ControlTemplate>
@ -39,6 +64,23 @@
</ContentPresenter>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForegroundPointerOver}" />
<Setter Property="Background" Value="{DynamicResource HyperlinkButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource HyperlinkButtonBorderBrushPointerOver}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForegroundPressed}" />
<Setter Property="Background" Value="{DynamicResource HyperlinkButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource HyperlinkButtonBorderBrushPressed}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForegroundDisabled}" />
<Setter Property="Background" Value="{DynamicResource HyperlinkButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource HyperlinkButtonBorderBrushDisabled}" />
</Style>
</ControlTheme>
</ResourceDictionary>

50
StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml

@ -2,33 +2,36 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<Design.PreviewWith>
<Panel Width="400" Height="600">
<Panel Width="350" Height="200" />
<Panel Width="250" Height="600">
<StackPanel Margin="8" Spacing="4">
<ui:FAComboBox
HorizontalAlignment="Stretch"
SelectedIndex="0"
Theme="{DynamicResource FAComboBoxHybridModelTheme}"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"/>
</StackPanel>
</Panel>
</Design.PreviewWith>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="FAComboBoxItemHybridModelTheme"
BasedOn="{StaticResource {x:Type ui:FAComboBoxItem}}"
TargetType="ui:FAComboBoxItem">
<!--<Setter Property="ContentTemplate">
<DataTemplate DataType="models:HybridModelFile">
<StackPanel>
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</Setter>-->
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
<sg:SpacedGrid
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
ColumnDefinitions="Auto,*"
ColumnSpacing="6"
RowSpacing="0">
<!-- Image -->
<controls:BetterAdvancedImage
Width="64"
@ -39,10 +42,7 @@
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Grid
MaxWidth="300"
VerticalAlignment="Stretch"
RowDefinitions="Auto,Auto,*">
<StackPanel Grid.Column="1" MaxWidth="300">
<!-- Title -->
<TextBlock
Margin="0,0,0,4"
@ -51,32 +51,30 @@
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
Text="{Binding Local.DisplayModelName}"
TextWrapping="WrapWithOverflow" />
<!-- Version -->
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
Text="{Binding Local.DisplayModelVersion}"
TextWrapping="WrapWithOverflow" />
<!-- Path -->
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding FileName}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</StackPanel>
</sg:SpacedGrid>
</Template>
</Setter>
</ControlTheme>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme x:Key="FAComboBoxHybridModelTheme"
TargetType="ui:FAComboBox"
@ -88,7 +86,7 @@
</StackPanel>
</DataTemplate>
</Setter>
<Setter Property="ItemContainerTheme" Value="{StaticResource FAComboBoxItemHybridModelTheme}"/>
<Setter Property="ItemContainerTheme" Value="{StaticResource FAComboBoxItemHybridModelTheme}" />
</ControlTheme>
</ResourceDictionary>

37
StabilityMatrix.Avalonia/Styles/TextBoxStyles.axaml

@ -0,0 +1,37 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fluentIcons="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters">
<Design.PreviewWith>
<Panel MinWidth="300">
<StackPanel Margin="16" Spacing="6">
<TextBox Classes="search" />
<TextBox Classes="search" Text="Some Text" />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Styles.Resources>
<converters:FuncCommandConverter x:Key="FuncCommandConverter"/>
</Styles.Resources>
<!-- Success -->
<Style Selector="TextBox.search">
<Setter Property="InnerRightContent">
<Template>
<Grid>
<Button Classes="transparent-full"
IsVisible="{Binding $parent[TextBox].Text, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[TextBox], Converter={StaticResource FuncCommandConverter}, ConverterParameter=Clear}">
<fluentIcons:SymbolIcon Symbol="Cancel" />
</Button>
<fluentIcons:SymbolIcon
IsVisible="{Binding $parent[TextBox].Text, Converter={x:Static StringConverters.IsNullOrEmpty}}"
Margin="0,0,10,0"
FontSize="16"
Symbol="Find" />
</Grid>
</Template>
</Setter>
</Style>
</Styles>

230
StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Management;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@ -13,6 +14,8 @@ using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using ExifLibrary;
using MetadataExtractor.Formats.Exif;
using NLog;
using Refit;
using SkiaSharp;
@ -24,6 +27,7 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Core.Animation;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
@ -33,7 +37,9 @@ using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = DesktopNotifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
@ -42,7 +48,9 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// This includes a progress reporter, image output view model, and generation virtual methods.
/// </summary>
[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
public abstract partial class InferenceGenerationViewModelBase : InferenceTabViewModelBase, IImageGalleryComponent
public abstract partial class InferenceGenerationViewModelBase
: InferenceTabViewModelBase,
IImageGalleryComponent
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -91,13 +99,22 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
bool isGrid = false
bool isGrid = false,
string fileExtension = "png"
)
{
var defaultOutputDir = settingsManager.ImagesInferenceDirectory;
defaultOutputDir.Create();
return WriteOutputImageAsync(imageStream, defaultOutputDir, args, batchNum, batchTotal, isGrid);
return WriteOutputImageAsync(
imageStream,
defaultOutputDir,
args,
batchNum,
batchTotal,
isGrid,
fileExtension
);
}
/// <summary>
@ -109,7 +126,8 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
bool isGrid = false
bool isGrid = false,
string fileExtension = "png"
)
{
var formatTemplateStr = settingsManager.Settings.InferenceOutputImageFileNameFormat;
@ -128,7 +146,10 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
)
{
// Fallback to default
Logger.Warn("Failed to parse format template: {FormatTemplate}, using default", formatTemplateStr);
Logger.Warn(
"Failed to parse format template: {FormatTemplate}, using default",
formatTemplateStr
);
format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider);
}
@ -144,7 +165,7 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
}
var fileName = format.GetFileName();
var file = outputDir.JoinFile($"{fileName}.png");
var file = outputDir.JoinFile($"{fileName}.{fileExtension}");
// Until the file is free, keep adding _{i} to the end
for (var i = 0; i < 100; i++)
@ -152,14 +173,19 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
if (!file.Exists)
break;
file = outputDir.JoinFile($"{fileName}_{i + 1}.png");
file = outputDir.JoinFile($"{fileName}_{i + 1}.{fileExtension}");
}
// If that fails, append an 7-char uuid
if (file.Exists)
{
var uuid = Guid.NewGuid().ToString("N")[..7];
file = outputDir.JoinFile($"{fileName}_{uuid}.png");
file = outputDir.JoinFile($"{fileName}_{uuid}.{fileExtension}");
}
if (file.Info.DirectoryName != null)
{
Directory.CreateDirectory(file.Info.DirectoryName);
}
await using var fileStream = file.Info.OpenWrite();
@ -271,17 +297,14 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
Task.Run(
async () =>
{
try
var delayTime = 250 - (int)timer.ElapsedMilliseconds;
if (delayTime > 0)
{
var delayTime = 250 - (int)timer.ElapsedMilliseconds;
if (delayTime > 0)
{
await Task.Delay(delayTime, cancellationToken);
}
// ReSharper disable once AccessToDisposedClosure
AttachRunningNodeChangedHandler(promptTask);
await Task.Delay(delayTime, cancellationToken);
}
catch (TaskCanceledException) { }
// ReSharper disable once AccessToDisposedClosure
AttachRunningNodeChangedHandler(promptTask);
},
cancellationToken
)
@ -305,10 +328,17 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
// Get output images
var imageOutputs = await client.GetImagesForExecutedPromptAsync(promptTask.Id, cancellationToken);
if (imageOutputs.Values.All(images => images is null or { Count: 0 }))
if (
!imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images)
|| images is not { Count: > 0 }
)
{
// No images match
notificationService.Show("No output", "Did not receive any output images", NotificationType.Warning);
notificationService.Show(
"No output",
"Did not receive any output images",
NotificationType.Warning
);
return;
}
@ -320,7 +350,19 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
ImageGalleryCardViewModel.ImageSources.Clear();
}
await ProcessAllOutputImages(imageOutputs, args);
var outputImages = await ProcessOutputImages(images, args);
var notificationImage = outputImages.FirstOrDefault()?.LocalFile;
await notificationService.ShowAsync(
NotificationKey.Inference_PromptCompleted,
new Notification
{
Title = "Prompt Completed",
Body = $"Prompt [{promptTask.Id[..7].ToLower()}] completed successfully",
BodyImagePath = notificationImage?.FullPath
}
);
}
finally
{
@ -338,30 +380,12 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
}
}
private async Task ProcessAllOutputImages(
IReadOnlyDictionary<string, List<ComfyImage>?> images,
ImageGenerationEventArgs args
)
{
foreach (var (nodeName, imageList) in images)
{
if (imageList is null)
{
Logger.Warn("No images for node {NodeName}", nodeName);
continue;
}
await ProcessOutputImages(imageList, args, nodeName.Replace('_', ' '));
}
}
/// <summary>
/// Handles image output metadata for generation runs
/// </summary>
private async Task ProcessOutputImages(
private async Task<List<ImageSource>> ProcessOutputImages(
IReadOnlyCollection<ComfyImage> images,
ImageGenerationEventArgs args,
string? imageLabel = null
ImageGenerationEventArgs args
)
{
var client = args.Client;
@ -405,14 +429,64 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
);
}
var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
if (comfyImage.FileName.EndsWith(".png"))
{
var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
// Write using generated name
var filePath = await WriteOutputImageAsync(
new MemoryStream(bytesWithMetadata),
args,
i + 1,
images.Count
);
// Write using generated name
var filePath = await WriteOutputImageAsync(new MemoryStream(bytesWithMetadata), args, i + 1, images.Count);
outputImages.Add(new ImageSource(filePath));
EventManager.Instance.OnImageFileAdded(filePath);
}
else if (comfyImage.FileName.EndsWith(".webp"))
{
var opts = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
var paramsJson = JsonSerializer.Serialize(parameters, opts);
var smProject = JsonSerializer.Serialize(project, opts);
var metadata = new Dictionary<ExifTag, string>
{
{ ExifTag.ImageDescription, paramsJson },
{ ExifTag.Software, smProject }
};
var bytesWithMetadata = ImageMetadata.AddMetadataToWebp(imageArray, metadata);
// Write using generated name
var filePath = await WriteOutputImageAsync(
new MemoryStream(bytesWithMetadata.ToArray()),
args,
i + 1,
images.Count,
fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "")
);
outputImages.Add(new ImageSource(filePath) { Label = imageLabel });
outputImages.Add(new ImageSource(filePath));
EventManager.Instance.OnImageFileAdded(filePath);
}
else
{
// Write using generated name
var filePath = await WriteOutputImageAsync(
new MemoryStream(imageArray),
args,
i + 1,
images.Count,
fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "")
);
EventManager.Instance.OnImageFileAdded(filePath);
outputImages.Add(new ImageSource(filePath));
EventManager.Instance.OnImageFileAdded(filePath);
}
}
// Download all images to make grid, if multiple
@ -430,25 +504,27 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
var gridBytesWithMetadata = PngDataHelper.AddMetadata(gridBytes, args.Parameters!, args.Project!);
// Save to disk
var gridPath = await WriteOutputImageAsync(new MemoryStream(gridBytesWithMetadata), args, isGrid: true);
var gridPath = await WriteOutputImageAsync(
new MemoryStream(gridBytesWithMetadata),
args,
isGrid: true
);
// Insert to start of images
var gridImage = new ImageSource(gridPath) { Label = imageLabel };
// Preload
await gridImage.GetBitmapAsync();
ImageGalleryCardViewModel.ImageSources.Add(gridImage);
var gridImage = new ImageSource(gridPath);
outputImages.Insert(0, gridImage);
EventManager.Instance.OnImageFileAdded(gridPath);
}
// Add rest of images
foreach (var img in outputImages)
{
// Preload
await img.GetBitmapAsync();
// Add images
ImageGalleryCardViewModel.ImageSources.Add(img);
}
return outputImages;
}
/// <summary>
@ -465,7 +541,10 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
/// <param name="options">Optional overrides (side buttons)</param>
/// <param name="cancellationToken">Cancellation token</param>
[RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)]
private async Task GenerateImage(GenerateFlags options = default, CancellationToken cancellationToken = default)
private async Task GenerateImage(
GenerateFlags options = default,
CancellationToken cancellationToken = default
)
{
var overrides = GenerateOverrides.FromFlags(options);
@ -475,12 +554,7 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
}
catch (OperationCanceledException)
{
Logger.Debug("Image Generation Canceled");
}
catch (ValidationException e)
{
Logger.Debug("Image Generation Validation Error: {Message}", e.Message);
notificationService.Show("Validation Error", e.Message, NotificationType.Error);
Logger.Debug($"Image Generation Canceled");
}
}
@ -513,17 +587,15 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
/// </summary>
protected virtual void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args)
{
Dispatcher
.UIThread
.Post(() =>
{
OutputProgress.Value = args.Value;
OutputProgress.Maximum = args.Maximum;
OutputProgress.IsIndeterminate = false;
Dispatcher.UIThread.Post(() =>
{
OutputProgress.Value = args.Value;
OutputProgress.Maximum = args.Maximum;
OutputProgress.IsIndeterminate = false;
OutputProgress.Text =
$"({args.Value} / {args.Maximum})" + (args.RunningNode != null ? $" {args.RunningNode}" : "");
});
OutputProgress.Text =
$"({args.Value} / {args.Maximum})" + (args.RunningNode != null ? $" {args.RunningNode}" : "");
});
}
private void AttachRunningNodeChangedHandler(ComfyTask comfyTask)
@ -548,15 +620,13 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
return;
}
Dispatcher
.UIThread
.Post(() =>
{
OutputProgress.IsIndeterminate = true;
OutputProgress.Value = 100;
OutputProgress.Maximum = 100;
OutputProgress.Text = nodeName;
});
Dispatcher.UIThread.Post(() =>
{
OutputProgress.IsIndeterminate = true;
OutputProgress.Value = 100;
OutputProgress.Maximum = 100;
OutputProgress.Text = nodeName;
});
}
public class ImageGenerationEventArgs : EventArgs

54
StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs

@ -28,24 +28,16 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly Type[] SerializerIgnoredTypes =
{
typeof(ICommand),
typeof(IRelayCommand)
};
private static readonly Type[] SerializerIgnoredTypes = { typeof(ICommand), typeof(IRelayCommand) };
private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors) };
private static readonly JsonSerializerOptions SerializerOptions =
new() { IgnoreReadOnlyProperties = true };
private static readonly JsonSerializerOptions SerializerOptions = new() { IgnoreReadOnlyProperties = true };
private static bool ShouldIgnoreProperty(PropertyInfo property)
{
// Skip if read-only and not IJsonLoadableState
if (
property.SetMethod is null
&& !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)
)
if (property.SetMethod is null && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType))
{
Logger.ConditionalTrace("Skipping {Property} - read-only", property.Name);
return true;
@ -107,11 +99,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
// Get all of our properties using reflection
var properties = GetType().GetProperties();
Logger.ConditionalTrace(
"Serializing {Type} with {Count} properties",
GetType(),
properties.Length
);
Logger.ConditionalTrace("Serializing {Type} with {Count} properties", GetType(), properties.Length);
foreach (var property in properties)
{
@ -119,9 +107,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
// If JsonPropertyName provided, use that as the key
if (
property
.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true)
.FirstOrDefault()
property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).FirstOrDefault()
is JsonPropertyNameAttribute jsonPropertyName
)
{
@ -168,10 +154,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
if (property.GetValue(this) is not IJsonLoadableState propertyValue)
{
// If null, it must have a default constructor
if (
property.PropertyType.GetConstructor(Type.EmptyTypes)
is not { } constructorInfo
)
if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo)
{
throw new InvalidOperationException(
$"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor"
@ -188,11 +171,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
}
else
{
Logger.ConditionalTrace(
"Loading {Property} ({Type})",
property.Name,
property.PropertyType
);
Logger.ConditionalTrace("Loading {Property} ({Type})", property.Name, property.PropertyType);
var propertyValue = value.Deserialize(property.PropertyType, SerializerOptions);
property.SetValue(this, propertyValue);
@ -216,11 +195,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
// Get all of our properties using reflection.
var properties = GetType().GetProperties();
Logger.ConditionalTrace(
"Serializing {Type} with {Count} properties",
GetType(),
properties.Length
);
Logger.ConditionalTrace("Serializing {Type} with {Count} properties", GetType(), properties.Length);
// Create a JSON object to store the state.
var state = new JsonObject();
@ -237,9 +212,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
// If JsonPropertyName provided, use that as the key.
if (
property
.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true)
.FirstOrDefault()
property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).FirstOrDefault()
is JsonPropertyNameAttribute jsonPropertyName
)
{
@ -270,11 +243,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
}
else
{
Logger.ConditionalTrace(
"Serializing {Property} ({Type})",
property.Name,
property.PropertyType
);
Logger.ConditionalTrace("Serializing {Property} ({Type})", property.Name, property.PropertyType);
var value = property.GetValue(this);
if (value is not null)
{
@ -297,8 +266,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
protected static JsonObject SerializeModel<T>(T model)
{
var node = JsonSerializer.SerializeToNode(model);
return node?.AsObject()
?? throw new NullReferenceException("Failed to serialize state to JSON object.");
return node?.AsObject() ?? throw new NullReferenceException("Failed to serialize state to JSON object.");
}
/// <summary>

62
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

@ -21,6 +21,7 @@ using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@ -120,30 +121,24 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var latestVersionInstalled =
latestVersion.Files != null
&& latestVersion
.Files
.Any(
file =>
file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
&& installedModels.Contains(file.Hashes.BLAKE3)
);
&& latestVersion.Files.Any(
file =>
file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
&& installedModels.Contains(file.Hashes.BLAKE3)
);
// check if any of the ModelVersion.Files.Hashes.BLAKE3 hashes are in the installedModels list
var anyVersionInstalled =
latestVersionInstalled
|| CivitModel
.ModelVersions
.Any(
version =>
version.Files != null
&& version
.Files
.Any(
file =>
file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
&& installedModels.Contains(file.Hashes.BLAKE3)
)
);
|| CivitModel.ModelVersions.Any(
version =>
version.Files != null
&& version.Files.Any(
file =>
file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
&& installedModels.Contains(file.Hashes.BLAKE3)
)
);
UpdateCardText = latestVersionInstalled
? "Installed"
@ -154,7 +149,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
ShowUpdateCard = anyVersionInstalled;
}
// Choose and load image based on nsfw setting
private void UpdateImage()
{
var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled;
@ -162,21 +156,17 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var images = version?.Images;
// Try to find a valid image
var image = images?.FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None");
var image = images
?.Where(img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains))
.FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None");
if (image != null)
{
// var imageStream = await downloadService.GetImageStreamFromUrl(image.Url);
// Dispatcher.UIThread.Post(() => { CardImage = new Bitmap(imageStream); });
CardImage = new Uri(image.Url);
return;
}
// If no valid image found, use no image
CardImage = Assets.NoImage;
// var assetStream = AssetLoader.Open(new Uri("avares://StabilityMatrix.Avalonia/Assets/noimage.png"));
// Otherwise Default image
// Dispatcher.UIThread.Post(() => { CardImage = new Bitmap(assetStream); });
}
[RelayCommand]
@ -270,8 +260,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
{
var prunedDescription =
model
.Description
?.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}")
.Description?.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("<br />", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</p>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h1>", $"{Environment.NewLine}{Environment.NewLine}")
@ -318,9 +307,9 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
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 imageDownloadPath = modelFilePath.Directory!.JoinFile(
$"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}"
);
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
await notificationService.TryAsync(imageTask, "Could not download preview image");
@ -358,7 +347,8 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
}
// Get latest version file
var modelFile = selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
var modelFile =
selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
if (modelFile is null)
{
notificationService.Show(
@ -374,7 +364,9 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir(model.Type.ConvertTo<SharedFolderType>().GetStringValue());
var downloadDirectory = rootModelsDirectory.JoinDir(
model.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
// Folders might be missing if user didn't install any packages yet
downloadDirectory.Create();

57
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

@ -18,6 +18,7 @@ using CommunityToolkit.Mvvm.Input;
using LiteDB;
using LiteDB.Async;
using NLog;
using OneOf.Types;
using Refit;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
@ -129,18 +130,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0)
.OrderBy(t => t.ToString());
public List<string> BaseModelOptions =>
[
"All",
"SD 1.5",
"SD 1.5 LCM",
"SD 2.1",
"SDXL 0.9",
"SDXL 1.0",
"SDXL 1.0 LCM",
"SDXL Turbo",
"Other"
];
public IEnumerable<string> BaseModelOptions =>
Enum.GetValues<CivitBaseModelType>().Select(t => t.GetStringValue());
public CivitAiBrowserViewModel(
ICivitApi civitApi,
@ -170,6 +161,17 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested;
}
private void OnNavigateAndFindCivitModelRequested(object? sender, int e)
{
if (e <= 0)
return;
SearchQuery = $"$#{e}";
SearchModelsCommand.ExecuteAsync(null).SafeFireAndForget();
}
public override void OnLoaded()
@ -409,6 +411,16 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
Page = CurrentPageNumber
};
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = [SelectedModelType];
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SearchQuery.StartsWith("#"))
{
modelRequest.Tag = SearchQuery[1..];
@ -417,19 +429,22 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase
{
modelRequest.Username = SearchQuery[1..];
}
else
else if (SearchQuery.StartsWith("$#"))
{
modelRequest.Query = SearchQuery;
}
modelRequest.Period = CivitPeriod.AllTime;
modelRequest.BaseModel = null;
modelRequest.Types = null;
modelRequest.CommaSeparatedModelIds = SearchQuery[2..];
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = new[] { SelectedModelType };
if (modelRequest.Sort is CivitSortMode.Favorites or CivitSortMode.Installed)
{
SortMode = CivitSortMode.HighestRated;
modelRequest.Sort = CivitSortMode.HighestRated;
}
}
if (SelectedBaseModelType != "All")
else
{
modelRequest.BaseModel = SelectedBaseModelType;
modelRequest.Query = SearchQuery;
}
if (SortMode == CivitSortMode.Installed)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save