Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 87b2dd0cff
Bump DeviceId.Linux and DeviceId 11 months ago
  1. 37
      .github/workflows/backport.yml
  2. 205
      .github/workflows/release.yml
  3. 1
      .gitignore
  4. 18
      Avalonia.Gif/Avalonia.Gif.csproj
  5. 10
      Avalonia.Gif/BgWorkerCommand.cs
  6. 12
      Avalonia.Gif/BgWorkerState.cs
  7. 10
      Avalonia.Gif/Decoding/BlockTypes.cs
  8. 8
      Avalonia.Gif/Decoding/ExtensionType.cs
  9. 10
      Avalonia.Gif/Decoding/FrameDisposal.cs
  10. 36
      Avalonia.Gif/Decoding/GifColor.cs
  11. 653
      Avalonia.Gif/Decoding/GifDecoder.cs
  12. 20
      Avalonia.Gif/Decoding/GifFrame.cs
  13. 19
      Avalonia.Gif/Decoding/GifHeader.cs
  14. 43
      Avalonia.Gif/Decoding/GifRect.cs
  15. 8
      Avalonia.Gif/Decoding/GifRepeatBehavior.cs
  16. 23
      Avalonia.Gif/Decoding/InvalidGifStreamException.cs
  17. 23
      Avalonia.Gif/Decoding/LzwDecompressionException.cs
  18. 81
      Avalonia.Gif/Extensions/StreamExtensions.cs
  19. 297
      Avalonia.Gif/GifImage.cs
  20. 147
      Avalonia.Gif/GifInstance.cs
  21. 15
      Avalonia.Gif/IGifInstance.cs
  22. 20
      Avalonia.Gif/InvalidGifStreamException.cs
  23. 180
      Avalonia.Gif/WebpInstance.cs
  24. 8
      Build/AppEntitlements.entitlements
  25. 12
      Build/EmbeddedEntitlements.entitlements
  26. 26
      Build/build_macos_app.sh
  27. 62
      Build/codesign_embedded_macos.sh
  28. 37
      Build/codesign_macos.sh
  29. 32
      Build/notarize_macos.sh
  30. 445
      CHANGELOG.md
  31. 5
      Directory.Build.props
  32. 76
      README.md
  33. 13
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  34. 12
      StabilityMatrix.Avalonia/App.axaml
  35. 240
      StabilityMatrix.Avalonia/App.axaml.cs
  36. 28
      StabilityMatrix.Avalonia/Assets.cs
  37. BIN
      StabilityMatrix.Avalonia/Assets/AppIcon.icns
  38. 11
      StabilityMatrix.Avalonia/Assets/hf-packages.json
  39. 14
      StabilityMatrix.Avalonia/Assets/sitecustomize.py
  40. 141
      StabilityMatrix.Avalonia/Collections/SearchCollection.cs
  41. 35
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
  42. 32
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs
  43. 38
      StabilityMatrix.Avalonia/Controls/BetterComboBox.cs
  44. 62
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  45. 208
      StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs
  46. 46
      StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs
  47. 113
      StabilityMatrix.Avalonia/Controls/HyperlinkIconButton.cs
  48. 78
      StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml
  49. 22
      StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml
  50. 9
      StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml
  51. 48
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml
  52. 7
      StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs
  53. 190
      StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
  54. 60
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml
  55. 2
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs
  56. 96
      StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml
  57. 52
      StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs
  58. 8
      StabilityMatrix.Avalonia/Controls/Inference/SamplerCard.axaml
  59. 2
      StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml
  60. 6
      StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
  61. 6
      StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml
  62. 2
      StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml
  63. 6
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  64. 8
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  65. 4
      StabilityMatrix.Avalonia/Controls/StarsRating.axaml
  66. 15
      StabilityMatrix.Avalonia/Controls/StarsRating.axaml.cs
  67. 65
      StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml
  68. 135
      StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml.cs
  69. 20
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs
  70. 42
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs
  71. 135
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs
  72. 248
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs
  73. 565
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs
  74. 18
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs
  75. 36
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs
  76. 76
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs
  77. 156
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs
  78. 49
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs
  79. 21
      StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE
  80. 48
      StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml
  81. 122
      StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
  82. 7
      StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml.cs
  83. 98
      StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
  84. 7
      StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml.cs
  85. 40
      StabilityMatrix.Avalonia/Converters/EnumAttributeConverter.cs
  86. 13
      StabilityMatrix.Avalonia/Converters/EnumAttributeConverters.cs
  87. 37
      StabilityMatrix.Avalonia/Converters/FileUriConverter.cs
  88. 53
      StabilityMatrix.Avalonia/Converters/FuncCommandConverter.cs
  89. 344
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  90. 21
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  91. 22
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  92. 19
      StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs
  93. 49
      StabilityMatrix.Avalonia/DialogHelper.cs
  94. 38
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  95. 10
      StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs
  96. 54
      StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs
  97. 20
      StabilityMatrix.Avalonia/Extensions/NotificationLevelExtensions.cs
  98. 54
      StabilityMatrix.Avalonia/Extensions/NotificationServiceExtensions.cs
  99. 9
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  100. 24
      StabilityMatrix.Avalonia/Helpers/IOCommands.cs
  101. Some files were not shown because too many files have changed in this diff Show More

37
.github/workflows/backport.yml

@ -1,37 +0,0 @@
name: Automatic Backport
on:
pull_request:
types: ["closed", "labeled"]
jobs:
backport:
if: ${{ (github.event.pull_request.merged == true) && (contains(github.event.pull_request.labels.*.name, 'backport-to-main') == true) }}
name: Backport PR
runs-on: ubuntu-latest
steps:
- name: Write json
id: create-json
uses: jsdaniell/create-json@v1.2.3
with:
name: ".backportrc.json"
json: |
{
"targetPRLabels": "backport",
"prTitle": "[{{sourceBranch}} to {{targetBranch}}] backport: {{sourcePullRequest.title}} ({{sourcePullRequest.number}})"
}
- name: Backport Action
uses: sorenlouv/backport-github-action@v9.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
auto_backport_label_prefix: backport-to-
- name: Info log
if: ${{ success() }}
run: cat ~/.backport/backport.info.log
- name: Debug log
if: ${{ failure() }}
run: cat ~/.backport/backport.debug.log

205
.github/workflows/release.yml

@ -43,10 +43,6 @@ on:
- stable
- preview
- development
test-release-artifacts:
type: boolean
description: "[Debug] Test release artifacts?"
default: false
jobs:
release-linux:
@ -76,15 +72,20 @@ jobs:
echo "Using version ${{ github.event.inputs.version }}"
echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
- name: Set up .NET 8
- name: Set up .NET 6 (for PupNet)
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Install PupNet
run: |
sudo apt-get -y install libfuse2
dotnet tool install -g KuiperZone.PupNet
dotnet tool install --framework net6.0 -g KuiperZone.PupNet
- name: Set up .NET 8
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: PupNet Build
env:
@ -171,119 +172,10 @@ jobs:
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-14
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.11.x'
- name: Install dependencies for dmg creation
run: brew install graphicsmagick imagemagick && npm install --global create-dmg
- 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, release-macos ]
needs: [ release-linux, release-windows ]
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.github-release == 'true' }}
runs-on: ubuntu-latest
steps:
@ -302,12 +194,11 @@ jobs:
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build (except macos which is already dmg)
# Zip each build
- 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
@ -318,75 +209,15 @@ 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, release-macos ]
needs: [ release-linux, release-windows ]
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:
@ -402,7 +233,7 @@ jobs:
python-version: '3.11'
- name: Install Python Dependencies
run: pip install stability-matrix-tools>=0.3.0 --upgrade
run: pip install stability-matrix-tools>=0.2.18 --upgrade
- name: Publish Auto-Update Release
env:
@ -415,7 +246,7 @@ jobs:
publish-auto-update-b2:
name: Publish Auto-Update Release (B2)
needs: [ release-linux, release-windows, release-macos ]
needs: [ release-linux, release-windows ]
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:
@ -430,19 +261,18 @@ jobs:
- name: Download Artifacts
uses: actions/download-artifact@v3
# Zip each build (except macos which is already dmg)
# Zip each build
- 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.3.0 --upgrade
run: pip install stability-matrix-tools>=0.2.18 --upgrade
# Check that the zips and CHANGELOG.md are in the current working directory
- name: Check files
@ -455,10 +285,6 @@ 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
@ -477,5 +303,4 @@ 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,4 +397,3 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
.husky/pre-commit

18
Avalonia.Gif/Avalonia.Gif.csproj

@ -1,18 +0,0 @@
<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="$(AvaloniaVersion)" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>
</Project>

10
Avalonia.Gif/BgWorkerCommand.cs

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

12
Avalonia.Gif/BgWorkerState.cs

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

10
Avalonia.Gif/Decoding/BlockTypes.cs

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

8
Avalonia.Gif/Decoding/ExtensionType.cs

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

10
Avalonia.Gif/Decoding/FrameDisposal.cs

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

36
Avalonia.Gif/Decoding/GifColor.cs

@ -1,36 +0,0 @@
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

@ -1,653 +0,0 @@
// 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

@ -1,20 +0,0 @@
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

@ -1,19 +0,0 @@
// 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

@ -1,43 +0,0 @@
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

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

23
Avalonia.Gif/Decoding/InvalidGifStreamException.cs

@ -1,23 +0,0 @@
// 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

@ -1,23 +0,0 @@
// 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

@ -1,81 +0,0 @@
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

@ -1,297 +0,0 @@
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

@ -1,147 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,180 +0,0 @@
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

@ -1,8 +0,0 @@
<?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

@ -1,12 +0,0 @@
<?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

@ -1,26 +0,0 @@
#!/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

@ -1,62 +0,0 @@
#!/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

@ -1,37 +0,0 @@
#!/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

@ -1,32 +0,0 @@
#!/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"

445
CHANGELOG.md

@ -5,451 +5,6 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.10.3
### Changed
- Centered OpenArt browser cards
### Fixed
- Fixed MPS install on macOS for ComfyUI, A1111, SDWebUI Forge, and SDWebUI UX causing torch to be upgraded to dev nightly versions and causing incompatibilities with dependencies.
- Fixed crash when failing to parse Python package details
- Fixed "Auto Scroll to End" not working in some scenarios
- Fixed "Auto Scroll to End" toggle button not scrolling to the end when toggled on
- Fixed/reverted output folder name changes for Automatic1111
- Fixed xformers being uninstalled with every ComfyUI update
- Fixed missing progress text during package installs
## v2.10.2
### Changed
- Updated translations for Spanish and Turkish
### Fixed
- Fixed more crashes when loading invalid connected model info files
- Fixed pip installs not parsing comments properly
- Fixed crash when sending input to a process that isn't running
- Fixed breadcrumb on console page showing incorrect running package name
- Fixed [#576](https://github.com/LykosAI/StabilityMatrix/issues/576) - drag & drop crashes on macOS & Linux
- Fixed [#594](https://github.com/LykosAI/StabilityMatrix/issues/594) - missing thumbnails in Inference model selector
- Fixed [#600](https://github.com/LykosAI/StabilityMatrix/issues/600) - kohya_ss v24+ not launching
- Downgraded Avalonia back to 11.0.9 to fix [#589](https://github.com/LykosAI/StabilityMatrix/issues/589) and possibly other rendering issues
## v2.10.1
### Added
- Added SVD Shared Model & Output Folders for Forge (fixes [#580](https://github.com/LykosAI/StabilityMatrix/issues/580))
### Changed
- Improved error message when logging in with a Lykos account fails due to incorrect email or password
- Model Browser & Workflow Browser now auto-load when first navigating to those pages
- Removed update confirmation dialog, instead showing the new version in the update button tooltip
### Fixed
- Fixed package launch not working when environment variable `SETUPTOOLS_USE_DISTUTILS` is set due to conflict with a default environment variable. User environment variables will now correctly override any default environment variables.
- Fixed "No refresh token found" error when failing to login with Lykos account in some cases
- Fixed blank entries appearing in the Categories dropdown on the Checkpoints page
- Fixed crash when loading invalid connected model info files
- Fixed [#585](https://github.com/LykosAI/StabilityMatrix/issues/585) - Crash when drag & drop source and destination are the same
- Fixed [#584](https://github.com/LykosAI/StabilityMatrix/issues/584) - `--launch-package` argument not working
- Fixed [#581](https://github.com/LykosAI/StabilityMatrix/issues/581) - Inference teaching tip showing more often than it should
- Fixed [#578](https://github.com/LykosAI/StabilityMatrix/issues/578) - "python setup.py egg_info did not run successfully" failure when installing Auto1111 or SDWebUI Forge
- Fixed [#574](https://github.com/LykosAI/StabilityMatrix/issues/574) - local images not showing on macOS or Linux
## v2.10.0
### Added
- Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting.
- Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image.
- Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too.
- Added Layer Diffuse sampler addon to Inference, allows generating foreground with transparency with SD1.5 and SDXL.
- Added support for deep links from the new Stability Matrix Chrome extension
- Added OpenArt.AI workflow browser for ComfyUI workflows
- Added more metadata to the image dialog info flyout
- Added Output Sharing toggle in Advanced Options during install flow
### Changed
- Revamped the Packages page to enable running multiple packages at the same time
- Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector
- Model download location selector now searches all subfolders
- Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option.
- Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading
- Updated translations for French, Spanish, and Turkish
- Changed to a new image control for pages with many images
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Fixed [#559](https://github.com/LykosAI/StabilityMatrix/issues/559) - "Unable to load bitmap from provided data" error in Checkpoints page
- Fixed [#522](https://github.com/LykosAI/StabilityMatrix/issues/522) - Incorrect output directory path for latest Auto1111
- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update
- Fixed Civitai model browser error when sorting by Installed with more than 100 installed models
- Fixed CLIP Install errors due to setuptools distutils conflict, added default environment variable setting `SETUPTOOLS_USE_DISTUTILS=stdlib`
- Fixed progress bars not displaying properly during package installs & updates
- Fixed ComfyUI extension updates not running install.py / updating requirements.txt
- Improved performance when deleting many images from the Outputs page
- Fixed ComfyUI torch downgrading to 2.1.2 when updating
- Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked
- Fixed model download location options for VAEs in the CivitAI Model Browser
### Removed
- Removed the main Launch page, as it is no longer needed with the new Packages page
## v2.10.0-pre.2
### Added
- Added more metadata to the image dialog info flyout
- Added Restart button to console page
### Changed
- Model download location selector now searches all subfolders
### Fixed
- Fixed Civitai model browser not showing images when "Show NSFW" is disabled
- Fixed crash when Installed Workflows page is opened with no Workflows folder
- Fixed progress bars not displaying properly during package installs & updates
- Fixed ComfyUI extension updates not running install.py / updating requirements.txt
## v2.10.0-pre.1
### Added
- Added OpenArt.AI workflow browser for ComfyUI workflows
- Added Output Sharing toggle in Advanced Options during install flow
### Changed
- Changed to a new image control for pages with many images
- Removed Symlink option for InvokeAI due to changes with InvokeAI v4.0+
- Output sharing is now enabled by default for new installations
- (Internal) Updated to Avalonia 11.0.10
### Fixed
- Improved performance when deleting many images from the Outputs page
- Fixed ComfyUI torch downgrading to 2.1.2 when updating
- Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update
- Fixed "Could not find entry point for InvokeAI" error on InvokeAI v4.0+
## v2.10.0-dev.3
### Added
- Added support for deep links from the new Stability Matrix Chrome extension
### Changed
- Due to changes on the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser
- Due to the above API changes, new pages are now loaded via "infinite scrolling"
### Fixed
- Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked
- Fixed model download location options for VAEs in the CivitAI Model Browser
- Fixed crash on startup when library directory is not set
- Fixed One-Click install progress dialog not disappearing after completion
- Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area
- Fixed no packages being available for one-click install on PCs without a GPU
- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page
- Fixed missing ratings on some models in the CivitAI Model Browser
- Fixed missing favorite count in the CivitAI Model Browser
- Fixed recommended models not showing all SDXL models
## v2.10.0-dev.2
### Added
- Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting.
### Changed
- Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option.
- Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading
### Fixed
- Fixed Outputs page not remembering where the user last was in the TreeView in certain circumstances
- Fixed Inference extension upgrades not being added to missing extensions list for prompted install
- Fixed "The Open Web UI button has moved" teaching tip spam
## v2.10.0-dev.1
### Added
- Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image.
- Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too.
### Changed
- Revamped the Packages page to enable running multiple packages at the same time
- Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector
### Removed
- Removed the main Launch page, as it is no longer needed with the new Packages page
## v2.9.3
### Changed
- Removed Symlink option for InvokeAI to prevent InvokeAI from moving models into its own directories (will be replaced with a Config option in a future update)
### Fixed
- Fixed images not appearing in Civitai Model Browser when "Show NSFW" was disabled
- Fixed [#556](https://github.com/LykosAI/StabilityMatrix/issues/556) - "Could not find entry point for InvokeAI" error
## v2.9.2
### Changed
- Due to changes with the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser
- Due to the above API changes, new pages are now loaded via "infinite scrolling"
### Fixed
- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page
- Fixed model download location options for VAEs in the CivitAI Model Browser
- Fixed One-Click install progress dialog not disappearing after completion
- Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area
- Fixed no packages being available for one-click install on PCs without a GPU
## v2.9.1
### Added
- Fixed [#498](https://github.com/LykosAI/StabilityMatrix/issues/498) Added "Pony" category to CivitAI Model Browser
### Changed
- Changed package deletion warning dialog to require additional confirmation
### Fixed
- Fixed [#502](https://github.com/LykosAI/StabilityMatrix/issues/502) - missing launch options for Forge
- Fixed [#500](https://github.com/LykosAI/StabilityMatrix/issues/500) - missing output images in Forge when using output sharing
- Fixed [#490](https://github.com/LykosAI/StabilityMatrix/issues/490) - `mpmath has no attribute 'rational'` error on macOS
- Fixed [#510](https://github.com/ionite34/StabilityMatrix/pull/564/files) - kohya_ss packages with v23.0.x failing to install due to missing 'packaging' dependency
- Fixed incorrect progress text when deleting a checkpoint from the Checkpoints page
- Fixed incorrect icon colors on macOS
## v2.9.0
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
#### Inference
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2)
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Changed
- Updated translations for German, Spanish, French, Japanese, Portuguese, and Turkish
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed [#335](https://github.com/LykosAI/StabilityMatrix/issues/335) Update hanging indefinitely after git step for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
- Fixed unknown model types not showing on checkpoints page (thanks Jerry!)
- Improved error handling for Inference Select Image hash calculation in case file is being written to while being read
## v2.9.0-pre.2
### Added
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
### Changed
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
## v2.9.0-pre.1
### Added
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2)
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed output sharing for Stable Diffusion WebUI Forge
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed default command line args for SDWebUI Forge on macOS
- Fixed output paths and output sharing for SDWebUI Forge
- Maybe fixed update hanging for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
## v2.9.0-dev.2
### Added
#### Inference
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
### Fixed
- Fixed icon sizes of Inference Addons and Steps buttons
## v2.9.0-dev.1
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
## v2.8.4
### Fixed
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
## v2.8.3
### Fixed
- Fixed user tokens read error causing failed downloads
- Failed downloads will now log error messages
- Fixed [#458](https://github.com/LykosAI/StabilityMatrix/issues/458) - Save Intermediate Image not working
- Fixed [#453](https://github.com/LykosAI/StabilityMatrix/issues/453) - Update Fooocus `--output-directory` argument to `--output-path`
## v2.8.2
### Added
- Added missing GFPGAN link to Automatic1111 packages
### Fixed
- Fixed Inference Image to Image Denoise setting becoming hidden after changing schedulers
- Fixed Inference ControlNet models showing as downloadable even when they are already installed
- Fixed Inference Sampler Addon conditioning not applying (i.e. ControlNet)
- Fixed extension modification dialog not showing any progress messages
## v2.8.1
### Fixed
- Fixed model links not working in RuinedFooocus for new installations
- Fixed incorrect nodejs download link on Linux (thanks to slogonomo for the fix)
- Fixed failing InvokeAI install on macOS due to missing nodejs
- Increased timeout on Recommended Models call to prevent potential timeout errors on slow connections
- Fixed SynchronizationLockException when saving settings
- Improved error messages with process output for 7z extraction errors
- Fixed missing tkinter dependency for OneTrainer on Windows
- Fixed auto-update on macOS not starting new version from an issue in starting .app bundles with arguments
- Fixed [#436](https://github.com/LykosAI/StabilityMatrix/issues/436) - Crash on invalid json files during checkpoint indexing
## 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
## v2.7.8
### Changed
- Python Packages install dialog now allows entering multiple arguments or option flags
### Fixed
- Fixed InvokeAI Package dependency versions ([#395](https://github.com/LykosAI/StabilityMatrix/pull/395))
## v2.7.7
### Added
- Added `--use-directml` launch argument for SDWebUI DirectML fork

5
Directory.Build.props

@ -1,5 +0,0 @@
<Project>
<PropertyGroup>
<AvaloniaVersion>11.0.9</AvaloniaVersion>
</PropertyGroup>
</Project>

76
README.md

@ -2,58 +2,53 @@
[![Build](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml/badge.svg)](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml)
[![Discord Server](https://img.shields.io/discord/1115555685476868168?logo=discord&logoColor=white&label=Discord%20Server)](https://discord.com/invite/TUrgfECxHz)
[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release]
[release]: https://github.com/LykosAI/StabilityMatrix/releases/latest
[download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip
[download-linux-appimage-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-linux-x64.zip
[download-linux-aur-x64]: https://aur.archlinux.org/packages/stabilitymatrix
[download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg
[download-linux-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-linux-x64.zip
[download-macos]: https://github.com/LykosAI/StabilityMatrix/issues/45
[auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui
[sdwebui-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml
[webui-ux]: https://github.com/anapnoe/stable-diffusion-webui-ux
[comfy]: https://github.com/comfyanonymous/ComfyUI
[sdnext]: https://github.com/vladmandic/automatic
[voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion
[invokeai]: https://github.com/invoke-ai/InvokeAI
[fooocus]: https://github.com/lllyasviel/Fooocus
[fooocus-mre]: https://github.com/MoonRide303/Fooocus-MRE
[ruined-fooocus]: https://github.com/runew0lf/RuinedFooocus
[fooocus-controlnet]: https://github.com/fenneishi/Fooocus-ControlNet-SDXL
[kohya-ss]: https://github.com/bmaltais/kohya_ss
[onetrainer]: https://github.com/Nerogar/OneTrainer
[forge]: https://github.com/lllyasviel/stable-diffusion-webui-forge
[stable-swarm]: https://github.com/Stability-AI/StableSwarmUI
[civitai]: https://civitai.com/
[huggingface]: https://huggingface.co/
![Header image for Stability Matrix, Multi-Platform Package Manager and Inference UI for Stable Diffusion](https://cdn.lykos.ai/static/sm-banner-rounded.webp)
[![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64]
[![Linux (AppImage)](https://img.shields.io/badge/Linux%20(AppImage)-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-appimage-x64]
[![Arch Linux (AUR)](https://img.shields.io/badge/Arch%20Linux%20(AUR)-1793D1?style=for-the-badge&logo=archlinux&logoColor=white)][download-linux-aur-x64]
[![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64]
Multi-Platform Package Manager and Inference UI for Stable Diffusion
### ✨ New in 2.5 - [Inference](#inference-A-reimagined-built-in-Stable-Diffusion-experience), a built-in interface for Stable Diffusion powered by ComfyUI
### 🖱 One click install and update for Stable Diffusion Web UI Packages
- Supports:
- [Stable Diffusion WebUI Forge][forge], [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext]
- [Fooocus][fooocus], [Fooocus MRE][fooocus-mre], [Fooocus ControlNet SDXL][fooocus-controlnet], [Ruined Fooocus][ruined-fooocus]
- [ComfyUI][comfy]
- [StableSwarmUI][stable-swarm]
- [VoltaML][voltaml]
- [InvokeAI][invokeai]
- [Kohya's GUI][kohya-ss]
- [OneTrainer][onetrainer]
- Manage plugins / extensions for supported packages ([Automatic1111][auto1111], [Comfy UI][comfy], [SD Web UI-UX][webui-ux], and [SD.Next][sdnext])
- Easily install or update Python dependencies for each package
- Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], [Fooocus][fooocus], and [Fooocus MRE][fooocus-mre]
- Embedded Git and Python dependencies, with no need for either to be globally installed
- Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time
### ✨ Inference - A Reimagined Interface for Stable Diffusion, Built-In to Stability Matrix
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install
- Configurable Environment Variables
### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai]
- Automatically imports to the associated model folder depending on the model type
- Downloads relevant metadata files and preview image
![header](https://cdn.lykos.ai/static/sm-banner-rounded.webp)
[![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release]
[![Windows](https://img.shields.io/badge/Windows-%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]
### Inference - A reimagined built-in Stable Diffusion experience
- Powerful auto-completion and syntax highlighting using a formal language grammar
- Workspaces open in tabs that save and load from `.smproj` project files
@ -67,20 +62,13 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/4341cc34-a584-4e9c-bb3b-276009bdae80" alt=""/>
</p>
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install
- Configurable Environment Variables
### Searchable launch options
<p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/75456866-9d95-47c6-8c0a-fdc19443ee02" alt=""/>
</p>
### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface]
- Automatically imports to the associated model folder depending on the model type
- Downloads relevant metadata files and preview image
### Model browser powered by [Civit AI][civitai]
- Downloads new models, automatically uses the appropriate shared model directory
- Pause and resume downloads, even after closing the app
<p align="center">
@ -118,10 +106,6 @@ 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,13 +19,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<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.1" />
<PackageReference Include="NLog" Version="5.2.8" />
<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" />
</ItemGroup>
<ItemGroup>

12
StabilityMatrix.Avalonia/App.axaml

@ -4,8 +4,6 @@
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
Name="Stability Matrix"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -25,11 +23,9 @@
<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"/>
<ResourceInclude Source="Controls/VendorLabs/Themes/BetterAsyncImage.axaml"/>
</ResourceDictionary.MergedDictionaries>
<idcr:ControlRecycling x:Key="ControlRecyclingKey" />
@ -55,8 +51,6 @@
<StyleInclude Source="Styles/ToggleButtonStyles.axaml"/>
<StyleInclude Source="Styles/DockStyles.axaml"/>
<StyleInclude Source="Styles/BorderStyles.axaml"/>
<StyleInclude Source="Styles/TextBoxStyles.axaml"/>
<StyleInclude Source="Styles/CommandBarButtonStyles.axaml"/>
<StyleInclude Source="Controls/AdvancedImageBox.axaml"/>
<StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>
@ -64,9 +58,6 @@
<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/TreeFileExplorer.axaml"/>
<StyleInclude Source="Controls/Inference/StackCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackEditableCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackExpander.axaml"/>
@ -81,9 +72,6 @@
<StyleInclude Source="Controls/Inference/SharpenCard.axaml"/>
<StyleInclude Source="Controls/Inference/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Inference/ControlNetCard.axaml"/>
<StyleInclude Source="Controls/Inference/PromptExpansionCard.axaml"/>
<StyleInclude Source="Controls/Inference/LayerDiffuseCard.axaml"/>
<labs:ControlThemes/>
<Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

240
StabilityMatrix.Avalonia/App.axaml.cs

@ -17,13 +17,11 @@ 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;
@ -40,6 +38,7 @@ using Polly.Extensions.Http;
using Polly.Timeout;
using Refit;
using Sentry;
using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
@ -53,12 +52,14 @@ using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
@ -79,8 +80,7 @@ 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,8 +96,6 @@ 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.
@ -108,8 +106,6 @@ public sealed class App : Application
{
AvaloniaXamlLoader.Load(this);
SetFontFamily(GetPlatformDefaultFontFamily());
// Set design theme
if (Design.IsDesignMode)
{
@ -121,7 +117,8 @@ 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)
@ -164,7 +161,10 @@ public sealed class App : Application
DesktopLifetime.MainWindow = setupWindow;
setupWindow.ShowAsyncCts.Token.Register(() =>
setupWindow
.ShowAsyncCts
.Token
.Register(() =>
{
if (setupWindow.Result == ContentDialogResult.Primary)
{
@ -185,58 +185,6 @@ 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>
@ -275,13 +223,17 @@ 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()
@ -294,10 +246,7 @@ public sealed class App : Application
var settingsManager = Services.GetRequiredService<ISettingsManager>();
if (Program.Args.DataDirectoryOverride is not null)
{
settingsManager.SetLibraryDirOverride(Program.Args.DataDirectoryOverride);
}
settingsManager.LibraryDirOverride = Program.Args.DataDirectoryOverride;
if (settingsManager.TryFindLibrary())
{
@ -320,18 +269,17 @@ public sealed class App : Application
provider.GetRequiredService<IDiscordRichPresenceService>(),
provider.GetRequiredService<ServiceManager<ViewModelBase>>(),
provider.GetRequiredService<ITrackedDownloadService>(),
provider.GetRequiredService<IModelIndexService>(),
provider.GetRequiredService<Lazy<IModelDownloadLinkHandler>>()
provider.GetRequiredService<IModelIndexService>()
)
{
Pages =
{
provider.GetRequiredService<NewPackageManagerViewModel>(),
provider.GetRequiredService<LaunchPageViewModel>(),
provider.GetRequiredService<InferenceViewModel>(),
provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>(),
provider.GetRequiredService<WorkflowsPageViewModel>()
provider.GetRequiredService<OutputsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -349,9 +297,7 @@ 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();
@ -376,7 +322,8 @@ 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();
@ -385,8 +332,7 @@ 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] });
@ -406,12 +352,9 @@ 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)
{
@ -443,9 +386,7 @@ 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())
@ -554,7 +495,7 @@ public sealed class App : Application
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(30);
c.Timeout = TimeSpan.FromSeconds(15);
})
.AddPolicyHandler(retryPolicy);
@ -563,7 +504,7 @@ public sealed class App : Application
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(30);
c.Timeout = TimeSpan.FromSeconds(15);
})
.AddPolicyHandler(retryPolicy);
@ -571,8 +512,8 @@ public sealed class App : Application
.AddRefitClient<ILykosAuthApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://auth.lykos.ai");
c.Timeout = TimeSpan.FromSeconds(60);
c.BaseAddress = new Uri("https://stableauthentication.azurewebsites.net");
c.Timeout = TimeSpan.FromSeconds(15);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
.AddPolicyHandler(retryPolicy)
@ -581,15 +522,6 @@ public sealed class App : Application
new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>())
);
services
.AddRefitClient<IOpenArtApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://openart.ai/api/public/workflows");
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
// Add Refit client managers
services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));
@ -625,11 +557,7 @@ 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());
@ -649,52 +577,42 @@ 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)
{
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);
lifetime.Shutdown(exitCode);
}
}
private static void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
/// <summary>
/// Handle shutdown requests (happens before <see cref="OnExit"/>)
/// </summary>
private static void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
{
Debug.WriteLine("Start OnShutdownRequested");
if (e.Cancel)
return;
var mainWindow = (MainWindow)sender!;
// Show confirmation if package running
var launchPageViewModel = Services.GetRequiredService<LaunchPageViewModel>();
launchPageViewModel.OnMainWindowClosing(e);
if (e.Cancel)
return;
// Check if we need to dispose IAsyncDisposables
if (
isAsyncDisposeComplete
|| Services.GetServices<IAsyncDisposable>().ToList() is not { Count: > 0 } asyncDisposables
!isAsyncDisposeComplete
&& Services.GetServices<IAsyncDisposable>().ToList() is { Count: > 0 } asyncDisposables
)
return;
{
// Cancel shutdown for now
e.Cancel = true;
isAsyncDisposeComplete = true;
Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
Dispatcher
.UIThread.InvokeAsync(async () =>
Task.Run(async () =>
{
foreach (var disposable in asyncDisposables)
{
@ -712,16 +630,38 @@ public sealed class App : Application
.ContinueWith(_ =>
{
// Shutdown again
Debug.WriteLine("Finished disposing IAsyncDisposables, shutting down");
if (Dispatcher.UIThread.SupportsRunLoops)
{
Dispatcher.UIThread.Invoke(() => Shutdown());
}
Environment.Exit(0);
})
.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
);
}
private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args)
@ -774,12 +714,10 @@ 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
}
@ -792,9 +730,7 @@ 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
@ -815,7 +751,9 @@ public sealed class App : Application
// Sentry
if (SentrySdk.IsEnabled)
{
LogManager.Configuration.AddSentry(o =>
LogManager
.Configuration
.AddSentry(o =>
{
o.InitializeSdk = false;
o.Layout = "${message}";
@ -865,14 +803,12 @@ public sealed class App : Application
results.Add(ms);
}
Dispatcher.UIThread.InvokeAsync(async () =>
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
var dest = await StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions()
{
SuggestedFileName = "screenshot.png",
ShowOverwritePrompt = true
}
new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true }
);
if (dest?.TryGetLocalPath() is { } localPath)

28
StabilityMatrix.Avalonia/Assets.cs

@ -11,19 +11,16 @@ 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");
@ -31,8 +28,7 @@ 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
@ -48,10 +44,7 @@ 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)
@ -119,9 +112,10 @@ internal static class Assets
PlatformKind.MacOS | PlatformKind.Arm,
new RemoteResource
{
// Requires our distribution with signed dylib for gatekeeper
Url = new Uri("https://cdn.lykos.ai/cpython-3.10.11-macos-arm64.zip"),
HashSha256 = "83c00486e0af9c460604a425e519d58e4b9604fbe7a4448efda0f648f86fb6e3"
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"
}
)
);
@ -154,9 +148,7 @@ 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.

11
StabilityMatrix.Avalonia/Assets/hf-packages.json

@ -58,17 +58,6 @@
"LicenseType": "Stable-Video-Diffusion-NC-Community",
"LicensePath": "LICENSE"
},
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Cascade",
"RepositoryPath": "stabilityai/stable-cascade",
"Files": [
"comfyui_checkpoints/stable_cascade_stage_b.safetensors",
"comfyui_checkpoints/stable_cascade_stage_c.safetensors"
],
"LicenseType": "stable-cascade-nc-community",
"LicensePath": "LICENSE"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Canny",

14
StabilityMatrix.Avalonia/Assets/sitecustomize.py

@ -69,20 +69,6 @@ def _patch_rich_console():
except Exception as e:
print("[sitecustomize error]:", e)
try:
from pip._vendor.rich import console
class _Console(console.Console):
@property
def is_terminal(self) -> bool:
return True
console.Console = _Console
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_rich_console()
# Patch tqdm to use stdout instead of stderr

141
StabilityMatrix.Avalonia/Collections/SearchCollection.cs

@ -1,141 +0,0 @@
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; }
}
}

35
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml

@ -8,53 +8,30 @@
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"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="models:ImageSource"
mc:Ignorable="d">
<Grid>
<!-- 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
Name="ImageBox"
CornerRadius="4"
Image="{Binding BitmapAsync^}"
SizeMode="Fit">
<controls:AdvancedImageBox.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
Command="{x:Static controls:AdvancedImageBoxView.FlyoutCopyCommand}"
CommandParameter="{Binding}"
x:Name="CopyMenuItem"
CommandParameter="{Binding #ImageBox.Image}"
HotKey="Ctrl+C"
IconSource="Copy"
Text="{x:Static lang:Resources.Action_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

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

@ -1,9 +1,10 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Extensions;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia.Controls;
@ -15,26 +16,25 @@ public partial class AdvancedImageBoxView : UserControl
InitializeComponent();
}
public static AsyncRelayCommand<ImageSource?> FlyoutCopyCommand { get; } = new(FlyoutCopy);
private static async Task FlyoutCopy(ImageSource? imageSource)
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (imageSource is null)
return;
base.OnApplyTemplate(e);
var copyMenuItem = this.FindControl<MenuFlyoutItem>("CopyMenuItem")!;
copyMenuItem.Command = new AsyncRelayCommand<Bitmap?>(FlyoutCopy);
}
if (Compat.IsWindows && imageSource.Bitmap is { } bitmap)
private static async Task FlyoutCopy(Bitmap? image)
{
// Use bitmap on Windows if available
if (image is null || !Compat.IsWindows) return;
await Task.Run(() =>
{
WindowsClipboard.SetBitmap(bitmap);
});
}
else if (imageSource.LocalFile is { } imagePath)
if (Compat.IsWindows)
{
// Other OS or no bitmap, use image source
var clipboard = App.Clipboard;
await clipboard.SetFileDataObjectAsync(imagePath);
WindowsClipboard.SetBitmap(image);
}
});
}
}

38
StabilityMatrix.Avalonia/Controls/BetterComboBox.cs

@ -1,38 +0,0 @@
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;
}
}
}
}

62
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -122,10 +122,8 @@ 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
{
@ -133,10 +131,8 @@ 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
{
@ -144,21 +140,8 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogWidthProperty, value);
}
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 static readonly StyledProperty<double> MaxDialogHeightProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogHeight");
public double MaxDialogHeight
{
@ -166,10 +149,8 @@ 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
{
@ -177,10 +158,8 @@ 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)
@ -208,18 +187,13 @@ public class BetterContentDialog : ContentDialog
var point = e.GetPosition(this);
if (!backgroundPart.Bounds.Contains(point))
{
// Use vm if available
if ((Content as Control)?.DataContext is ContentDialogViewModelBase vm)
if (
!backgroundPart.Bounds.Contains(point)
&& (Content as Control)?.DataContext is ContentDialogViewModelBase vm
)
{
vm.OnCloseButtonClick();
}
else
{
Hide(ContentDialogResult.None);
}
}
}
}
@ -237,7 +211,10 @@ 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;
@ -257,7 +234,8 @@ public class BetterContentDialog : ContentDialog
}
else
{
PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
PrimaryButton.IsVisible =
IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
}
}

208
StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs

@ -1,208 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Xaml.Interactions.DragAndDrop;
using Avalonia.Xaml.Interactivity;
namespace StabilityMatrix.Avalonia.Controls;
public class BetterContextDragBehavior : Behavior<Control>
{
private Point _dragStartPoint;
private PointerEventArgs? _triggerEvent;
private bool _lock;
private bool _captured;
public static readonly StyledProperty<object?> ContextProperty = AvaloniaProperty.Register<
ContextDragBehavior,
object?
>(nameof(Context));
public static readonly StyledProperty<IDragHandler?> HandlerProperty = AvaloniaProperty.Register<
ContextDragBehavior,
IDragHandler?
>(nameof(Handler));
public static readonly StyledProperty<double> HorizontalDragThresholdProperty = AvaloniaProperty.Register<
ContextDragBehavior,
double
>(nameof(HorizontalDragThreshold), 3);
public static readonly StyledProperty<double> VerticalDragThresholdProperty = AvaloniaProperty.Register<
ContextDragBehavior,
double
>(nameof(VerticalDragThreshold), 3);
public static readonly StyledProperty<string> DataFormatProperty = AvaloniaProperty.Register<
BetterContextDragBehavior,
string
>("DataFormat");
public string DataFormat
{
get => GetValue(DataFormatProperty);
set => SetValue(DataFormatProperty, value);
}
public object? Context
{
get => GetValue(ContextProperty);
set => SetValue(ContextProperty, value);
}
public IDragHandler? Handler
{
get => GetValue(HandlerProperty);
set => SetValue(HandlerProperty, value);
}
public double HorizontalDragThreshold
{
get => GetValue(HorizontalDragThresholdProperty);
set => SetValue(HorizontalDragThresholdProperty, value);
}
public double VerticalDragThreshold
{
get => GetValue(VerticalDragThresholdProperty);
set => SetValue(VerticalDragThresholdProperty, value);
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree()
{
AssociatedObject?.AddHandler(
InputElement.PointerPressedEvent,
AssociatedObject_PointerPressed,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerReleasedEvent,
AssociatedObject_PointerReleased,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerMovedEvent,
AssociatedObject_PointerMoved,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
AssociatedObject?.AddHandler(
InputElement.PointerCaptureLostEvent,
AssociatedObject_CaptureLost,
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble
);
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree()
{
AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed);
AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased);
AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved);
AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost);
}
private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value)
{
var data = new DataObject();
data.Set(DataFormat, value!);
var effect = DragDropEffects.None;
if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt))
{
effect |= DragDropEffects.Link;
}
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
effect |= DragDropEffects.Move;
}
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control))
{
effect |= DragDropEffects.Copy;
}
else
{
effect |= DragDropEffects.Move;
}
await DragDrop.DoDragDrop(triggerEvent, data, effect);
}
private void Released()
{
_triggerEvent = null;
_lock = false;
}
private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (properties.IsLeftButtonPressed)
{
if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext)
{
_dragStartPoint = e.GetPosition(null);
_triggerEvent = e;
_lock = true;
_captured = true;
}
}
}
private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_captured)
{
if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { })
{
Released();
}
_captured = false;
}
}
private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { })
{
var point = e.GetPosition(null);
var diff = _dragStartPoint - point;
var horizontalDragThreshold = HorizontalDragThreshold;
var verticalDragThreshold = VerticalDragThreshold;
if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold)
{
if (_lock)
{
_lock = false;
}
else
{
return;
}
var context = Context ?? AssociatedObject?.DataContext;
Handler?.BeforeDragDrop(sender, _triggerEvent, context);
await DoDragDrop(_triggerEvent, context);
Handler?.AfterDragDrop(sender, _triggerEvent, context);
_triggerEvent = null;
}
}
}
private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
Released();
_captured = false;
}
}

46
StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs

@ -1,46 +0,0 @@
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,122 +1,13 @@
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 : Button
public class HyperlinkIconButton : HyperlinkButton
{
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";
}
protected override Type StyleKeyOverride => typeof(HyperlinkIconButton);
}

78
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.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
@ -36,46 +36,29 @@
<sg:SpacedGrid
ColumnDefinitions="0.34*,0.65*"
ColumnSpacing="8"
RowDefinitions="Auto,Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto"
RowSpacing="4">
<!-- Image Select -->
<controls:SelectImageCard
Grid.RowSpan="3"
Grid.RowSpan="2"
Padding="6"
VerticalAlignment="Stretch"
DataContext="{Binding SelectImageCardViewModel}"
FontSize="13" />
<!-- Preprocessor Model -->
<sg:SpacedGrid
<!-- TODO: Preprocessor Model -->
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
ColumnDefinitions="*,Auto">
<ui:FAComboBox
Margin="0,0,0,4"
SelectedItem="{Binding SelectedPreprocessor}"
ItemsSource="{Binding ClientManager.Preprocessors}"
DisplayMemberBinding="{Binding DisplayName}"
HorizontalAlignment="Stretch"
Header="{x:Static lang:Resources.Label_Preprocessor}"/>
<Button
ToolTip.Tip="{x:Static lang:Resources.Action_PreviewPreprocessor}"
Padding="7"
VerticalAlignment="Center"
Margin="0,23,0,0"
Command="{Binding PreviewPreprocessorCommand}"
CommandParameter="{Binding SelectedPreprocessor}"
Grid.Column="1">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="15"
IsFilled="True"
Symbol="Play" />
</Button>
</sg:SpacedGrid>
markupExtensions:ShowDisabledTooltipExtension.ShowOnDisabled="True"
Header="{x:Static lang:Resources.Label_Preprocessor}"
IsEnabled="False"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
ToolTip.ShowDelay="1200"
ToolTip.Tip="{StaticResource WipFeatureToolTip}" />
<!-- ControlNet Model -->
<ui:FAComboBox
@ -119,45 +102,6 @@
</ui:FAComboBox>
<!-- Dimensions -->
<Grid
Grid.Row="2"
Grid.Column="1"
Margin="0,4,0,0"
ColumnDefinitions="*,Auto,*"
RowDefinitions="Auto,*">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,4,4"
Text="{x:Static lang:Resources.Label_Width}"/>
<ui:NumberBox
Grid.Row="1"
Grid.Column="0"
Margin="0,0,4,0"
PlaceholderText="128"
SmallChange="128"
Value="{Binding Width}"
ValidationMode="InvalidInputOverwritten"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Compact"/>
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="4,0,0,4"
Text="{x:Static lang:Resources.Label_Height}"/>
<ui:NumberBox
Grid.Row="1"
Grid.Column="2"
Margin="4,0,0,0"
PlaceholderText="128"
SmallChange="128"
Value="{Binding Height}"
ValidationMode="InvalidInputOverwritten"
HorizontalAlignment="Stretch"
SpinButtonPlacementMode="Compact"/>
</Grid>
</sg:SpacedGrid>
<sg:SpacedGrid
ColumnDefinitions="*,Auto,Auto"

22
StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml

@ -9,9 +9,6 @@
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith>
@ -22,8 +19,6 @@
</Panel>
</Design.PreviewWith>
<converters:FileUriConverter x:Key="FileUriConverter" />
<ControlTheme x:Key="{x:Type controls:ImageFolderCard}" TargetType="controls:ImageFolderCard">
<Setter Property="Background" Value="Transparent" />
<!-- Set Defaults -->
@ -48,12 +43,18 @@
<Grid RowDefinitions="Auto,*">
<TextBox
x:Name="SearchBox"
Classes="search"
Margin="6,4,6,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Text="{Binding SearchQuery}"
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}"/>
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}">
<TextBox.InnerRightContent>
<ui:SymbolIcon
Margin="0,0,6,0"
FontSize="16"
Symbol="Find" />
</TextBox.InnerRightContent>
</TextBox>
<scroll:BetterScrollViewer
Grid.Row="1"
@ -200,11 +201,12 @@
<Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto">
<vendorLabs:BetterAsyncImage
<controls:BetterAdvancedImage
Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Height="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Height}"
Source="{Binding AbsolutePath, Converter={StaticResource FileUriConverter}}"
Stretch="UniformToFill" />
Source="{Binding AbsolutePath}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Border
Grid.Row="1"

9
StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml

@ -5,8 +5,6 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"
x:DataType="vmInference:ImageGalleryCardViewModel">
<Design.PreviewWith>
@ -117,11 +115,12 @@
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:ImageSource}">
<vendorLabs:BetterAsyncImage
<controls:BetterAdvancedImage
MaxHeight="65"
CornerRadius="4"
Source="{Binding Uri}"
Stretch="Uniform" />
Source="{Binding}"
Stretch="Uniform"
StretchDirection="Both" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

48
StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml

@ -1,48 +0,0 @@
<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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
x:DataType="vmInference:LayerDiffuseCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:LayerDiffuseCard DataContext="{x:Static mocks:DesignData.LayerDiffuseCardViewModel}"/>
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|LayerDiffuseCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Mode Selection -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="Mode"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding Converter={x:Static converters:EnumAttributeConverters.DisplayName}}"
ItemsSource="{Binding AvailableModes}"
SelectedItem="{Binding SelectedMode}" />
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

7
StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs

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

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

@ -1,93 +1,146 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace: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"
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>
<Styles.Resources>
<fluent:SymbolIconSource
x:Key="IconCube"
IsFilled="False"
Symbol="Cube" />
<fluent:SymbolIconSource
x:Key="IconQuestionCircle"
IsFilled="True"
Symbol="QuestionCircle" />
<fluent:SymbolIconSource
x:Key="IconTableCellEdit"
IsFilled="True"
Symbol="TableCellEdit" />
</Styles.Resources>
<Style Selector="controls|ModelCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<!-- Model -->
<TextBlock
Grid.Column="0"
MinWidth="60"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<controls:BetterComboBox
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
Padding="8,6,4,6"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedModel}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />
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>
<Button
Grid.Row="0"
Grid.Column="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
Margin="8,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconCube}"
IsChecked="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconCube}"
IsChecked="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconTableCellEdit}"
IsChecked="{Binding IsClipSkipEnabled}"
Text="{x:Static lang:Resources.Label_CLIPSkip}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{Binding ConfigClickCommand}"
IconSource="{StaticResource IconQuestionCircle}"
Text="{x:Static lang:Resources.Label_Config}" />
<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:FAMenuFlyout>
</Button.Flyout>
</Button>
@ -96,28 +149,30 @@
<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" />
<controls:BetterComboBox
<ui:FAComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
Padding="8,6,4,6"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
IsVisible="{Binding IsRefinerSelectionEnabled}"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedRefiner}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />
SelectedItem="{Binding SelectedRefiner}" />
<!-- VAE -->
<TextBlock
Grid.Row="2"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsVaeSelectionEnabled}"
@ -136,32 +191,7 @@
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"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
ClipValueToMinMax="True"
Increment="1"
IsVisible="{Binding IsClipSkipEnabled}"
Maximum="24"
Minimum="1"
ParsingNumberStyle="Integer"
Watermark="1"
Value="{Binding ClipSkip, Converter={x:Static converters:NullableDefaultNumericConverters.IntToDecimal}}" />
</sg:SpacedGrid>
</Grid>
</controls:Card>
</ControlTemplate>
</Setter>

60
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml

@ -8,8 +8,6 @@
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
x:DataType="vmInference:PromptCardViewModel">
<Design.PreviewWith>
<Grid Width="460" Height="600">
@ -35,7 +33,7 @@
</Style>
</controls:Card.Styles>
<Grid RowDefinitions="*,16,*,16,Auto">
<Grid RowDefinitions="*,16,*">
<!-- Prompt -->
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<StackPanel
@ -58,9 +56,7 @@
HorizontalAlignment="Right"
Orientation="Horizontal">
<Panel>
<Button
x:Name="PART_HelpButton"
Margin="0,-2,0,0"
Padding="10,4"
VerticalAlignment="Top"
@ -69,12 +65,6 @@
Classes="transparent-full"
Command="{Binding ShowHelpDialogCommand}" />
<ui:TeachingTip
Target="{Binding #PART_HelpButton}"
Title="{x:Static lang:Resources.TeachingTip_InferencePromptHelpButton}"
IsOpen="{Binding IsHelpButtonTeachingTipOpen, Mode=TwoWay}"/>
</Panel>
<Button
Padding="8,4"
Command="{Binding DebugShowTokensCommand}"
@ -147,54 +137,6 @@
</Border>
</Grid>
<GridSplitter
Grid.Row="3"
MaxWidth="45"
VerticalAlignment="Center"
BorderThickness="1"
CornerRadius="4"
Opacity="0.3" />
<controls:StackEditableCard
Margin="2,0,0,0"
DataContext="{Binding ModulesCardViewModel}"
Grid.Row="4">
</controls:StackEditableCard>
<!-- Styles and Prompt Expansions -->
<!--<Grid Grid.Row="4" RowDefinitions="Auto,*">
<StackPanel Margin="4,0,4,8" Orientation="Horizontal">
<TextBlock FontSize="14" Text="Styles" />
<icons:Icon
Margin="8,0"
FontSize="10"
Value="fa-solid fa-caret-down" />
</StackPanel>
<Border
Grid.Row="1"
Classes="theme-dark"
VerticalAlignment="Stretch"
CornerRadius="4">
<avaloniaEdit:TextEditor
x:Name="ExtraPromptEditor"
Document="{Binding NegativePromptDocument}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace">
<i:Interaction.Behaviors>
<behaviors:TextEditorCompletionBehavior
CompletionProvider="{Binding CompletionProvider}"
IsEnabled="{Binding IsAutoCompletionEnabled}"
TokenizerProvider="{Binding TokenizerProvider}" />
<behaviors:TextEditorToolTipBehavior IsEnabled="False" TokenizerProvider="{Binding TokenizerProvider}" />
</i:Interaction.Behaviors>
</avaloniaEdit:TextEditor>
</Border>
</Grid>-->
</Grid>
</controls:Card>
</ControlTemplate>

2
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs

@ -12,7 +12,7 @@ using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptCard : TemplatedControlBase
public class PromptCard : TemplatedControl
{
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

96
StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml

@ -1,96 +0,0 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
x:DataType="inference:PromptExpansionCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:PromptExpansionCard />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|PromptExpansionCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Model -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
x:Name="PART_ModelComboBox"
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
ItemContainerTheme="{StaticResource FAComboBoxItemHybridModelTheme}"
ItemsSource="{Binding ClientManager.PromptExpansionModels}"
SelectedItem="{Binding SelectedModel}">
<ui:FAComboBox.Resources>
<input:StandardUICommand x:Key="RemoteDownloadCommand"
Command="{Binding RemoteDownloadCommand}" />
</ui:FAComboBox.Resources>
<ui:FAComboBox.DataTemplates>
<controls: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.None}" DataType="models:HybridModelFile">
<TextBlock Text="{Binding ShortDisplayName}" />
</DataTemplate>
</controls:HybridModelTemplateSelector>
</ui:FAComboBox.DataTemplates>
</ui:FAComboBox>
<!--<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Padding="8,6,4,6"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Upscalers}"
SelectedItem="{Binding SelectedModel}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />-->
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

52
StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs

@ -1,52 +0,0 @@
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptExpansionCard : TemplatedControl
{
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var upscalerComboBox = e.NameScope.Find("PART_ModelComboBox") as FAComboBox;
upscalerComboBox!.SelectionChanged += UpscalerComboBox_OnSelectionChanged;
}
private void UpscalerComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
var item = e.AddedItems[0];
if (item is HybridModelFile { IsDownloadable: true })
{
// Reset the selection
e.Handled = true;
if (
e.RemovedItems.Count > 0
&& e.RemovedItems[0] is HybridModelFile { IsDownloadable: false } removedItem
)
{
(sender as FAComboBox)!.SelectedItem = removedItem;
}
else
{
(sender as FAComboBox)!.SelectedItem = null;
}
// Show dialog to download the model
(DataContext as PromptExpansionCardViewModel)!
.RemoteDownloadCommand.ExecuteAsync(item)
.SafeFireAndForget();
}
}
}

8
StabilityMatrix.Avalonia/Controls/Inference/SamplerCard.axaml

@ -123,13 +123,7 @@
<StackPanel>
<!-- Denoise Strength -->
<StackPanel>
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.Or}">
<Binding Path="IsDenoiseStrengthEnabled"/>
<Binding Path="IsDenoiseStrengthTempEnabled"/>
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel IsVisible="{Binding IsDenoiseStrengthEnabled}">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
VerticalAlignment="Center"

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.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"

6
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.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
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"
@ -39,7 +39,7 @@
Padding="12,4"
IsEnabled="{Binding !!Cards.Count}"
IsChecked="{Binding IsEditEnabled}">
<fluentIcons:SymbolIcon Symbol="Edit" FontSize="14" />
<fluentIcons:SymbolIcon Symbol="Edit" />
</ToggleButton>
<!-- Add button -->
<Button Grid.Column="3"
@ -47,7 +47,7 @@
VerticalAlignment="Bottom"
CornerRadius="4"
Padding="12,4" >
<fluentIcons:SymbolIcon Symbol="Add" IsFilled="True" FontSize="14" />
<fluentIcons:SymbolIcon Symbol="Add" IsFilled="True" />
</Button>
</sg:SpacedGrid>

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}">
<fluent:SymbolIcon FontSize="17" Symbol="Settings" />
<fluentIcons:SymbolIcon FontSize="17" Symbol="Settings" />
</Button>
<!-- Delete button for StackEditableCard -->
<Button
@ -76,7 +76,7 @@
Classes="transparent-red"
Command="{Binding RemoveFromParentListCommand}"
IsVisible="{Binding IsEditEnabled}">
<fluent:SymbolIcon FontSize="16" Symbol="Delete" />
<fluentIcons: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.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
x:DataType="vmInference:UpscalerCardViewModel">
<Design.PreviewWith>
<Grid MinWidth="300">

6
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml

@ -1,9 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:controls1="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
@ -40,7 +38,7 @@
CornerRadius="12"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}">
<vendorLabs:BetterAsyncImage
<controls:BetterAdvancedImage
Stretch="UniformToFill"
CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}"

8
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs

@ -11,10 +11,8 @@ public class SelectableImageButton : Button
public static readonly StyledProperty<bool?> IsSelectedProperty =
ToggleButton.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
SelectableImageButton,
Uri?
>("Source");
public static readonly StyledProperty<string?> SourceProperty =
AdvancedImage.SourceProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<double> ImageWidthProperty = AvaloniaProperty.Register<
SelectableImageButton,
@ -50,7 +48,7 @@ public class SelectableImageButton : Button
set => SetValue(IsSelectedProperty, value);
}
public Uri? Source
public string? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);

4
StabilityMatrix.Avalonia/Controls/StarsRating.axaml

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

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

@ -3,12 +3,17 @@ 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;
@ -31,10 +36,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
{

65
StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml

@ -1,65 +0,0 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:system="using:System"
xmlns:treeFileExplorer="clr-namespace:StabilityMatrix.Avalonia.Models.TreeFileExplorer"
xmlns:mock="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent">
<Design.PreviewWith>
<StackPanel Spacing="16">
<Panel Height="300" Margin="4">
<controls:TreeFileExplorer
RootPath="{x:Static mock:DesignData.CurrentDirectory}" />
</Panel>
<Panel Height="300" Margin="4">
<controls:TreeFileExplorer
IndexFiles="False"
CanSelectFiles="False"
RootPath="{x:Static mock:DesignData.CurrentDirectory}" />
</Panel>
</StackPanel>
</Design.PreviewWith>
<Style Selector="controls|TreeFileExplorer">
<!-- Set Defaults -->
<Setter Property="Template">
<ControlTemplate>
<Grid>
<TreeView
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=RootItem.Children}">
<TreeView.DataTemplates>
<DataTemplate
DataType="treeFileExplorer:TreeFileExplorerFile">
<sg:SpacedGrid ColumnDefinitions="Auto,*" RowSpacing="0" ColumnSpacing="4">
<fluentIcons:SymbolIcon
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Symbol="Document" />
<TextBlock
Grid.Column="1"
Text="{Binding Path.Name}" />
</sg:SpacedGrid>
</DataTemplate>
<TreeDataTemplate
DataType="treeFileExplorer:TreeFileExplorerDirectory"
ItemsSource="{Binding Children}">
<sg:SpacedGrid ColumnDefinitions="Auto,*" RowSpacing="0" ColumnSpacing="4">
<fluentIcons:SymbolIcon
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
IsFilled="True"
Symbol="Folder" />
<TextBlock
Grid.Column="1"
Text="{Binding Path.Name}" />
</sg:SpacedGrid>
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

135
StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml.cs

@ -1,135 +0,0 @@
using Avalonia;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Avalonia.Models.TreeFileExplorer;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Controls;
public class TreeFileExplorer : TemplatedControl
{
public static readonly StyledProperty<TreeFileExplorerDirectory?> RootItemProperty =
AvaloniaProperty.Register<TreeFileExplorer, TreeFileExplorerDirectory?>("RootItem");
public TreeFileExplorerDirectory? RootItem
{
get => GetValue(RootItemProperty);
set => SetValue(RootItemProperty, value);
}
public static readonly StyledProperty<string?> RootPathProperty = AvaloniaProperty.Register<
TreeFileExplorer,
string?
>("RootPath");
public string? RootPath
{
get => GetValue(RootPathProperty);
set => SetValue(RootPathProperty, value);
}
public static readonly StyledProperty<IPathObject?> SelectedPathProperty = AvaloniaProperty.Register<
TreeFileExplorer,
IPathObject?
>("SelectedPath");
public IPathObject? SelectedPath
{
get => GetValue(SelectedPathProperty);
set => SetValue(SelectedPathProperty, value);
}
public static readonly StyledProperty<bool> CanSelectFilesProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("CanSelectFiles", true);
public bool CanSelectFiles
{
get => GetValue(CanSelectFilesProperty);
set => SetValue(CanSelectFilesProperty, value);
}
public static readonly StyledProperty<bool> CanSelectFoldersProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("CanSelectFolders", true);
public bool CanSelectFolders
{
get => GetValue(CanSelectFoldersProperty);
set => SetValue(CanSelectFoldersProperty, value);
}
public static readonly StyledProperty<bool> IndexFilesProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("IndexFiles", true);
public bool IndexFiles
{
get => GetValue(IndexFilesProperty);
set => SetValue(IndexFilesProperty, value);
}
public static readonly StyledProperty<bool> IndexFoldersProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("IndexFolders", true);
public bool IndexFolders
{
get => GetValue(IndexFoldersProperty);
set => SetValue(IndexFoldersProperty, value);
}
private TreeFileExplorerOptions GetOptions()
{
var options = TreeFileExplorerOptions.None;
if (CanSelectFiles)
{
options |= TreeFileExplorerOptions.CanSelectFiles;
}
if (CanSelectFolders)
{
options |= TreeFileExplorerOptions.CanSelectFolders;
}
if (IndexFiles)
{
options |= TreeFileExplorerOptions.IndexFiles;
}
if (IndexFolders)
{
options |= TreeFileExplorerOptions.IndexFolders;
}
return options;
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (RootItem is null)
{
RootItem = RootPath is null
? null
: new TreeFileExplorerDirectory(new DirectoryPath(RootPath), GetOptions());
}
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RootPathProperty)
{
var path = change.GetNewValue<string?>();
RootItem = path is null
? null
: new TreeFileExplorerDirectory(new DirectoryPath(path), GetOptions());
}
}
}

20
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs

@ -1,20 +0,0 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
public class AsyncImageFailedEventArgs : RoutedEventArgs
{
internal AsyncImageFailedEventArgs(Exception? errorException = null, string errorMessage = "")
: base(FailedEvent)
{
ErrorException = errorException;
ErrorMessage = errorMessage;
}
public Exception? ErrorException { get; private set; }
public string ErrorMessage { get; private set; }
}
}

42
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs

@ -1,42 +0,0 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Deines the <see cref="Opened"/> event
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent = RoutedEvent.Register<
BetterAsyncImage,
RoutedEventArgs
>(nameof(Opened), RoutingStrategies.Bubble);
/// <summary>
/// Deines the <see cref="Failed"/> event
/// </summary>
public static readonly RoutedEvent<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs> FailedEvent =
RoutedEvent.Register<
BetterAsyncImage,
global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs
>(nameof(Failed), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when the image is successfully loaded.
/// </summary>
public event EventHandler<RoutedEventArgs>? Opened
{
add => AddHandler(OpenedEvent, value);
remove => RemoveHandler(OpenedEvent, value);
}
/// <summary>
/// Occurs when the image fails to load the uri provided.
/// </summary>
public event EventHandler<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs>? Failed
{
add => AddHandler(FailedEvent, value);
remove => RemoveHandler(FailedEvent, value);
}
}

135
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs

@ -1,135 +0,0 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Labs.Controls;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Defines the <see cref="PlaceholderSource"/> property.
/// </summary>
public static readonly StyledProperty<IImage?> PlaceholderSourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
IImage?
>(nameof(PlaceholderSource));
/// <summary>
/// Defines the <see cref="Source"/> property.
/// </summary>
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Uri?
>(nameof(Source));
/// <summary>
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(Stretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="PlaceholderStretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> PlaceholderStretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(PlaceholderStretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="State"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, AsyncImageState> StateProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, AsyncImageState>(
nameof(State),
o => o.State,
(o, v) => o.State = v
);
/// <summary>
/// Defines the <see cref="ImageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> ImageTransitionProperty =
AvaloniaProperty.Register<BetterAsyncImage, IPageTransition?>(
nameof(ImageTransition),
new CrossFade(TimeSpan.FromSeconds(0.25))
);
/// <summary>
/// Defines the <see cref="IsCacheEnabled"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, bool> IsCacheEnabledProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, bool>(
nameof(IsCacheEnabled),
o => o.IsCacheEnabled,
(o, v) => o.IsCacheEnabled = v
);
private bool _isCacheEnabled;
/// <summary>
/// Gets or sets the placeholder image.
/// </summary>
public IImage? PlaceholderSource
{
get => GetValue(PlaceholderSourceProperty);
set => SetValue(PlaceholderSourceProperty, value);
}
/// <summary>
/// Gets or sets the uri pointing to the image resource
/// </summary>
public Uri? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets a value controlling how the image will be stretched.
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets or sets a value controlling how the placeholder will be stretched.
/// </summary>
public Stretch PlaceholderStretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets the current loading state of the image.
/// </summary>
public AsyncImageState State
{
get => _state;
private set => SetAndRaise(StateProperty, ref _state, value);
}
/// <summary>
/// Gets or sets the transition to run when the image is loaded.
/// </summary>
public IPageTransition? ImageTransition
{
get => GetValue(ImageTransitionProperty);
set => SetValue(ImageTransitionProperty, value);
}
/// <summary>
/// Gets or sets whether to use cache for retrieved images
/// </summary>
public bool IsCacheEnabled
{
get => _isCacheEnabled;
set => SetAndRaise(IsCacheEnabledProperty, ref _isCacheEnabled, value);
}
}

248
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs

@ -1,248 +0,0 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Labs.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
/// <summary>
/// An image control that asynchronously retrieves an image using a <see cref="Uri"/>.
/// </summary>
[TemplatePart("PART_Image", typeof(Image))]
[TemplatePart("PART_PlaceholderImage", typeof(Image))]
public partial class BetterAsyncImage : TemplatedControl
{
protected Image? ImagePart { get; private set; }
protected Image? PlaceholderPart { get; private set; }
private bool _isInitialized;
private CancellationTokenSource? _tokenSource;
private AsyncImageState _state;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
ImagePart = e.NameScope.Get<Image>("PART_Image");
PlaceholderPart = e.NameScope.Get<Image>("PART_PlaceholderImage");
_tokenSource = new CancellationTokenSource();
_isInitialized = true;
if (Source != null)
{
SetSource(Source);
}
}
private async void SetSource(object? source)
{
if (!_isInitialized)
{
return;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
AttachSource(null);
if (source == null)
{
return;
}
State = AsyncImageState.Loading;
if (Source is IImage image)
{
AttachSource(image);
return;
}
if (Source == null)
{
return;
}
var uri = Source;
if (uri != null && uri.IsAbsoluteUri)
{
if (uri.Scheme == "http" || uri.Scheme == "https")
{
Bitmap? bitmap = null;
// Android doesn't allow network requests on the main thread, even though we are using async apis.
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroid())
{
await Task.Run(async () =>
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
});
}
else
#endif
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
}
AttachSource(bitmap);
}
else if (uri.Scheme == "avares")
{
try
{
AttachSource(new Bitmap(AssetLoader.Open(uri)));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else if (uri.Scheme == "file" && File.Exists(uri.LocalPath))
{
// Added error handling here for local files
try
{
AttachSource(new Bitmap(uri.LocalPath));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Uri has unsupported scheme. Uri:{source}")
)
);
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Relative paths aren't supported. Uri:{source}")
)
);
}
}
private void AttachSource(IImage? image)
{
if (ImagePart != null)
{
ImagePart.Source = image;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
if (image == null)
{
State = AsyncImageState.Unloaded;
ImageTransition?.Start(ImagePart, PlaceholderPart, true, _tokenSource.Token);
}
else if (image.Size != default)
{
State = AsyncImageState.Loaded;
ImageTransition?.Start(PlaceholderPart, ImagePart, true, _tokenSource.Token);
RaiseEvent(new RoutedEventArgs(OpenedEvent));
}
}
private async Task<Bitmap> LoadImageAsync(Uri? url, CancellationToken token)
{
if (await ProvideCachedResourceAsync(url, token) is { } bitmap)
{
return bitmap;
}
#if NET6_0_OR_GREATER
using var client = new HttpClient();
var stream = await client.GetStreamAsync(url, token).ConfigureAwait(false);
await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, token).ConfigureAwait(false);
#elif NETSTANDARD2_0
using var client = new HttpClient();
var response = await client.GetAsync(url, token).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
#endif
memoryStream.Position = 0;
return new Bitmap(memoryStream);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SourceProperty)
{
SetSource(Source);
}
}
protected virtual async Task<Bitmap?> ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token)
{
if (IsCacheEnabled && imageUri != null)
{
return await ImageCache
.Instance.GetFromCacheAsync(imageUri, cancellationToken: token)
.ConfigureAwait(false);
}
return null;
}
}

565
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs

@ -1,565 +0,0 @@
// Parts of this file was taken from Windows Community Toolkit CacheBase implementation
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Cache/ImageCache.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
internal abstract class CacheBase<T>
{
private class ConcurrentRequest
{
public Task<T?>? Task { get; set; }
public bool EnsureCachedCopy { get; set; }
}
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1);
private string? _baseFolder = null;
private string? _cacheFolderName = null;
private string? _cacheFolder = null;
private InMemoryStorage<T>? _inMemoryFileStorage = null;
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks =
new ConcurrentDictionary<string, ConcurrentRequest>();
private HttpClient? _httpClient = null;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
protected CacheBase()
{
var options = CacheOptions.Default;
CacheDuration = options?.CacheDuration ?? TimeSpan.FromDays(1);
_baseFolder = options?.BaseCachePath ?? null;
_inMemoryFileStorage = new InMemoryStorage<T>();
RetryCount = 1;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; set; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; set; }
/// <summary>
/// Gets or sets max in-memory item storage count
/// </summary>
public int MaxMemoryCacheCount
{
get { return _inMemoryFileStorage?.MaxItemCount ?? 0; }
set
{
if (_inMemoryFileStorage != null)
_inMemoryFileStorage.MaxItemCount = value;
}
}
/// <summary>
/// Gets instance of <see cref="HttpClient"/>
/// </summary>
protected HttpClient HttpClient
{
get
{
if (_httpClient == null)
{
var messageHandler = new HttpClientHandler();
_httpClient = new HttpClient(messageHandler);
}
return _httpClient;
}
}
/// <summary>
/// Initializes FileCache and provides root folder and cache folder name
/// </summary>
/// <param name="folder">Folder that is used as root for cache</param>
/// <param name="folderName">Cache folder name</param>
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param>
/// <returns>awaitable task</returns>
public virtual async Task InitializeAsync(
string? folder = null,
string? folderName = null,
HttpMessageHandler? httpMessageHandler = null
)
{
_baseFolder = folder;
_cacheFolderName = folderName;
_cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false);
if (httpMessageHandler != null)
{
_httpClient = new HttpClient(httpMessageHandler);
}
}
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
public async Task ClearAsync()
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
await InternalClearAsync(files.Select(x => x as string)).ConfigureAwait(false);
_inMemoryFileStorage?.Clear();
}
/// <summary>
/// Clears file if it has expired
/// </summary>
/// <param name="duration">timespan to compute whether file has expired or not</param>
/// <returns>awaitable task</returns>
public Task ClearAsync(TimeSpan duration)
{
return RemoveExpiredAsync(duration);
}
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
{
TimeSpan expiryDuration = duration ?? CacheDuration;
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
foreach (var file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Clear(expiryDuration);
}
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
var keys = new List<string>();
Dictionary<string, string> hashDictionary = new Dictionary<string, string>();
foreach (var file in files)
{
hashDictionary.Add(Path.GetFileName(file), file);
}
foreach (var uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (hashDictionary.TryGetValue(fileName, out var file))
{
filesToDelete.Add(file);
keys.Add(fileName);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Remove(keys);
}
/// <summary>
/// Assures that item represented by Uri is cached.
/// </summary>
/// <param name="uri">Uri of the item</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param>
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>Awaitable Task</returns>
public Task PreCacheAsync(
Uri uri,
bool throwOnError = false,
bool storeToMemoryCache = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken);
}
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>an instance of Generic type</returns>
public Task<T?> GetFromCacheAsync(
Uri uri,
bool throwOnError = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, false, cancellationToken);
}
/// <summary>
/// Gets the string containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a string</returns>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
return Path.Combine(folder!, GetCacheFileName(uri));
}
/// <summary>
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>an instance of Generic type</returns>
public T? GetFromMemoryCache(Uri uri)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
return instance;
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(Stream stream);
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(string baseFile);
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected virtual async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var info = new FileInfo(file);
return info.Length == 0 || DateTime.Now.Subtract(info.LastWriteTime) > duration;
}
private static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
}
private async Task<T?> GetItemAsync(
Uri uri,
bool throwOnError,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
_concurrentTasks.TryGetValue(fileName, out var request);
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
if (request != null && request.EnsureCachedCopy && !preCacheOnly)
{
if (request.Task != null)
await request.Task.ConfigureAwait(false);
request = null;
}
if (request == null)
{
request = new ConcurrentRequest()
{
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken),
EnsureCachedCopy = preCacheOnly
};
_concurrentTasks[fileName] = request;
}
try
{
if (request.Task != null)
instance = await request.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
if (throwOnError)
{
throw;
}
}
finally
{
_concurrentTasks.TryRemove(fileName, out _);
}
return instance;
}
private async Task<T?> GetFromCacheOrDownloadAsync(
Uri uri,
string fileName,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
if (instance != null)
{
return instance;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var baseFile = Path.Combine(folder!, fileName);
bool downloadDataFile =
!File.Exists(baseFile)
|| await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false);
if (!File.Exists(baseFile))
{
File.Create(baseFile).Dispose();
}
if (downloadDataFile)
{
uint retries = 0;
try
{
while (retries < RetryCount)
{
try
{
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken)
.ConfigureAwait(false);
if (instance != null)
{
break;
}
}
catch (FileNotFoundException) { }
retries++;
}
}
catch (Exception ex)
{
File.Delete(baseFile);
throw; // re-throwing the exception changes the stack trace. just throw
}
}
if (EqualityComparer<T>.Default.Equals(instance, default(T)) && !preCacheOnly)
{
instance = await ConvertFromAsync(baseFile).ConfigureAwait(false);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var properties = new FileInfo(baseFile);
var msi = new InMemoryStorageItem<T>(fileName, properties.LastWriteTime, instance);
_inMemoryFileStorage?.SetItem(msi);
}
}
return instance;
}
private async Task<T?> DownloadFileAsync(
Uri uri,
string baseFile,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
using (MemoryStream ms = new MemoryStream())
{
using (var stream = await HttpClient.GetStreamAsync(uri))
{
stream.CopyTo(ms);
ms.Flush();
ms.Position = 0;
using (var fs = File.Open(baseFile, FileMode.OpenOrCreate, FileAccess.Write))
{
ms.CopyTo(fs);
fs.Flush();
ms.Position = 0;
}
}
// if its pre-cache we aren't looking to load items in memory
if (!preCacheOnly)
{
instance = await ConvertFromAsync(ms).ConfigureAwait(false);
}
}
return instance;
}
private async Task InternalClearAsync(IEnumerable<string?> files)
{
foreach (var file in files)
{
try
{
File.Delete(file!);
}
catch
{
// Just ignore errors for now}
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task ForceInitialiseAsync()
{
if (_cacheFolder != null)
{
return;
}
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false);
_inMemoryFileStorage = new InMemoryStorage<T>();
if (_baseFolder == null)
{
_baseFolder = Path.GetTempPath();
}
if (string.IsNullOrWhiteSpace(_cacheFolderName))
{
_cacheFolderName = GetType().Name;
}
try
{
_cacheFolder = Path.Combine(_baseFolder, _cacheFolderName);
Directory.CreateDirectory(_cacheFolder);
}
finally
{
_cacheFolderSemaphore.Release();
}
}
private async Task<string?> GetCacheFolderAsync()
{
if (_cacheFolder == null)
{
await ForceInitialiseAsync().ConfigureAwait(false);
}
return _cacheFolder;
}
}

18
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs

@ -1,18 +0,0 @@
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
public class CacheOptions
{
private static CacheOptions? _cacheOptions;
public static CacheOptions Default => _cacheOptions ??= new CacheOptions();
public static void SetDefault(CacheOptions defaultCacheOptions)
{
_cacheOptions = defaultCacheOptions;
}
public string? BaseCachePath { get; set; }
public TimeSpan? CacheDuration { get; set; }
}

36
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs

@ -1,36 +0,0 @@
using System.IO;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// </summary>
internal class FileCache : CacheBase<string>
{
/// <summary>
/// Private singleton field.
/// </summary>
private static FileCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static FileCache Instance => _instance ?? (_instance = new FileCache());
protected override Task<string> ConvertFromAsync(Stream stream)
{
// nothing to do in this instance;
return Task.FromResult<string>("");
}
/// <summary>
/// Returns a cached path
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected override Task<string> ConvertFromAsync(string baseFile)
{
return Task.FromResult(baseFile);
}
}

76
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs

@ -1,76 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache images in a folder
/// </summary>
internal class ImageCache : CacheBase<Bitmap>
{
/// <summary>
/// Private singleton field.
/// </summary>
[ThreadStatic]
private static ImageCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static ImageCache Instance => _instance ?? (_instance = new ImageCache());
/// <summary>
/// Creates a bitmap from a stream
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(Stream stream)
{
if (stream.Length == 0)
{
throw new FileNotFoundException();
}
return new Bitmap(stream);
}
/// <summary>
/// Creates a bitmap from a cached file
/// </summary>
/// <param name="baseFile">file</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(string baseFile)
{
using (var stream = File.OpenRead(baseFile))
{
return await ConvertFromAsync(stream).ConfigureAwait(false);
}
}
/// <summary>
/// Checks whether file is valid or not.
/// </summary>
/// <param name="file">file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected override async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var fileInfo = new FileInfo(file);
return fileInfo.Length == 0
|| DateTime.Now.Subtract(File.GetLastAccessTime(file)) > duration
|| DateTime.Now.Subtract(File.GetLastWriteTime(file)) > duration;
}
}

156
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs

@ -1,156 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Labs.Controls.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic in-memory storage of items
/// </summary>
/// <typeparam name="T">T defines the type of item stored</typeparam>
public class InMemoryStorage<T>
{
private int _maxItemCount;
private ConcurrentDictionary<string, InMemoryStorageItem<T>> _inMemoryStorage =
new ConcurrentDictionary<string, InMemoryStorageItem<T>>();
private object _settingMaxItemCountLocker = new object();
/// <summary>
/// Gets or sets the maximum count of Items that can be stored in this InMemoryStorage instance.
/// </summary>
public int MaxItemCount
{
get { return _maxItemCount; }
set
{
if (_maxItemCount == value)
{
return;
}
_maxItemCount = value;
lock (_settingMaxItemCountLocker)
{
EnsureStorageBounds(value);
}
}
}
/// <summary>
/// Clears all items stored in memory
/// </summary>
public void Clear()
{
_inMemoryStorage.Clear();
}
/// <summary>
/// Clears items stored in memory based on duration passed
/// </summary>
/// <param name="duration">TimeSpan to identify expired items</param>
public void Clear(TimeSpan duration)
{
var expirationDate = DateTime.Now.Subtract(duration);
var itemsToRemove = _inMemoryStorage
.Where(kvp => kvp.Value.LastUpdated <= expirationDate)
.Select(kvp => kvp.Key);
if (itemsToRemove.Any())
{
Remove(itemsToRemove);
}
}
/// <summary>
/// Remove items based on provided keys
/// </summary>
/// <param name="keys">identified of the in-memory storage item</param>
public void Remove(IEnumerable<string> keys)
{
foreach (var key in keys)
{
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
_inMemoryStorage.TryRemove(key, out _);
}
}
/// <summary>
/// Add new item to in-memory storage
/// </summary>
/// <param name="item">item to be stored</param>
public void SetItem(InMemoryStorageItem<T> item)
{
if (MaxItemCount == 0)
{
return;
}
_inMemoryStorage[item.Id] = item;
// ensure max limit is maintained. trim older entries first
if (_inMemoryStorage.Count > MaxItemCount)
{
var itemsToRemove = _inMemoryStorage
.OrderBy(kvp => kvp.Value.Created)
.Take(_inMemoryStorage.Count - MaxItemCount)
.Select(kvp => kvp.Key);
Remove(itemsToRemove);
}
}
/// <summary>
/// Get item from in-memory storage as long as it has not ex
/// </summary>
/// <param name="id">id of the in-memory storage item</param>
/// <param name="duration">timespan denoting expiration</param>
/// <returns>Valid item if not out of date or return null if out of date or item does not exist</returns>
public InMemoryStorageItem<T>? GetItem(string id, TimeSpan duration)
{
if (!_inMemoryStorage.TryGetValue(id, out var tempItem))
{
return null;
}
var expirationDate = DateTime.Now.Subtract(duration);
if (tempItem.LastUpdated > expirationDate)
{
return tempItem;
}
_inMemoryStorage.TryRemove(id, out _);
return null;
}
private void EnsureStorageBounds(int maxCount)
{
if (_inMemoryStorage.Count == 0)
{
return;
}
if (maxCount == 0)
{
_inMemoryStorage.Clear();
return;
}
if (_inMemoryStorage.Count > maxCount)
{
Remove(_inMemoryStorage.Keys.Take(_inMemoryStorage.Count - maxCount));
}
}
}

49
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs

@ -1,49 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic InMemoryStorageItem holds items for InMemoryStorage.
/// </summary>
/// <typeparam name="T">Type is set by consuming cache</typeparam>
public class InMemoryStorageItem<T>
{
/// <summary>
/// Gets the item identifier
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Gets the item created timestamp.
/// </summary>
public DateTime Created { get; private set; }
/// <summary>
/// Gets the item last updated timestamp.
/// </summary>
public DateTime LastUpdated { get; private set; }
/// <summary>
/// Gets the item being stored.
/// </summary>
public T Item { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryStorageItem{T}"/> class.
/// Constructor for InMemoryStorageItem
/// </summary>
/// <param name="id">uniquely identifies the item</param>
/// <param name="lastUpdated">last updated timestamp</param>
/// <param name="item">the item being stored</param>
public InMemoryStorageItem(string id, DateTime lastUpdated, T item)
{
Id = id;
LastUpdated = lastUpdated;
Item = item;
Created = DateTime.Now;
}
}

21
StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 AvaloniaUI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
StabilityMatrix.Avalonia/Controls/VendorLabs/Themes/BetterAsyncImage.axaml

@ -1,48 +0,0 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs">
<Design.PreviewWith>
<Border Width="200"
Height="200">
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type vendorLabs:BetterAsyncImage}"
TargetType="vendorLabs:BetterAsyncImage">
<Setter Property="Background" Value="Transparent" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border Margin="0"
Padding="0"
ClipToBounds="True"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Name="PART_PlaceholderImage"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Source="{TemplateBinding PlaceholderSource}"
Stretch="{TemplateBinding PlaceholderStretch}"/>
<Image Name="PART_Image"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Stretch="{TemplateBinding Stretch}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[State=Failed] /template/ Image#PART_Image">
<Setter Property="Opacity"
Value="0.0" />
</Style>
<Style Selector="^[State=Failed] /template/ Image#PART_PlaceholderImage">
<Setter Property="Opacity"
Value="1.0" />
</Style>
</ControlTheme>
</ResourceDictionary>

122
StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml

@ -1,122 +0,0 @@
<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

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

98
StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml

@ -1,98 +0,0 @@
<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

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

40
StabilityMatrix.Avalonia/Converters/EnumAttributeConverter.cs

@ -1,40 +0,0 @@
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

@ -1,13 +0,0 @@
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);
}

37
StabilityMatrix.Avalonia/Converters/FileUriConverter.cs

@ -1,37 +0,0 @@
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 when str.StartsWith("avares://") => new Uri(str),
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

@ -1,53 +0,0 @@
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();
}
}

344
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,14 +4,15 @@ 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;
@ -23,9 +24,8 @@ 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.Video;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
@ -36,12 +36,9 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
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;
@ -52,7 +49,6 @@ using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointB
namespace StabilityMatrix.Avalonia.DesignData;
[Localizable(false)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData
{
@ -100,19 +96,6 @@ public static class DesignData
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
new()
{
Id = Guid.NewGuid(),
DisplayName = "Running Comfy",
PackageName = "ComfyUI",
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
}
},
ActiveInstalledPackageId = activePackageId
@ -192,24 +175,12 @@ public static class DesignData
};
LaunchOptionsViewModel.UpdateFilterCards();
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,
packageFactory
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = new ObservableCollectionExtended<BasePackage>(
packageFactory.GetAllAvailablePackages()
);
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
ObservableCacheEx.AddOrUpdate(
CheckpointsPageViewModel.CheckpointFoldersCache,
@ -227,7 +198,6 @@ 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",
@ -327,7 +297,7 @@ public static class DesignData
);
}*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
@ -360,7 +330,7 @@ public static class DesignData
{
new()
{
NsfwLevel = 1,
Nsfw = "None",
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
@ -403,15 +373,13 @@ public static class DesignData
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }
};
ProgressManagerViewModel.ProgressItems.AddRange(
ProgressManagerViewModel
.ProgressItems
.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(
Guid.NewGuid(),
"Test File.exe",
new ProgressReport(0.5f, "Downloading...")
)
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"
@ -419,8 +387,7 @@ public static class DesignData
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package..."),
ModificationCompleteMessage = "Package installed successfully"
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
}
@ -435,10 +402,7 @@ public static class DesignData
}
[NotNull]
public static PackageInstallBrowserViewModel? NewInstallerDialogViewModel { get; private set; }
[NotNull]
public static PackageInstallDetailViewModel? PackageInstallDetailViewModel { get; private set; }
public static InstallerViewModel? InstallerViewModel { get; private set; }
[NotNull]
public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; }
@ -449,64 +413,16 @@ 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
@ -523,16 +439,6 @@ public static class DesignData
}
)
};
vm.Categories = new ObservableCollectionExtended<PackageOutputCategory>
{
new()
{
Name = "Category 1",
Path = "path1",
SubDirectories = [new PackageOutputCategory { Name = "SubCategory 1", Path = "path3" }]
},
new() { Name = "Category 2", Path = "path2" }
};
return vm;
}
}
@ -546,10 +452,7 @@ 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;
@ -558,39 +461,6 @@ 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>();
@ -599,21 +469,14 @@ 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
@ -693,10 +556,7 @@ 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 };
@ -770,22 +630,13 @@ 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 };
@ -801,8 +652,6 @@ 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 =>
@ -832,9 +681,6 @@ 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 =>
{
@ -860,8 +706,7 @@ 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>();
@ -914,8 +759,7 @@ 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 =>
@ -923,39 +767,6 @@ The gallery images are often inpainted, but you will get something very similar
vm.IsBatchIndexEnabled = true;
});
public static LayerDiffuseCardViewModel LayerDiffuseCardViewModel =>
DialogFactory.Get<LayerDiffuseCardViewModel>();
public static InstalledWorkflowsViewModel InstalledWorkflowsViewModel
{
get
{
var vm = Services.GetRequiredService<InstalledWorkflowsViewModel>();
vm.DisplayedWorkflows = new ObservableCollectionExtended<OpenArtMetadata>
{
new()
{
Workflow = new()
{
Name = "Test Workflow",
Creator = new OpenArtCreator { Name = "Test Creator" },
Thumbnails =
[
new OpenArtThumbnail
{
Url = new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512"
)
}
]
}
}
};
return vm;
}
}
public static IList<ICompletionData> SampleCompletionData =>
new List<ICompletionData>
{
@ -979,42 +790,6 @@ 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 =>
{
@ -1041,8 +816,7 @@ 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 =>
@ -1055,73 +829,12 @@ 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 OpenArtWorkflowViewModel OpenArtWorkflowViewModel =>
new(Services.GetRequiredService<ISettingsManager>(), Services.GetRequiredService<IPackageFactory>())
{
Workflow = new OpenArtSearchResult
{
Name = "Test Workflow",
Creator = new OpenArtCreator
{
Name = "Test Creator Name",
Username = "Test Creator Username"
},
Thumbnails =
[
new OpenArtThumbnail
{
Url = new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
}
],
NodesIndex =
[
"Anything Everywhere",
"Reroute",
"Note",
".",
"ComfyUI's ControlNet Auxiliary Preprocessors",
"DWPreprocessor",
"PixelPerfectResolution",
"AIO_Preprocessor",
",",
"ComfyUI",
"PreviewImage",
"CLIPTextEncode",
"EmptyLatentImage",
"SplitImageWithAlpha",
"ControlNetApplyAdvanced",
"JoinImageWithAlpha",
"LatentUpscaleBy",
"VAEEncode",
"LoadImage",
"ControlNetLoader",
"CLIPVisionLoader",
"SaveImage",
",",
"ComfyUI Impact Pack",
"SAMLoader",
"UltralyticsDetectorProvider",
"FaceDetailer",
","
]
}
};
public static string CurrentDirectory => Directory.GetCurrentDirectory();
public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get<ControlNetCardViewModel>();
public static Indexer Types { get; } = new();
@ -1131,8 +844,7 @@ 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);

21
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -27,9 +27,6 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
public IObservableCollection<HybridModelFile> ControlNetModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
public IObservableCollection<HybridModelFile> PromptExpansionModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
public IObservableCollection<ComfySampler> Samplers { get; } =
new ObservableCollectionExtended<ComfySampler>(ComfySampler.Defaults);
@ -41,9 +38,6 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
public IObservableCollection<ComfyScheduler> Schedulers { get; } =
new ObservableCollectionExtended<ComfyScheduler>(ComfyScheduler.Defaults);
public IObservableCollection<ComfyAuxPreprocessor> Preprocessors { get; } =
new ObservableCollectionExtended<ComfyAuxPreprocessor>(ComfyAuxPreprocessor.Defaults);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanUserConnect))]
private bool isConnected;
@ -70,19 +64,28 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
}
/// <inheritdoc />
public Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default)
public Task CopyImageToInputAsync(
FilePath imageFile,
CancellationToken cancellationToken = default
)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task UploadInputImageAsync(ImageSource image, CancellationToken cancellationToken = default)
public Task UploadInputImageAsync(
ImageSource image,
CancellationToken cancellationToken = default
)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task WriteImageToInputAsync(ImageSource imageSource, CancellationToken cancellationToken = default)
public Task WriteImageToInputAsync(
ImageSource imageSource,
CancellationToken cancellationToken = default
)
{
return Task.CompletedTask;
}

22
StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
@ -26,26 +25,9 @@ public class MockModelIndexService : IModelIndexService
}
/// <inheritdoc />
public Task<IEnumerable<LocalModelFile>> FindAsync(SharedFolderType type)
public Task<IReadOnlyList<LocalModelFile>> GetModelsOfType(SharedFolderType type)
{
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;
return Task.FromResult<IReadOnlyList<LocalModelFile>>(new List<LocalModelFile>());
}
/// <inheritdoc />

19
StabilityMatrix.Avalonia/DesignData/MockSettingsManager.cs

@ -1,22 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockSettingsManager : SettingsManager
{
protected override void LoadSettings(CancellationToken cancellationToken = default) { }
protected override Task LoadSettingsAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
protected override void SaveSettings(CancellationToken cancellationToken = default) { }
protected override Task SaveSettingsAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
protected override void LoadSettings() {}
protected override void SaveSettings() {}
}

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,7 +48,11 @@ public static class DialogHelper
IReadOnlyList<TextBoxField> textFields
)
{
return CreateTextEntryDialog(title, new MarkdownScrollViewer { Markdown = description }, textFields);
return CreateTextEntryDialog(
title,
new MarkdownScrollViewer { Markdown = description },
textFields
);
}
/// <summary>
@ -76,7 +80,11 @@ 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 }
};
@ -101,14 +109,18 @@ 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();
@ -242,16 +254,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();
@ -263,7 +275,9 @@ 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
{
@ -340,7 +354,9 @@ 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
{
@ -421,7 +437,8 @@ 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;
@ -509,7 +526,7 @@ public static class DialogHelper
models.Select(m => m.FileNameWithoutExtension)
);
if (result is { Score: > 40 })
if (result.Score > 40)
{
mainGrid.Children.Add(
new InfoBar

38
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -36,16 +36,15 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder
.Nodes.AddTypedNode(
new ComfyNodeBuilder.LatentFromBatch
{
Name = "LatentFromBatch",
Samples = builder.GetPrimaryAsLatent(),
builder.Connections.Primary = builder.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
builder.GetPrimaryAsLatent(),
// remote expects a 0-based index, vm is 1-based
BatchIndex = batchIndex.Value - 1,
Length = 1
}
batchIndex.Value - 1,
1
)
)
.Output;
}
@ -75,16 +74,15 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder
.Nodes.AddTypedNode(
new ComfyNodeBuilder.LatentFromBatch
{
Name = "LatentFromBatch",
Samples = builder.GetPrimaryAsLatent(),
builder.Connections.Primary = builder.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
builder.GetPrimaryAsLatent(),
// remote expects a 0-based index, vm is 1-based
BatchIndex = batchIndex.Value - 1,
Length = 1
}
batchIndex.Value - 1,
1
)
)
.Output;
}
@ -99,8 +97,8 @@ public static class ComfyNodeBuilderExtensions
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.Refiner.VAE
?? builder.Connections.Base.VAE
?? builder.Connections.RefinerVAE
?? builder.Connections.BaseVAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image

10
StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs

@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
using Avalonia.Input;
using Avalonia.Input;
namespace StabilityMatrix.Avalonia.Extensions;
@ -9,18 +8,11 @@ public static class DataObjectExtensions
/// Get Context from IDataObject, set by Xaml Behaviors
/// </summary>
public static T? GetContext<T>(this IDataObject dataObject)
{
try
{
if (dataObject.Get("Context") is T context)
{
return context;
}
}
catch (COMException)
{
return default;
}
return default;
}

54
StabilityMatrix.Core/Extensions/DirectoryPathExtensions.cs → StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs

@ -1,9 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Polly;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.Extensions;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DirectoryPathExtensions
@ -12,32 +15,21 @@ public static class DirectoryPathExtensions
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
public static Task DeleteVerboseAsync(
this DirectoryPath directory,
ILogger? logger = default,
CancellationToken cancellationToken = default
)
public static Task DeleteVerboseAsync(this DirectoryPath directory, ILogger? logger = default)
{
var policy = Policy
.Handle<IOException>()
.WaitAndRetryAsync(
3,
attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger?.LogWarning(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
directory,
calculatedWaitDuration
);
}
);
directory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() => DeleteVerbose(directory, logger, cancellationToken), cancellationToken)
.ConfigureAwait(false);
await Task.Run(() => { DeleteVerbose(directory, logger); });
});
}
@ -45,14 +37,8 @@ public static class DirectoryPathExtensions
/// Deletes a directory and all of its contents recursively.
/// Removes link targets without deleting the source.
/// </summary>
public static void DeleteVerbose(
this DirectoryPath directory,
ILogger? logger = default,
CancellationToken cancellationToken = default
)
public static void DeleteVerbose(this DirectoryPath directory, ILogger? logger = default)
{
cancellationToken.ThrowIfCancellationRequested();
// Skip if directory does not exist
if (!directory.Exists)
{
@ -61,7 +47,7 @@ public static class DirectoryPathExtensions
// For junction points, delete with recursive false
if (directory.IsSymbolicLink)
{
logger?.LogInformation("Removing junction point {TargetDirectory}", directory.FullPath);
logger?.LogInformation("Removing junction point {TargetDirectory}", directory);
try
{
directory.Delete(false);
@ -69,28 +55,26 @@ public static class DirectoryPathExtensions
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {directory.FullPath}", ex);
throw new IOException($"Failed to delete junction point {directory}", ex);
}
}
// Recursively delete all subdirectories
foreach (var subDir in directory.EnumerateDirectories())
foreach (var subDir in directory.Info.EnumerateDirectories())
{
DeleteVerbose(subDir, logger, cancellationToken);
DeleteVerbose(subDir, logger);
}
// Delete all files in the directory
foreach (var filePath in directory.EnumerateFiles())
foreach (var filePath in directory.Info.EnumerateFiles())
{
cancellationToken.ThrowIfCancellationRequested();
try
{
filePath.Info.Attributes = FileAttributes.Normal;
filePath.Attributes = FileAttributes.Normal;
filePath.Delete();
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath.FullPath}", ex);
throw new IOException($"Failed to delete file {filePath.FullName}", ex);
}
}

20
StabilityMatrix.Avalonia/Extensions/NotificationLevelExtensions.cs

@ -1,20 +0,0 @@
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

@ -1,54 +0,0 @@
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
}
);
}
}
}

9
StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs

@ -103,13 +103,4 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
}
}
}
public void ClearCache()
{
var cache =
this.GetPrivateField<ConcurrentDictionary<string, Task<Bitmap?>>>("_memoryCache")
?? throw new NullReferenceException("Memory cache not found");
cache.Clear();
}
}

24
StabilityMatrix.Avalonia/Helpers/IOCommands.cs

@ -16,28 +16,4 @@ public static class IOCommands
},
url => !string.IsNullOrWhiteSpace(url)
);
public static AsyncRelayCommand<string?> OpenFileBrowserCommand { get; } =
new(
async path =>
{
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFileBrowser(path);
},
path => !string.IsNullOrWhiteSpace(path)
);
public static AsyncRelayCommand<string?> OpenFolderBrowserCommand { get; } =
new(
async path =>
{
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFolderBrowser(path);
},
path => !string.IsNullOrWhiteSpace(path)
);
}

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

Loading…
Cancel
Save