diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 5b4bb470..55da51a7 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,10 +15,10 @@
]
},
"csharpier": {
- "version": "0.25.0",
+ "version": "0.26.4",
"commands": [
"dotnet-csharpier"
]
}
}
-}
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index ddc54b53..15cae5b1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,3 +7,7 @@ dotnet_sort_system_directives_first = true
# ReSharper properties
resharper_csharp_max_line_length = 120
+
+# dotnet code quality
+# noinspection EditorConfigKeyCorrectness
+dotnet_code_quality.CA1826.exclude_ordefault_methods = true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cf47cd4e..88b5eae3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -209,8 +209,6 @@ jobs:
files: |
StabilityMatrix-win-x64.zip
StabilityMatrix-linux-x64.zip
- StabilityMatrix-win-x64/StabilityMatrix.exe
- StabilityMatrix-linux-x64/StabilityMatrix.AppImage
fail_on_unmatched_files: true
tag_name: v${{ github.event.inputs.version }}
body: ${{ steps.release_notes.outputs.release_notes }}
@@ -244,7 +242,7 @@ jobs:
SM_CF_CACHE_PURGE_TOKEN: ${{ secrets.SM_CF_CACHE_PURGE_TOKEN }}
SM_CF_ZONE_ID: ${{ secrets.SM_CF_ZONE_ID }}
SM_SIGNING_PRIVATE_KEY: ${{ secrets.SM_SIGNING_PRIVATE_KEY }}
- run: sm-tools updates publish-matrix-v3 -v $RELEASE_VERSION -y
+ run: sm-tools updates publish-matrix-v3 -v ${{ github.event.inputs.version }} -y
publish-auto-update-b2:
name: Publish Auto-Update Release (B2)
diff --git a/Avalonia.Gif/Avalonia.Gif.csproj b/Avalonia.Gif/Avalonia.Gif.csproj
new file mode 100644
index 00000000..2d72f153
--- /dev/null
+++ b/Avalonia.Gif/Avalonia.Gif.csproj
@@ -0,0 +1,16 @@
+
+
+ net8.0
+ latest
+ true
+ win-x64;linux-x64;osx-x64;osx-arm64
+ enable
+ enable
+ true
+ true
+
+
+
+
+
+
diff --git a/Avalonia.Gif/BgWorkerCommand.cs b/Avalonia.Gif/BgWorkerCommand.cs
new file mode 100644
index 00000000..aebd44dd
--- /dev/null
+++ b/Avalonia.Gif/BgWorkerCommand.cs
@@ -0,0 +1,10 @@
+namespace Avalonia.Gif
+{
+ internal enum BgWorkerCommand
+ {
+ Null,
+ Play,
+ Pause,
+ Dispose
+ }
+}
diff --git a/Avalonia.Gif/BgWorkerState.cs b/Avalonia.Gif/BgWorkerState.cs
new file mode 100644
index 00000000..1b09bba1
--- /dev/null
+++ b/Avalonia.Gif/BgWorkerState.cs
@@ -0,0 +1,12 @@
+namespace Avalonia.Gif
+{
+ internal enum BgWorkerState
+ {
+ Null,
+ Start,
+ Running,
+ Paused,
+ Complete,
+ Dispose
+ }
+}
diff --git a/Avalonia.Gif/Decoding/BlockTypes.cs b/Avalonia.Gif/Decoding/BlockTypes.cs
new file mode 100644
index 00000000..2d804d5b
--- /dev/null
+++ b/Avalonia.Gif/Decoding/BlockTypes.cs
@@ -0,0 +1,10 @@
+namespace Avalonia.Gif.Decoding
+{
+ internal enum BlockTypes
+ {
+ Empty = 0,
+ Extension = 0x21,
+ ImageDescriptor = 0x2C,
+ Trailer = 0x3B,
+ }
+}
diff --git a/Avalonia.Gif/Decoding/ExtensionType.cs b/Avalonia.Gif/Decoding/ExtensionType.cs
new file mode 100644
index 00000000..5db6d575
--- /dev/null
+++ b/Avalonia.Gif/Decoding/ExtensionType.cs
@@ -0,0 +1,8 @@
+namespace Avalonia.Gif.Decoding
+{
+ internal enum ExtensionType
+ {
+ GraphicsControl = 0xF9,
+ Application = 0xFF
+ }
+}
diff --git a/Avalonia.Gif/Decoding/FrameDisposal.cs b/Avalonia.Gif/Decoding/FrameDisposal.cs
new file mode 100644
index 00000000..bf4f00b7
--- /dev/null
+++ b/Avalonia.Gif/Decoding/FrameDisposal.cs
@@ -0,0 +1,10 @@
+namespace Avalonia.Gif.Decoding
+{
+ public enum FrameDisposal
+ {
+ Unknown = 0,
+ Leave = 1,
+ Background = 2,
+ Restore = 3
+ }
+}
diff --git a/Avalonia.Gif/Decoding/GifColor.cs b/Avalonia.Gif/Decoding/GifColor.cs
new file mode 100644
index 00000000..222d7303
--- /dev/null
+++ b/Avalonia.Gif/Decoding/GifColor.cs
@@ -0,0 +1,36 @@
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Gif
+{
+ [StructLayout(LayoutKind.Explicit)]
+ public readonly struct GifColor
+ {
+ [FieldOffset(3)]
+ public readonly byte A;
+
+ [FieldOffset(2)]
+ public readonly byte R;
+
+ [FieldOffset(1)]
+ public readonly byte G;
+
+ [FieldOffset(0)]
+ public readonly byte B;
+
+ ///
+ /// A struct that represents a ARGB color and is aligned as
+ /// a BGRA bytefield in memory.
+ ///
+ /// Red
+ /// Green
+ /// Blue
+ /// Alpha
+ public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue)
+ {
+ A = a;
+ R = r;
+ G = g;
+ B = b;
+ }
+ }
+}
diff --git a/Avalonia.Gif/Decoding/GifDecoder.cs b/Avalonia.Gif/Decoding/GifDecoder.cs
new file mode 100644
index 00000000..adc26bb0
--- /dev/null
+++ b/Avalonia.Gif/Decoding/GifDecoder.cs
@@ -0,0 +1,653 @@
+// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer
+// as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer)
+// Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
+// Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved.
+
+// The rest of the source file is licensed under MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using Avalonia;
+using Avalonia.Media.Imaging;
+using static Avalonia.Gif.Extensions.StreamExtensions;
+
+namespace Avalonia.Gif.Decoding
+{
+ public sealed class GifDecoder : IDisposable
+ {
+ private static readonly ReadOnlyMemory G87AMagic = "GIF87a"u8.ToArray().AsMemory();
+
+ private static readonly ReadOnlyMemory G89AMagic = "GIF89a"u8.ToArray().AsMemory();
+
+ private static readonly ReadOnlyMemory 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 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.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.Shared.Return(tmpB);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void DrawFrame(GifFrame curFrame, Memory 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 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();
+ }
+
+ ///
+ /// Directly copies the struct array to a bitmap IntPtr.
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Processes GIF Header.
+ ///
+ private void ProcessHeaderData()
+ {
+ var str = _fileStream;
+ var tmpB = ArrayPool.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.Shared.Return(tmpB);
+ }
+
+ ///
+ /// Parses colors from file stream to target color table.
+ ///
+ 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;
+ }
+
+ ///
+ /// Parses screen and other GIF descriptors.
+ ///
+ 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);
+ }
+
+ ///
+ /// Parses all frame data.
+ ///
+ private void ProcessFrameData()
+ {
+ _fileStream.Position = Header.HeaderSize;
+
+ var tempBuf = ArrayPool.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.Shared.Return(tempBuf);
+ }
+
+ ///
+ /// Parses GIF Image Descriptor Block.
+ ///
+ 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());
+ }
+
+ ///
+ /// Parses GIF Extension Blocks.
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/Avalonia.Gif/Decoding/GifFrame.cs b/Avalonia.Gif/Decoding/GifFrame.cs
new file mode 100644
index 00000000..ea0e6640
--- /dev/null
+++ b/Avalonia.Gif/Decoding/GifFrame.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Avalonia.Gif.Decoding
+{
+ public class GifFrame
+ {
+ public bool HasTransparency,
+ IsInterlaced,
+ IsLocalColorTableUsed;
+ public byte TransparentColorIndex;
+ public int LzwMinCodeSize,
+ LocalColorTableSize;
+ public long LzwStreamPosition;
+ public TimeSpan FrameDelay;
+ public FrameDisposal FrameDisposalMethod;
+ public bool ShouldBackup;
+ public GifRect Dimensions;
+ public GifColor[] LocalColorTable;
+ }
+}
diff --git a/Avalonia.Gif/Decoding/GifHeader.cs b/Avalonia.Gif/Decoding/GifHeader.cs
new file mode 100644
index 00000000..16638f79
--- /dev/null
+++ b/Avalonia.Gif/Decoding/GifHeader.cs
@@ -0,0 +1,19 @@
+// Licensed under the MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+namespace Avalonia.Gif.Decoding
+{
+ public class GifHeader
+ {
+ public bool HasGlobalColorTable;
+ public int GlobalColorTableSize;
+ public ulong GlobalColorTableCacheId;
+ public int BackgroundColorIndex;
+ public long HeaderSize;
+ internal int Iterations = -1;
+ public GifRepeatBehavior IterationCount;
+ public GifRect Dimensions;
+ private GifColor[] _globarColorTable;
+ public GifColor[] GlobarColorTable;
+ }
+}
diff --git a/Avalonia.Gif/Decoding/GifRect.cs b/Avalonia.Gif/Decoding/GifRect.cs
new file mode 100644
index 00000000..01f621de
--- /dev/null
+++ b/Avalonia.Gif/Decoding/GifRect.cs
@@ -0,0 +1,43 @@
+namespace Avalonia.Gif.Decoding
+{
+ public readonly struct GifRect
+ {
+ public int X { get; }
+ public int Y { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public int TotalPixels { get; }
+
+ public GifRect(int x, int y, int width, int height)
+ {
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ TotalPixels = width * height;
+ }
+
+ public static bool operator ==(GifRect a, GifRect b)
+ {
+ return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height;
+ }
+
+ public static bool operator !=(GifRect a, GifRect b)
+ {
+ return !(a == b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null || GetType() != obj.GetType())
+ return false;
+
+ return this == (GifRect)obj;
+ }
+
+ public override int GetHashCode()
+ {
+ return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode();
+ }
+ }
+}
diff --git a/Avalonia.Gif/Decoding/GifRepeatBehavior.cs b/Avalonia.Gif/Decoding/GifRepeatBehavior.cs
new file mode 100644
index 00000000..4b27a7bb
--- /dev/null
+++ b/Avalonia.Gif/Decoding/GifRepeatBehavior.cs
@@ -0,0 +1,8 @@
+namespace Avalonia.Gif.Decoding
+{
+ public class GifRepeatBehavior
+ {
+ public bool LoopForever { get; set; }
+ public int? Count { get; set; }
+ }
+}
diff --git a/Avalonia.Gif/Decoding/InvalidGifStreamException.cs b/Avalonia.Gif/Decoding/InvalidGifStreamException.cs
new file mode 100644
index 00000000..b3554bac
--- /dev/null
+++ b/Avalonia.Gif/Decoding/InvalidGifStreamException.cs
@@ -0,0 +1,23 @@
+// Licensed under the MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+using System;
+using System.Runtime.Serialization;
+
+namespace Avalonia.Gif.Decoding
+{
+ [Serializable]
+ public class InvalidGifStreamException : Exception
+ {
+ public InvalidGifStreamException() { }
+
+ public InvalidGifStreamException(string message)
+ : base(message) { }
+
+ public InvalidGifStreamException(string message, Exception innerException)
+ : base(message, innerException) { }
+
+ protected InvalidGifStreamException(SerializationInfo info, StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/Avalonia.Gif/Decoding/LzwDecompressionException.cs b/Avalonia.Gif/Decoding/LzwDecompressionException.cs
new file mode 100644
index 00000000..ed25c0aa
--- /dev/null
+++ b/Avalonia.Gif/Decoding/LzwDecompressionException.cs
@@ -0,0 +1,23 @@
+// Licensed under the MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+using System;
+using System.Runtime.Serialization;
+
+namespace Avalonia.Gif.Decoding
+{
+ [Serializable]
+ public class LzwDecompressionException : Exception
+ {
+ public LzwDecompressionException() { }
+
+ public LzwDecompressionException(string message)
+ : base(message) { }
+
+ public LzwDecompressionException(string message, Exception innerException)
+ : base(message, innerException) { }
+
+ protected LzwDecompressionException(SerializationInfo info, StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/Avalonia.Gif/Extensions/StreamExtensions.cs b/Avalonia.Gif/Extensions/StreamExtensions.cs
new file mode 100644
index 00000000..ac08fa68
--- /dev/null
+++ b/Avalonia.Gif/Extensions/StreamExtensions.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Gif.Extensions
+{
+ [DebuggerStepThrough]
+ internal static class StreamExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ushort SpanToShort(Span b) => (ushort)(b[0] | (b[1] << 8));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Skip(this Stream stream, long count)
+ {
+ stream.Position += count;
+ }
+
+ ///
+ /// Read a Gif block from stream while advancing the position.
+ ///
+ [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;
+ }
+
+ ///
+ /// Skips GIF blocks until it encounters an empty block.
+ ///
+ [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);
+ }
+
+ ///
+ /// Read a from stream by providing a temporary buffer.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ushort ReadUShortS(this Stream stream, byte[] tempBuf)
+ {
+ stream.Read(tempBuf, 0, 2);
+ return SpanToShort(tempBuf);
+ }
+
+ ///
+ /// Read a from stream by providing a temporary buffer.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte ReadByteS(this Stream stream, byte[] tempBuf)
+ {
+ stream.Read(tempBuf, 0, 1);
+ var finalVal = tempBuf[0];
+ return finalVal;
+ }
+ }
+}
diff --git a/Avalonia.Gif/GifImage.cs b/Avalonia.Gif/GifImage.cs
new file mode 100644
index 00000000..d53cbd19
--- /dev/null
+++ b/Avalonia.Gif/GifImage.cs
@@ -0,0 +1,296 @@
+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 SourceUriRawProperty = AvaloniaProperty.Register<
+ GifImage,
+ string
+ >("SourceUriRaw");
+
+ public static readonly StyledProperty SourceUriProperty = AvaloniaProperty.Register(
+ "SourceUri"
+ );
+
+ public static readonly StyledProperty SourceStreamProperty = AvaloniaProperty.Register<
+ GifImage,
+ Stream
+ >("SourceStream");
+
+ public static readonly StyledProperty IterationCountProperty = AvaloniaProperty.Register<
+ GifImage,
+ IterationCount
+ >("IterationCount", IterationCount.Infinite);
+
+ private GifInstance? _gifInstance;
+
+ public static readonly StyledProperty StretchDirectionProperty = AvaloniaProperty.Register<
+ GifImage,
+ StretchDirection
+ >("StretchDirection");
+
+ public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(
+ "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 GifInstance? _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 GifInstance 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());
+ }
+ }
+ }
+
+ ///
+ /// Measures the control.
+ ///
+ /// The available size.
+ /// The desired size of the control.
+ 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;
+ }
+
+ ///
+ 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 GifInstance(source);
+ _gifInstance.IterationCount = IterationCount;
+ _customVisual?.SendHandlerMessage(_gifInstance);
+ }
+ }
+}
diff --git a/Avalonia.Gif/GifInstance.cs b/Avalonia.Gif/GifInstance.cs
new file mode 100644
index 00000000..30e002d1
--- /dev/null
+++ b/Avalonia.Gif/GifInstance.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Gif.Decoding;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+namespace Avalonia.Gif
+{
+ public class GifInstance : IDisposable
+ {
+ 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 _frameTimes;
+ private uint _iterationCount;
+ private int _currentFrameIndex;
+ private readonly List _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;
+ }
+ }
+}
diff --git a/Avalonia.Gif/InvalidGifStreamException.cs b/Avalonia.Gif/InvalidGifStreamException.cs
new file mode 100644
index 00000000..9771d9cb
--- /dev/null
+++ b/Avalonia.Gif/InvalidGifStreamException.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Avalonia.Gif
+{
+ [Serializable]
+ internal class InvalidGifStreamException : Exception
+ {
+ public InvalidGifStreamException() { }
+
+ public InvalidGifStreamException(string message)
+ : base(message) { }
+
+ public InvalidGifStreamException(string message, Exception innerException)
+ : base(message, innerException) { }
+
+ protected InvalidGifStreamException(SerializationInfo info, StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2f6870b..cd5995d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,142 @@ 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.7.2
+### Changed
+- Changed Symlink shared folder link targets for Automatic1111 and ComfyUI. From `ControlNet -> models/controlnet` to `ControlNet -> models/controlnet/ControlNet` and `T2IAdapter -> models/controlnet/T2IAdapter`.
+- Changed FreeU defaults to match recommended SD1.5 defaults
+- Changed default denoise strength from 1.0 to 0.7
+### Fixed
+- Fixed ControlNet / T2IAdapter shared folder links for Automatic1111 conflicting with each other
+- Fixed URIScheme registration errors on Linux
+- Fixed RuinedFooocus missing output folder on startup
+- Fixed incorrect Fooocus VRAM launch arguments
+
+## v2.7.1
+### Added
+- Added Turkish UI language option, thanks to Progresor for the translation
+### Fixed
+- Fixed Inference Image to Image projects missing denoise strength setting
+
+## v2.7.0
+### Added
+#### General
+- New package: [RuinedFooocus](https://github.com/runew0lf/RuinedFooocus)
+- Added an X button to all search fields to instantly clear them (Esc key also works)
+- Added System Information section to Settings
+#### Inference
+- Added Image to Image project type
+- Added Modular custom steps
+ - Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline.
+- Added Sampler addons
+ - Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes.
+- Added SD Turbo Scheduler
+- Added display names for new samplers ("Heun++ 2", "DDPM", "LCM")
+- Added Ctrl+Enter as a shortcut for the Generate Image button
+#### Accounts Settings Subpage
+- Lykos Account sign-up and login - currently for Patreon OAuth connections but GitHub requests caching and settings sync are planned
+- Supporters can now connect your Patreon accounts, then head to the Updates page to choose to receive auto-updates from the Dev or Preview channels
+- CivitAI Account login with API key - enables downloading models from the Browser page that require CivitAI logins, more integrations like liking and commenting are also planned
+#### Updates Settings Subpage
+- Toggle auto-update notifications and manually check for updates
+- Choose between Stable, Preview, and Dev update channels
+#### Inference Settings Subpage
+- Moved Inference settings to subpage
+- Updated with more localized labels
+#### Outputs Page
+- Added Refresh button to update gallery from file system changes
+#### Checkpoints Page
+- Added the ability to drag & drop checkpoints between different folders
+- Added "Copy Trigger Words" option to the three-dots menu on the Checkpoints page (when data is available)
+- Added trigger words on checkpoint card and tooltip
+- Added "Find Connected Metadata" options for root-level and file-level scans
+- Added "Update Existing Metadata" button
+#### Model Browser
+- Added Hugging Face tab to the Model Browser
+- Added additional base model filter options for CivitAI ("SD 1.5 LCM", "SDXL 1.0 LCM", "SDXL Turbo", "Other")
+- Added the ability to type in a specific page number in the CivitAI Model Browser
+- Right clicking anywhere on the model card will open the same menu as the three-dots button
+- New model downloads will save trigger words in metadata, if available
+- Model author username and avatar display, with clickable link to their profile
+### Changed
+#### General
+- Model Browser page has been redesigned, featuring more information like rating and download counts
+- Model Browser navigation has improved animations on hover and compact number formatting
+- Updated Outputs Page button and menu layout
+- Rearranged Add Package dialog slightly to accommodate longer package list
+- Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders
+- Model Browser now split into "CivitAI" and "Hugging Face" tabs
+#### Inference
+- Selected images (i.e. Image2Image, Upscale, ControlNet) will now save their source paths saved and restored on load. If the image is moved or deleted, the selection will show as missing and can be reselected
+- Project files (.smproj) have been updated to v3, existing projects will be upgraded on load and will no longer be compatible with older versions of Stability Matrix
+### Fixed
+- Fixed Outputs page reverting back to Shared Output Folder every time the page is reloaded
+- Potentially fixed updates sometimes clearing settings or launching in the wrong directory
+- Improved startup time and window load time after exiting dialogs
+- Fixed control character decoding that caused some progress bars to show as `\u2588`
+- Fixed Python `rich` package's progress bars not showing in console
+- Optimized ProgressRing animation bindings to reduce CPU usage
+- Improved safety checks in custom control rendering to reduce potential graphical artifacts
+- Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111)
+- Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters
+- Fixed crash when loading an empty settings file
+- Improve Settings save and load performance with .NET 8 Source Generating Serialization
+- Fixed ApplicationException during database shutdown
+- InvokeAI model links for T2I/IpAdapters now point to the correct folders
+- Added extra checks to help prevent settings resetting in certain scenarios
+- Fixed Refiner model enabled state not saving to Inference project files
+- Fixed NullReference error labels when clearing the Inference batch size settings, now shows improved message with minimum and maximum value constraints
+
+## v2.7.0-pre.4
+### Added
+#### Inference
+- Added Image to Image project type
+- Added Modular custom steps
+ - Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline.
+- Added Sampler addons
+ - Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes.
+- Added SD Turbo Scheduler
+- Added display names for new samplers ("Heun++ 2", "DDPM", "LCM")
+#### Model Browser
+- Added additional base model filter options ("SD 1.5 LCM", "SDXL 1.0 LCM", "SDXL Turbo", "Other")
+### Changed
+#### Inference
+- Selected images (i.e. Image2Image, Upscale, ControlNet) will now save their source paths saved and restored on load. If the image is moved or deleted, the selection will show as missing and can be reselected
+- Project files (.smproj) have been updated to v3, existing projects will be upgraded on load and will no longer be compatible with older versions of Stability Matrix
+### Fixed
+- Fixed Refiner model enabled state not saving to Inference project files
+
+## v2.7.0-pre.3
+### Added
+- Added "Find Connected Metadata" options for root-level and file-level scans to the Checkpoints page
+- Added "Update Existing Metadata" button to the Checkpoints page
+- Added Hugging Face tab to the Model Browser
+- Added the ability to type in a specific page number in the CivitAI Model Browser
+### Changed
+- Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders
+- Model Browser now split into "CivitAI" and "Hugging Face" tabs
+### Fixed
+- InvokeAI model links for T2I/IpAdapters now point to the correct folders
+- Added extra checks to help prevent settings resetting in certain scenarios
+
+## v2.7.0-pre.2
+### Added
+- Added System Information section to Settings
+### Changed
+- Moved Inference Settings to subpage
+### Fixed
+- Fixed crash when loading an empty settings file
+- Improve Settings save and load performance with .NET 8 Source Generating Serialization
+- Fixed ApplicationException during database shutdown
+
+## v2.7.0-pre.1
+### Fixed
+- Fixed control character decoding that caused some progress bars to show as `\u2588`
+- Fixed Python `rich` package's progress bars not showing in console
+- Optimized ProgressRing animation bindings to reduce CPU usage
+- Improved safety checks in custom control rendering to reduce potential graphical artifacts
+- Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111)
+
## v2.7.0-dev.4
### Fixed
- Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters
@@ -36,7 +172,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Added Refresh button to update gallery from file system changes
#### Checkpoints Page
- Added the ability to drag & drop checkpoints between different folders
-## Changed
+### Changed
#### Outputs Page
- Updated button and menu layout
#### Packages Page
@@ -55,9 +191,14 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Updates Settings Subpage
- Toggle auto-update notifications and manually check for updates
- Choose between Stable, Preview, and Dev update channels
-## Changed
+### Changed
- Model Browser page has been redesigned, featuring more information like rating and download counts
+## v2.6.7
+### Fixed
+- Fixed prerequisite install not unpacking due to improperly formatted 7z argument (Caused the "python310._pth FileNotFoundException")
+- Fixed [#301](https://github.com/LykosAI/StabilityMatrix/issues/301) - Package updates failing silently because of a PortableGit error
+
## v2.6.6
### Fixed
- Fixed [#297](https://github.com/LykosAI/StabilityMatrix/issues/297) - Model browser LiteAsyncException occuring when fetching entries with unrecognized values from enum name changes
diff --git a/README.md b/README.md
index d94a82d2..daadd8f5 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,8 @@ Stability Matrix is now available in the following languages, thanks to our comm
- Lautaroturina
- 🇷🇺 Русский
- aolko
+- 🇹🇷 Türkçe
+ - Progresor
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.
diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
index eb84e3f3..84e8c3f9 100644
--- a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
+++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
@@ -21,6 +21,7 @@
+
diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index 4e02cb07..7439ffa5 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/StabilityMatrix.Avalonia/App.axaml
@@ -4,6 +4,7 @@
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
+ Name="Stability Matrix"
RequestedThemeVariant="Dark">
@@ -21,18 +22,16 @@
-
+
700
- 32
-
avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP
@@ -53,29 +52,30 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs
index d5e91657..742b10a3 100644
--- a/StabilityMatrix.Avalonia/App.axaml.cs
+++ b/StabilityMatrix.Avalonia/App.axaml.cs
@@ -1,7 +1,3 @@
-#if DEBUG
-using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
-using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
-#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -14,6 +10,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@@ -64,11 +61,17 @@ 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;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
+#endif
namespace StabilityMatrix.Avalonia;
public sealed class App : Application
{
+ private static bool isAsyncDisposeComplete;
+
[NotNull]
public static IServiceProvider? Services { get; private set; }
@@ -77,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; }
@@ -114,7 +116,8 @@ public sealed class App : Application
public override void OnFrameworkInitializationCompleted()
{
// Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit
- var dataValidationPluginsToRemove = BindingPlugins.DataValidators
+ var dataValidationPluginsToRemove = BindingPlugins
+ .DataValidators
.OfType()
.ToArray();
@@ -158,19 +161,22 @@ public sealed class App : Application
DesktopLifetime.MainWindow = setupWindow;
- setupWindow.ShowAsyncCts.Token.Register(() =>
- {
- if (setupWindow.Result == ContentDialogResult.Primary)
+ setupWindow
+ .ShowAsyncCts
+ .Token
+ .Register(() =>
{
- settingsManager.SetEulaAccepted();
- ShowMainWindow();
- DesktopLifetime.MainWindow.Show();
- }
- else
- {
- Shutdown();
- }
- });
+ if (setupWindow.Result == ContentDialogResult.Primary)
+ {
+ settingsManager.SetEulaAccepted();
+ ShowMainWindow();
+ DesktopLifetime.MainWindow.Show();
+ }
+ else
+ {
+ Shutdown();
+ }
+ });
}
else
{
@@ -217,25 +223,7 @@ public sealed class App : Application
mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
- mainWindow.Closing += (_, _) =>
- {
- var validWindowPosition = mainWindow.Screens.All.Any(
- screen => screen.Bounds.Contains(mainWindow.Position)
- );
-
- settingsManager.Transaction(
- s =>
- {
- s.WindowSettings = new WindowSettings(
- mainWindow.Width,
- mainWindow.Height,
- validWindowPosition ? mainWindow.Position.X : 0,
- validWindowPosition ? mainWindow.Position.Y : 0
- );
- },
- ignoreMissingLibraryDir: true
- );
- };
+ mainWindow.Closing += OnMainWindowClosing;
mainWindow.Closed += (_, _) => Shutdown();
mainWindow.SetDefaultFonts();
@@ -301,10 +289,7 @@ public sealed class App : Application
services.AddSingleton(p => p.GetRequiredService());
}
- internal static void ConfigureDialogViewModels(
- IServiceCollection services,
- Type[] exportedTypes
- )
+ internal static void ConfigureDialogViewModels(IServiceCollection services, Type[] exportedTypes)
{
// Dialog factory
services.AddSingleton>(provider =>
@@ -312,17 +297,7 @@ public sealed class App : Application
var serviceManager = new ServiceManager();
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();
@@ -346,21 +321,18 @@ public sealed class App : Application
services.AddMessagePipe();
services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix");
- var exportedTypes = AppDomain.CurrentDomain
+ var exportedTypes = AppDomain
+ .CurrentDomain
.GetAssemblies()
.Where(a => a.FullName?.StartsWith("StabilityMatrix") == true)
.SelectMany(a => a.GetExportedTypes())
.ToArray();
var transientTypes = exportedTypes
- .Select(
- t =>
- new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) }
- )
+ .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] });
@@ -377,23 +349,12 @@ public sealed class App : Application
}
var singletonTypes = exportedTypes
- .Select(
- t =>
- new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) }
- )
+ .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().ToArray()
- }
- );
+ .Select(t1 => new { Type = t1.t, Attributes = t1.attributes.Cast().ToArray() });
foreach (var typePair in singletonTypes)
{
@@ -425,9 +386,7 @@ public sealed class App : Application
// Rich presence
services.AddSingleton();
- services.AddSingleton(
- provider => provider.GetRequiredService()
- );
+ services.AddSingleton(provider => provider.GetRequiredService());
Config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@@ -473,9 +432,7 @@ public sealed class App : Application
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter());
- jsonSerializerOptions.Converters.Add(
- new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
- );
+ jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
var defaultRefitSettings = new RefitSettings
@@ -484,8 +441,7 @@ public sealed class App : Application
};
// Refit settings for IApiFactory
- var defaultSystemTextJsonSettings =
- SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
+ var defaultSystemTextJsonSettings = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
defaultSystemTextJsonSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
var apiFactoryRefitSettings = new RefitSettings
{
@@ -559,27 +515,19 @@ public sealed class App : Application
c.BaseAddress = new Uri("https://stableauthentication.azurewebsites.net");
c.Timeout = TimeSpan.FromSeconds(15);
})
- .ConfigurePrimaryHttpMessageHandler(
- () => new HttpClientHandler { AllowAutoRedirect = false }
- )
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
.AddPolicyHandler(retryPolicy)
.AddHttpMessageHandler(
serviceProvider =>
- new TokenAuthHeaderHandler(
- serviceProvider.GetRequiredService()
- )
+ new TokenAuthHeaderHandler(serviceProvider.GetRequiredService())
);
// Add Refit client managers
- services
- .AddHttpClient("A3Client")
- .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));
+ services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));
services
.AddHttpClient("DontFollowRedirects")
- .ConfigurePrimaryHttpMessageHandler(
- () => new HttpClientHandler { AllowAutoRedirect = false }
- )
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
.AddPolicyHandler(retryPolicy);
/*services.AddHttpClient("IComfyApi")
@@ -609,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());
@@ -639,6 +583,87 @@ public sealed class App : Application
}
}
+ ///
+ /// Handle shutdown requests (happens before )
+ ///
+ private static void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
+ {
+ if (e.Cancel)
+ return;
+
+ var mainWindow = (MainWindow)sender!;
+
+ // Show confirmation if package running
+ var launchPageViewModel = Services.GetRequiredService();
+ launchPageViewModel.OnMainWindowClosing(e);
+
+ if (e.Cancel)
+ return;
+
+ // Check if we need to dispose IAsyncDisposables
+ if (
+ !isAsyncDisposeComplete
+ && Services.GetServices().ToList() is { Count: > 0 } asyncDisposables
+ )
+ {
+ // Cancel shutdown for now
+ e.Cancel = true;
+ isAsyncDisposeComplete = true;
+
+ Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
+
+ Task.Run(async () =>
+ {
+ foreach (var disposable in asyncDisposables)
+ {
+ Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})");
+ try
+ {
+ await disposable.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Debug.Fail(ex.ToString());
+ }
+ }
+ })
+ .ContinueWith(_ =>
+ {
+ // Shutdown again
+ Dispatcher.UIThread.Invoke(() => Shutdown());
+ })
+ .SafeFireAndForget();
+
+ return;
+ }
+
+ OnMainWindowClosingTerminal(mainWindow);
+ }
+
+ ///
+ /// Called at the end of before the main window is closed.
+ ///
+ private static void OnMainWindowClosingTerminal(Window sender)
+ {
+ var settingsManager = Services.GetRequiredService();
+
+ // 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)
{
Debug.WriteLine("Start OnExit");
@@ -646,16 +671,15 @@ public sealed class App : Application
var settingsManager = Services.GetRequiredService();
// If RemoveFolderLinksOnShutdown is set, delete all package junctions
- if (
- settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true }
- )
+ if (settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true })
{
var sharedFolders = Services.GetRequiredService();
sharedFolders.RemoveLinksForAllPackages();
}
Debug.WriteLine("Start OnExit: Disposing services");
- // Dispose all services
+
+ // Dispose IDisposable services
foreach (var disposable in Services.GetServices())
{
Debug.WriteLine($"Disposing {disposable.GetType().Name}");
@@ -690,13 +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",
+ FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
+ ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveNumbering = ArchiveNumberingMode.Rolling,
MaxArchiveFiles = 2
}
@@ -709,8 +730,11 @@ 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);
+
+ // Disable LoadableViewModelBase trace logging by default
builder
- .ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
+ .ForLogger("StabilityMatrix.Avalonia.ViewModels.Base.LoadableViewModelBase")
.WriteToNil(NLog.LogLevel.Debug);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget);
@@ -727,18 +751,20 @@ public sealed class App : Application
// Sentry
if (SentrySdk.IsEnabled)
{
- LogManager.Configuration.AddSentry(o =>
- {
- o.InitializeSdk = false;
- o.Layout = "${message}";
- o.ShutdownTimeoutSeconds = 5;
- o.IncludeEventDataOnBreadcrumbs = true;
- o.BreadcrumbLayout = "${logger}: ${message}";
- // Debug and higher are stored as breadcrumbs (default is Info)
- o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
- // Error and higher is sent as event (default is Error)
- o.MinimumEventLevel = NLog.LogLevel.Error;
- });
+ LogManager
+ .Configuration
+ .AddSentry(o =>
+ {
+ o.InitializeSdk = false;
+ o.Layout = "${message}";
+ o.ShutdownTimeoutSeconds = 5;
+ o.IncludeEventDataOnBreadcrumbs = true;
+ o.BreadcrumbLayout = "${logger}: ${message}";
+ // Debug and higher are stored as breadcrumbs (default is Info)
+ o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
+ // Error and higher is sent as event (default is Error)
+ o.MinimumEventLevel = NLog.LogLevel.Error;
+ });
}
LogManager.ReconfigExistingLoggers();
@@ -777,36 +803,34 @@ public sealed class App : Application
results.Add(ms);
}
- Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var dest = await StorageProvider.SaveFilePickerAsync(
- new FilePickerSaveOptions()
- {
- SuggestedFileName = "screenshot.png",
- ShowOverwritePrompt = true
- }
- );
-
- if (dest?.TryGetLocalPath() is { } localPath)
+ Dispatcher
+ .UIThread
+ .InvokeAsync(async () =>
{
- var localFile = new FilePath(localPath);
- foreach (var (i, stream) in results.Enumerate())
+ var dest = await StorageProvider.SaveFilePickerAsync(
+ new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true }
+ );
+
+ if (dest?.TryGetLocalPath() is { } localPath)
{
- var name = localFile.NameWithoutExtension;
- if (results.Count > 1)
+ var localFile = new FilePath(localPath);
+ foreach (var (i, stream) in results.Enumerate())
{
- name += $"_{i + 1}";
- }
+ var name = localFile.NameWithoutExtension;
+ if (results.Count > 1)
+ {
+ name += $"_{i + 1}";
+ }
- localFile = localFile.Directory!.JoinFile(name + ".png");
- localFile.Create();
+ localFile = localFile.Directory!.JoinFile(name + ".png");
+ localFile.Create();
- await using var fileStream = localFile.Info.OpenWrite();
- stream.Seek(0, SeekOrigin.Begin);
- await stream.CopyToAsync(fileStream);
+ await using var fileStream = localFile.Info.OpenWrite();
+ stream.Seek(0, SeekOrigin.Begin);
+ await stream.CopyToAsync(fileStream);
+ }
}
- }
- });
+ });
}
[Conditional("DEBUG")]
@@ -822,8 +846,7 @@ public sealed class App : Application
{
#if DEBUG
setupBuilder.SetupExtensions(
- extensionBuilder =>
- extensionBuilder.RegisterTarget("DataStoreLogger")
+ extensionBuilder => extensionBuilder.RegisterTarget("DataStoreLogger")
);
#endif
}
diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs
index 58867158..68f76884 100644
--- a/StabilityMatrix.Avalonia/Assets.cs
+++ b/StabilityMatrix.Avalonia/Assets.cs
@@ -11,20 +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");
///
/// Fixed image for models with no images.
///
- public static Uri NoImage { get; } =
- new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
+ 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");
@@ -32,6 +28,8 @@ internal static class Assets
public static AvaloniaResource ThemeMatrixDarkJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json");
+ public static AvaloniaResource HfPackagesJson => new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json");
+
private const UnixFileMode Unix755 =
UnixFileMode.UserRead
| UnixFileMode.UserWrite
@@ -46,23 +44,14 @@ 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
- )
+ new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", Unix755)
),
(
PlatformKind.MacOS | PlatformKind.Arm,
- new AvaloniaResource(
- "avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz",
- Unix755
- )
+ new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", Unix755)
)
);
@@ -73,21 +62,15 @@ internal static class Assets
Compat.Switch(
(
PlatformKind.Windows,
- new AvaloniaResource(
- "avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt"
- )
+ new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt")
),
(
PlatformKind.Linux | PlatformKind.X64,
- new AvaloniaResource(
- "avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt"
- )
+ new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt")
),
(
PlatformKind.MacOS | PlatformKind.Arm,
- new AvaloniaResource(
- "avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt"
- )
+ new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt")
)
);
@@ -111,9 +94,7 @@ internal static class Assets
PlatformKind.Windows | PlatformKind.X64,
new RemoteResource
{
- Url = new Uri(
- "https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"
- ),
+ Url = new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"),
HashSha256 = "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d"
}
),
@@ -167,9 +148,7 @@ internal static class Assets
///
/// Yield AvaloniaResources given a relative directory path within the 'Assets' folder.
///
- 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);
diff --git a/StabilityMatrix.Avalonia/Assets/hf-packages.json b/StabilityMatrix.Avalonia/Assets/hf-packages.json
new file mode 100644
index 00000000..e103d80e
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Assets/hf-packages.json
@@ -0,0 +1,544 @@
+[
+ {
+ "ModelCategory": "BaseModel",
+ "ModelName": "Stable Diffusion 1.5",
+ "RepositoryPath": "runwayml/stable-diffusion-v1-5",
+ "Files": [
+ "v1-5-pruned-emaonly.safetensors"
+ ],
+ "LicenseType": "CreativeML Open RAIL-M"
+ },
+ {
+ "ModelCategory": "BaseModel",
+ "ModelName": "Stable Diffusion 2.1",
+ "RepositoryPath": "stabilityai/stable-diffusion-2-1",
+ "Files": [
+ "v2-1_768-ema-pruned.safetensors"
+ ],
+ "LicenseType": "Open RAIL++"
+ },
+ {
+ "ModelCategory": "BaseModel",
+ "ModelName": "Stable Diffusion XL (Base)",
+ "RepositoryPath": "stabilityai/stable-diffusion-xl-base-1.0",
+ "Files": [
+ "sd_xl_base_1.0_0.9vae.safetensors",
+ "sd_xl_offset_example-lora_1.0.safetensors"
+ ],
+ "LicenseType": "Open RAIL++",
+ "LicensePath": "LICENSE.md"
+ },
+ {
+ "ModelCategory": "BaseModel",
+ "ModelName": "Stable Diffusion XL (Refiner)",
+ "RepositoryPath": "stabilityai/stable-diffusion-xl-refiner-1.0",
+ "Files": [
+ "sd_xl_refiner_1.0_0.9vae.safetensors"
+ ],
+ "LicenseType": "Open RAIL++",
+ "LicensePath": "LICENSE.md"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Canny",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_canny.pth",
+ "control_v11p_sd15_canny.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Depth",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11f1p_sd15_depth.pth",
+ "control_v11f1p_sd15_depth.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "MLSD",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_mlsd.pth",
+ "control_v11p_sd15_mlsd.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Inpaint",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_inpaint.pth",
+ "control_v11p_sd15_inpaint.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "IP2P",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11e_sd15_ip2p.pth",
+ "control_v11e_sd15_ip2p.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Tile",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11f1e_sd15_tile.pth",
+ "control_v11f1e_sd15_tile.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "OpenPose",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_openpose.pth",
+ "control_v11p_sd15_openpose.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "LineArt",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_lineart.pth",
+ "control_v11p_sd15_lineart.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "LineArt Anime",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15s2_lineart_anime.pth",
+ "control_v11p_sd15s2_lineart_anime.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "NormalBae",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_normalbae.pth",
+ "control_v11p_sd15_normalbae.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Seg",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_seg.pth",
+ "control_v11p_sd15_seg.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Soft Edge",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_softedge.pth",
+ "control_v11p_sd15_softedge.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Scribble",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11p_sd15_scribble.pth",
+ "control_v11p_sd15_scribble.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "ControlNet",
+ "ModelName": "Shuffle",
+ "RepositoryPath": "lllyasviel/ControlNet-v1-1",
+ "Files": [
+ "control_v11e_sd15_shuffle.pth",
+ "control_v11e_sd15_shuffle.yaml"
+ ],
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Canny",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_canny",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "canny",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Depth",
+ "RepositoryPath": "lllyasviel/control_v11f1p_sd15_depth",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "depth",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "MLSD",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_mlsd",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "MLSD",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Inpaint",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_inpaint",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "inpaint",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "IP2P",
+ "RepositoryPath": "lllyasviel/control_v11e_sd15_ip2p",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "ip2p",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Tile",
+ "RepositoryPath": "lllyasviel/control_v11f1e_sd15_tile",
+ "Files": [
+ "diffusion_pytorch_model.bin",
+ "config.json"
+ ],
+ "Subfolder": "tile",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "OpenPose",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_openpose",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "openpose",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "LineArt",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_lineart",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "lineart",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "LineArt Anime",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15s2_lineart_anime",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "lineart_anime",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "NormalBae",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_normalbae",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "normalbae",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Seg",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_seg",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "seg",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Soft Edge",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_softedge",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "softedge",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Scribble",
+ "RepositoryPath": "lllyasviel/control_v11p_sd15_scribble",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "scribble",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersControlNet",
+ "ModelName": "Shuffle",
+ "RepositoryPath": "lllyasviel/control_v11e_sd15_shuffle",
+ "Files": [
+ "diffusion_pytorch_model.safetensors",
+ "config.json"
+ ],
+ "Subfolder": "shuffle",
+ "LicenseType": "Open RAIL"
+ },
+ {
+ "ModelCategory": "DiffusersClipVision",
+ "ModelName": "IP Adapter Encoder",
+ "RepositoryPath": "InvokeAI/ip_adapter_sd_image_encoder",
+ "Files": [
+ "config.json",
+ "model.safetensors"
+ ],
+ "Subfolder": "ip_adapter_sd_image_encoder",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersClipVision",
+ "ModelName": "IP Adapter Encoder (SDXL)",
+ "RepositoryPath": "InvokeAI/ip_adapter_sdxl_image_encoder",
+ "Files": [
+ "config.json",
+ "model.safetensors"
+ ],
+ "Subfolder": "ip_adapter_sd_image_encoder",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapter",
+ "ModelName": "SD 1.5 Adapter",
+ "RepositoryPath": "InvokeAI/ip_adapter_sd15",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_sd15",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapter",
+ "ModelName": "SD 1.5 Light Adapter",
+ "RepositoryPath": "InvokeAI/ip_adapter_sd15_light",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_sd15_light",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapter",
+ "ModelName": "SD 1.5 Plus Adapter",
+ "RepositoryPath": "InvokeAI/ip_adapter_plus_sd15",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_plus_sd15",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapter",
+ "ModelName": "SD 1.5 Face Plus Adapter",
+ "RepositoryPath": "InvokeAI/ip_adapter_plus_face_sd15",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_plus_face_sd15",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapterXl",
+ "ModelName": "SDXL Adapter",
+ "RepositoryPath": "InvokeAI/ip_adapter_sdxl",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_sdxl",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapterXl",
+ "ModelName": "SDXL Plus Adapter",
+ "RepositoryPath": "InvokeAI/ip-adapter-plus_sdxl_vit-h",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_plus_sdxl_vit-h",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersIpAdapterXl",
+ "ModelName": "SDXL Face Plus Adapter",
+ "RepositoryPath": "InvokeAI/ip-adapter-plus-face_sdxl_vit-h",
+ "Files": [
+ "image_encoder.txt",
+ "ip_adapter.bin"
+ ],
+ "Subfolder": "ip_adapter_plus_face_sdxl_vit-h",
+ "LicenseType": "CreativeML Open RAIL-M"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Sketch",
+ "RepositoryPath": "TencentARC/t2iadapter_sketch_sd15v2",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.bin"
+ ],
+ "Subfolder": "t2iadapter_sketch_sd15v2",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Depth",
+ "RepositoryPath": "TencentARC/t2iadapter_depth_sd15v2",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.bin"
+ ],
+ "Subfolder": "t2iadapter_depth_sd15v2",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Canny",
+ "RepositoryPath": "TencentARC/t2iadapter_canny_sd15v2",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.bin"
+ ],
+ "Subfolder": "t2iadapter_canny_sd15v2",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Depth-Zoe",
+ "RepositoryPath": "TencentARC/t2iadapter_zoedepth_sd15v1",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.bin"
+ ],
+ "Subfolder": "t2iadapter_zoedepth_sd15v1",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Sketch (SDXL)",
+ "RepositoryPath": "TencentARC/t2i-adapter-sketch-sdxl-1.0",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.safetensors"
+ ],
+ "Subfolder": "t2i-adapter-sketch-sdxl-1.0",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Depth-Zoe (SDXL)",
+ "RepositoryPath": "TencentARC/t2i-adapter-depth-zoe-sdxl-1.0",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.safetensors"
+ ],
+ "Subfolder": "t2i-adapter-depth-zoe-sdxl-1.0",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "OpenPose (SDXL)",
+ "RepositoryPath": "TencentARC/t2i-adapter-openpose-sdxl-1.0",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.safetensors"
+ ],
+ "Subfolder": "t2i-adapter-openpose-sdxl-1.0",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Depth-MiDaS (SDXL)",
+ "RepositoryPath": "TencentARC/t2i-adapter-depth-midas-sdxl-1.0",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.safetensors"
+ ],
+ "Subfolder": "t2i-adapter-depth-midas-sdxl-1.0",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "LineArt (SDXL)",
+ "RepositoryPath": "TencentARC/t2i-adapter-lineart-sdxl-1.0",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.safetensors"
+ ],
+ "Subfolder": "t2i-adapter-lineart-sdxl-1.0",
+ "LicenseType": "Apache 2.0"
+ },
+ {
+ "ModelCategory": "DiffusersT2IAdapter",
+ "ModelName": "Canny (SDXL)",
+ "RepositoryPath": "TencentARC/t2i-adapter-canny-sdxl-1.0",
+ "Files": [
+ "config.json",
+ "diffusion_pytorch_model.safetensors"
+ ],
+ "Subfolder": "t2i-adapter-canny-sdxl-1.0",
+ "LicenseType": "Apache 2.0"
+ }
+]
diff --git a/StabilityMatrix.Avalonia/Assets/santahat.png b/StabilityMatrix.Avalonia/Assets/santahat.png
new file mode 100644
index 00000000..40815d07
Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/santahat.png differ
diff --git a/StabilityMatrix.Avalonia/Assets/sitecustomize.py b/StabilityMatrix.Avalonia/Assets/sitecustomize.py
index 8c2718f4..b6ebb4c1 100644
--- a/StabilityMatrix.Avalonia/Assets/sitecustomize.py
+++ b/StabilityMatrix.Avalonia/Assets/sitecustomize.py
@@ -46,7 +46,41 @@ def audit(event: str, *args):
# Reconfigure stdout to UTF-8
# noinspection PyUnresolvedReferences
+sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
+sys.stderr.reconfigure(encoding="utf-8")
# Install the audit hook
sys.addaudithook(audit)
+
+# Patch Rich terminal detection
+def _patch_rich_console():
+ try:
+ from 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
+def _patch_tqdm():
+ try:
+ import sys
+ from tqdm import std
+
+ sys.stderr = sys.stdout
+ except ImportError:
+ pass
+ except Exception as e:
+ print("[sitecustomize error]:", e)
+
+_patch_tqdm()
diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs
index 80704547..76e08f3c 100644
--- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs
+++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs
@@ -54,11 +54,7 @@ public class AdvancedImageBox : TemplatedControl
remove => _propertyChanged -= value;
}
- protected bool RaiseAndSetIfChanged(
- ref T field,
- T value,
- [CallerMemberName] string? propertyName = null
- )
+ protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer.Default.Equals(field, value))
return false;
@@ -335,9 +331,7 @@ public class AdvancedImageBox : TemplatedControl
if (index < Count - 1)
index++;
- return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel
- ? constrainZoomLevel
- : this[index];
+ return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index];
}
///
@@ -352,9 +346,7 @@ public class AdvancedImageBox : TemplatedControl
if (index > 0)
index--;
- return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel
- ? constrainZoomLevel
- : this[index];
+ return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index];
}
///
@@ -525,11 +517,10 @@ public class AdvancedImageBox : TemplatedControl
#endregion
#region Properties
- public static readonly DirectProperty CanRenderProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(CanRender),
- o => o.CanRender
- );
+ public static readonly DirectProperty CanRenderProperty = AvaloniaProperty.RegisterDirect<
+ AdvancedImageBox,
+ bool
+ >(nameof(CanRender), o => o.CanRender);
///
/// Gets or sets if control can render the image
@@ -560,11 +551,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridCellSizeProperty, value);
}
- public static readonly StyledProperty GridColorProperty =
- AvaloniaProperty.Register(
- nameof(GridColor),
- SolidColorBrush.Parse("#181818")
- );
+ public static readonly StyledProperty GridColorProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ ISolidColorBrush
+ >(nameof(GridColor), SolidColorBrush.Parse("#181818"));
///
/// Gets or sets the color used to create the checkerboard style background
@@ -575,11 +565,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridColorProperty, value);
}
- public static readonly StyledProperty GridColorAlternateProperty =
- AvaloniaProperty.Register(
- nameof(GridColorAlternate),
- SolidColorBrush.Parse("#252525")
- );
+ public static readonly StyledProperty GridColorAlternateProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ ISolidColorBrush
+ >(nameof(GridColorAlternate), SolidColorBrush.Parse("#252525"));
///
/// Gets or sets the color used to create the checkerboard style background
@@ -606,7 +595,8 @@ public class AdvancedImageBox : TemplatedControl
{
var loader = ImageLoader.AsyncImageLoader;
- Dispatcher.UIThread
+ Dispatcher
+ .UIThread
.InvokeAsync(async () =>
{
Image = await loader.ProvideImageAsync(value);
@@ -616,10 +606,9 @@ public class AdvancedImageBox : TemplatedControl
}
}
- public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- Bitmap?
- >(nameof(Image));
+ public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register(
+ nameof(Image)
+ );
///
/// Gets or sets the image to be displayed
@@ -656,8 +645,7 @@ public class AdvancedImageBox : TemplatedControl
{
offsetBackup = Offset;
- var zoomFactorScale =
- (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
+ var zoomFactorScale = (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
var imageScale = newImage.Size / oldImage.Size;
Debug.WriteLine($"Image scale: {imageScale}");
@@ -731,8 +719,10 @@ public class AdvancedImageBox : TemplatedControl
public bool HaveTrackerImage => _trackerImage is not null;
- public static readonly StyledProperty TrackerImageAutoZoomProperty =
- AvaloniaProperty.Register(nameof(TrackerImageAutoZoom), true);
+ public static readonly StyledProperty TrackerImageAutoZoomProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ bool
+ >(nameof(TrackerImageAutoZoom), true);
///
/// Gets or sets if the tracker image will be scaled to the current zoom
@@ -743,8 +733,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(TrackerImageAutoZoomProperty, value);
}
- public static readonly StyledProperty IsTrackerImageEnabledProperty =
- AvaloniaProperty.Register("IsTrackerImageEnabled");
+ public static readonly StyledProperty IsTrackerImageEnabledProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ bool
+ >("IsTrackerImageEnabled");
public bool IsTrackerImageEnabled
{
@@ -776,10 +768,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
- public static readonly StyledProperty ShowGridProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- bool
- >(nameof(ShowGrid), true);
+ public static readonly StyledProperty ShowGridProperty = AvaloniaProperty.Register(
+ nameof(ShowGrid),
+ true
+ );
///
/// Gets or sets the grid visibility when reach high zoom levels
@@ -791,10 +783,7 @@ public class AdvancedImageBox : TemplatedControl
}
public static readonly DirectProperty PointerPositionProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(PointerPosition),
- o => o.PointerPosition
- );
+ AvaloniaProperty.RegisterDirect(nameof(PointerPosition), o => o.PointerPosition);
///
/// Gets the current pointer position
@@ -805,11 +794,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value);
}
- public static readonly DirectProperty IsPanningProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(IsPanning),
- o => o.IsPanning
- );
+ public static readonly DirectProperty IsPanningProperty = AvaloniaProperty.RegisterDirect<
+ AdvancedImageBox,
+ bool
+ >(nameof(IsPanning), o => o.IsPanning);
///
/// Gets if control is currently panning
@@ -836,11 +824,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
- public static readonly DirectProperty IsSelectingProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(IsSelecting),
- o => o.IsSelecting
- );
+ public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect<
+ AdvancedImageBox,
+ bool
+ >(nameof(IsSelecting), o => o.IsSelecting);
///
/// Gets if control is currently selecting a ROI
@@ -863,10 +850,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
- public static readonly StyledProperty AutoPanProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- bool
- >(nameof(AutoPan), true);
+ public static readonly StyledProperty AutoPanProperty = AvaloniaProperty.Register(
+ nameof(AutoPan),
+ true
+ );
///
/// Gets or sets if the control can pan with the mouse
@@ -877,11 +864,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AutoPanProperty, value);
}
- public static readonly StyledProperty PanWithMouseButtonsProperty =
- AvaloniaProperty.Register(
- nameof(PanWithMouseButtons),
- MouseButtons.LeftButton | MouseButtons.MiddleButton
- );
+ public static readonly StyledProperty PanWithMouseButtonsProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ MouseButtons
+ >(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton);
///
/// Gets or sets the mouse buttons to pan the image
@@ -906,11 +892,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PanWithArrowsProperty, value);
}
- public static readonly StyledProperty SelectWithMouseButtonsProperty =
- AvaloniaProperty.Register(
- nameof(SelectWithMouseButtons),
- MouseButtons.LeftButton
- );
+ public static readonly StyledProperty SelectWithMouseButtonsProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ MouseButtons
+ >(nameof(SelectWithMouseButtons), MouseButtons.LeftButton);
///
/// Gets or sets the mouse buttons to select a region on image
@@ -935,10 +920,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(InvertMousePanProperty, value);
}
- public static readonly StyledProperty AutoCenterProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- bool
- >(nameof(AutoCenter), true);
+ public static readonly StyledProperty AutoCenterProperty = AvaloniaProperty.Register(
+ nameof(AutoCenter),
+ true
+ );
///
/// Gets or sets if image is auto centered
@@ -993,10 +978,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
- public static readonly StyledProperty AllowZoomProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- bool
- >(nameof(AllowZoom), true);
+ public static readonly StyledProperty AllowZoomProperty = AvaloniaProperty.Register(
+ nameof(AllowZoom),
+ true
+ );
///
/// Gets or sets if zoom is allowed
@@ -1007,14 +992,12 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AllowZoomProperty, value);
}
- public static readonly DirectProperty<
- AdvancedImageBox,
- ZoomLevelCollection
- > ZoomLevelsProperty = AvaloniaProperty.RegisterDirect(
- nameof(ZoomLevels),
- o => o.ZoomLevels,
- (o, v) => o.ZoomLevels = v
- );
+ public static readonly DirectProperty ZoomLevelsProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ZoomLevels),
+ o => o.ZoomLevels,
+ (o, v) => o.ZoomLevels = v
+ );
ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default;
@@ -1028,10 +1011,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value);
}
- public static readonly StyledProperty MinZoomProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- int
- >(nameof(MinZoom), 10);
+ public static readonly StyledProperty MinZoomProperty = AvaloniaProperty.Register(
+ nameof(MinZoom),
+ 10
+ );
///
/// Gets or sets the minimum possible zoom.
@@ -1043,10 +1026,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MinZoomProperty, value);
}
- public static readonly StyledProperty MaxZoomProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- int
- >(nameof(MaxZoom), 6400);
+ public static readonly StyledProperty MaxZoomProperty = AvaloniaProperty.Register(
+ nameof(MaxZoom),
+ 6400
+ );
///
/// Gets or sets the maximum possible zoom.
@@ -1058,8 +1041,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MaxZoomProperty, value);
}
- public static readonly StyledProperty ConstrainZoomOutToFitLevelProperty =
- AvaloniaProperty.Register(nameof(ConstrainZoomOutToFitLevel), true);
+ public static readonly StyledProperty ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ bool
+ >(nameof(ConstrainZoomOutToFitLevel), true);
///
/// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level.
@@ -1070,8 +1055,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(ConstrainZoomOutToFitLevelProperty, value);
}
- public static readonly DirectProperty OldZoomProperty =
- AvaloniaProperty.RegisterDirect(nameof(OldZoom), o => o.OldZoom);
+ public static readonly DirectProperty OldZoomProperty = AvaloniaProperty.RegisterDirect<
+ AdvancedImageBox,
+ int
+ >(nameof(OldZoom), o => o.OldZoom);
private int _oldZoom = 100;
@@ -1085,10 +1072,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value);
}
- public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register<
- AdvancedImageBox,
- int
- >(nameof(Zoom), 100);
+ public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register(
+ nameof(Zoom),
+ 100
+ );
///
/// Gets or sets the zoom.
@@ -1178,11 +1165,10 @@ public class AdvancedImageBox : TemplatedControl
/// The height of the scaled image.
public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0;
- public static readonly StyledProperty PixelGridColorProperty =
- AvaloniaProperty.Register(
- nameof(PixelGridColor),
- Brushes.DimGray
- );
+ public static readonly StyledProperty PixelGridColorProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ ISolidColorBrush
+ >(nameof(PixelGridColor), Brushes.DimGray);
///
/// Gets or sets the color of the pixel grid.
@@ -1194,8 +1180,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridColorProperty, value);
}
- public static readonly StyledProperty PixelGridZoomThresholdProperty =
- AvaloniaProperty.Register(nameof(PixelGridZoomThreshold), 13);
+ public static readonly StyledProperty PixelGridZoomThresholdProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ int
+ >(nameof(PixelGridZoomThreshold), 13);
///
/// Minimum size of zoomed pixel's before the pixel grid will be drawn
@@ -1206,11 +1194,15 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridZoomThresholdProperty, value);
}
- public static readonly StyledProperty SelectionModeProperty =
- AvaloniaProperty.Register(nameof(SelectionMode));
+ public static readonly StyledProperty SelectionModeProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ SelectionModes
+ >(nameof(SelectionMode));
- public static readonly StyledProperty IsPixelGridEnabledProperty =
- AvaloniaProperty.Register("IsPixelGridEnabled", true);
+ public static readonly StyledProperty IsPixelGridEnabledProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ bool
+ >("IsPixelGridEnabled", true);
///
/// Whether or not to draw the pixel grid at the
@@ -1227,11 +1219,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(SelectionModeProperty, value);
}
- public static readonly StyledProperty SelectionColorProperty =
- AvaloniaProperty.Register(
- nameof(SelectionColor),
- new SolidColorBrush(new Color(127, 0, 128, 255))
- );
+ public static readonly StyledProperty SelectionColorProperty = AvaloniaProperty.Register<
+ AdvancedImageBox,
+ ISolidColorBrush
+ >(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255)));
public ISolidColorBrush SelectionColor
{
@@ -1313,14 +1304,9 @@ public class AdvancedImageBox : TemplatedControl
AffectsRender(ShowGridProperty);
HorizontalScrollBar =
- e.NameScope.Find("PART_HorizontalScrollBar")
- ?? throw new NullReferenceException();
- VerticalScrollBar =
- e.NameScope.Find("PART_VerticalScrollBar")
- ?? throw new NullReferenceException();
- ViewPort =
- e.NameScope.Find("PART_ViewPort")
- ?? throw new NullReferenceException();
+ e.NameScope.Find("PART_HorizontalScrollBar") ?? throw new NullReferenceException();
+ VerticalScrollBar = e.NameScope.Find("PART_VerticalScrollBar") ?? throw new NullReferenceException();
+ ViewPort = e.NameScope.Find("PART_ViewPort") ?? throw new NullReferenceException();
SizeModeChanged();
@@ -1347,13 +1333,12 @@ public class AdvancedImageBox : TemplatedControl
// If we're in high zoom, switch off bitmap interpolation mode
// Otherwise use high quality
- BitmapInterpolationMode = isHighZoom
- ? BitmapInterpolationMode.None
- : BitmapInterpolationMode.HighQuality;
+ BitmapInterpolationMode = isHighZoom ? BitmapInterpolationMode.None : BitmapInterpolationMode.HighQuality;
InvalidateVisual();
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RenderBackgroundGrid(DrawingContext context)
{
var size = GridCellSize;
@@ -1387,8 +1372,6 @@ public class AdvancedImageBox : TemplatedControl
public override void Render(DrawingContext context)
{
- base.Render(context);
-
var gridCellSize = GridCellSize;
if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible))
@@ -1399,9 +1382,7 @@ public class AdvancedImageBox : TemplatedControl
var zoomFactor = ZoomFactor;
var shouldDrawPixelGrid =
- IsPixelGridEnabled
- && SizeMode == SizeModes.Normal
- && zoomFactor > PixelGridZoomThreshold;
+ IsPixelGridEnabled && SizeMode == SizeModes.Normal && zoomFactor > PixelGridZoomThreshold;
// Draw Grid
/*var viewPortSize = ViewPortSize;
@@ -1441,16 +1422,10 @@ public class AdvancedImageBox : TemplatedControl
if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 })
{
var destSize = TrackerImageAutoZoom
- ? new Size(
- _trackerImage!.Size.Width * zoomFactor,
- _trackerImage.Size.Height * zoomFactor
- )
+ ? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor)
: image.Size;
- var destPos = new Point(
- _pointerPosition.X - destSize.Width / 2,
- _pointerPosition.Y - destSize.Height / 2
- );
+ var destPos = new Point(_pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2);
context.DrawImage(_trackerImage!, new Rect(destPos, destSize));
}
@@ -1462,30 +1437,14 @@ public class AdvancedImageBox : TemplatedControl
var offsetY = Offset.Y % zoomFactor;
Pen pen = new(PixelGridColor);
- for (
- var x = imageViewPort.X + zoomFactor - offsetX;
- x < imageViewPort.Right;
- x += zoomFactor
- )
+ for (var x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor)
{
- context.DrawLine(
- pen,
- new Point(x, imageViewPort.X),
- new Point(x, imageViewPort.Bottom)
- );
+ context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom));
}
- for (
- var y = imageViewPort.Y + zoomFactor - offsetY;
- y < imageViewPort.Bottom;
- y += zoomFactor
- )
+ for (var y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor)
{
- context.DrawLine(
- pen,
- new Point(imageViewPort.Y, y),
- new Point(imageViewPort.Right, y)
- );
+ context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y));
}
context.DrawRectangle(pen, imageViewPort);
@@ -1496,12 +1455,7 @@ public class AdvancedImageBox : TemplatedControl
var rect = GetOffsetRectangle(SelectionRegion);
var selectionColor = SelectionColor;
context.FillRectangle(selectionColor, rect);
- var color = Color.FromArgb(
- 255,
- selectionColor.Color.R,
- selectionColor.Color.G,
- selectionColor.Color.B
- );
+ var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B);
context.DrawRectangle(new Pen(color.ToUInt32()), rect);
}
}
@@ -1609,8 +1563,7 @@ public class AdvancedImageBox : TemplatedControl
{
if (
!(
- pointer.Properties.IsLeftButtonPressed
- && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
+ pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed
&& (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed
@@ -1624,12 +1577,10 @@ public class AdvancedImageBox : TemplatedControl
{
if (
!(
- pointer.Properties.IsLeftButtonPressed
- && (PanWithMouseButtons & MouseButtons.LeftButton) != 0
+ pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed
&& (PanWithMouseButtons & MouseButtons.MiddleButton) != 0
- || pointer.Properties.IsRightButtonPressed
- && (PanWithMouseButtons & MouseButtons.RightButton) != 0
+ || pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0
)
|| !AutoPan
|| SizeMode != SizeModes.Normal
@@ -2669,12 +2620,8 @@ public class AdvancedImageBox : TemplatedControl
case SizeModes.Normal:
if (AutoCenter)
{
- xOffset = (
- !IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0
- );
- yOffset = (
- !IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0
- );
+ xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0);
+ yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0);
}
width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width);
@@ -2729,12 +2676,7 @@ public class AdvancedImageBox : TemplatedControl
var pixelSize = SelectionPixelSize;
using var frameBuffer = image.Lock();
- var newBitmap = new WriteableBitmap(
- pixelSize,
- image.Dpi,
- frameBuffer.Format,
- AlphaFormat.Unpremul
- );
+ var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul);
using var newFrameBuffer = newBitmap.Lock();
var i = 0;
diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
index d9032f21..eb681d53 100644
--- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
+++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
@@ -1,30 +1,70 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs b/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs
index 8f5f4502..0af2ea7a 100644
--- a/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs
+++ b/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs
@@ -15,24 +15,28 @@ public class BetterAdvancedImage : AdvancedImage
#region Reflection Shenanigans to access private parent fields
[NotNull]
private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField(
- "_isCornerRadiusUsed",BindingFlags.Instance | BindingFlags.NonPublic);
-
+ "_isCornerRadiusUsed",
+ BindingFlags.Instance | BindingFlags.NonPublic
+ );
+
[NotNull]
private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField(
- "_cornerRadiusClip",BindingFlags.Instance | BindingFlags.NonPublic);
-
+ "_cornerRadiusClip",
+ BindingFlags.Instance | BindingFlags.NonPublic
+ );
+
private bool IsCornerRadiusUsed
{
get => IsCornerRadiusUsedField.GetValue(this) as bool? ?? false;
set => IsCornerRadiusUsedField.SetValue(this, value);
}
-
+
private RoundedRect CornerRadiusClip
{
- get => (RoundedRect) CornerRadiusClipField.GetValue(this)!;
+ get => (RoundedRect)CornerRadiusClipField.GetValue(this)!;
set => CornerRadiusClipField.SetValue(this, value);
}
-
+
static BetterAdvancedImage()
{
if (IsCornerRadiusUsedField is null)
@@ -45,16 +49,14 @@ public class BetterAdvancedImage : AdvancedImage
}
}
#endregion
-
+
protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage);
-
- public BetterAdvancedImage(Uri? baseUri) : base(baseUri)
- {
- }
- public BetterAdvancedImage(IServiceProvider serviceProvider) : base(serviceProvider)
- {
- }
+ public BetterAdvancedImage(Uri? baseUri)
+ : base(baseUri) { }
+
+ public BetterAdvancedImage(IServiceProvider serviceProvider)
+ : base(serviceProvider) { }
///
///
@@ -69,61 +71,66 @@ public class BetterAdvancedImage : AdvancedImage
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
var scaledSize = sourceSize * scale;
-
+
// Calculate starting points for dest
var destX = HorizontalContentAlignment switch
{
HorizontalAlignment.Left => 0,
- HorizontalAlignment.Center => (int) (viewPort.Width - scaledSize.Width) / 2,
- HorizontalAlignment.Right => (int) (viewPort.Width - scaledSize.Width),
+ HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2,
+ HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width),
// Stretch is default, use center
- HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2,
+ HorizontalAlignment.Stretch
+ => (int)(viewPort.Width - scaledSize.Width) / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment))
};
var destY = VerticalContentAlignment switch
{
VerticalAlignment.Top => 0,
- VerticalAlignment.Center => (int) (viewPort.Height - scaledSize.Height) / 2,
- VerticalAlignment.Bottom => (int) (viewPort.Height - scaledSize.Height),
+ VerticalAlignment.Center => (int)(viewPort.Height - scaledSize.Height) / 2,
+ VerticalAlignment.Bottom => (int)(viewPort.Height - scaledSize.Height),
VerticalAlignment.Stretch => 0, // Stretch is default, use top
_ => throw new ArgumentException(nameof(VerticalContentAlignment))
};
-
- var destRect = viewPort
- .CenterRect(new Rect(scaledSize))
- .WithX(destX)
- .WithY(destY)
- .Intersect(viewPort);
+
+ var destRect = viewPort.CenterRect(new Rect(scaledSize)).WithX(destX).WithY(destY).Intersect(viewPort);
var destRectUnscaledSize = destRect.Size / scale;
-
+
// Calculate starting points for source
var sourceX = HorizontalContentAlignment switch
{
HorizontalAlignment.Left => 0,
- HorizontalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Width / 2,
- HorizontalAlignment.Right => (int) (sourceSize - destRectUnscaledSize).Width,
+ HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2,
+ HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width,
// Stretch is default, use center
- HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2,
+ HorizontalAlignment.Stretch
+ => (int)(sourceSize - destRectUnscaledSize).Width / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment))
};
var sourceY = VerticalContentAlignment switch
{
VerticalAlignment.Top => 0,
- VerticalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Height / 2,
- VerticalAlignment.Bottom => (int) (sourceSize - destRectUnscaledSize).Height,
+ VerticalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Height / 2,
+ VerticalAlignment.Bottom => (int)(sourceSize - destRectUnscaledSize).Height,
VerticalAlignment.Stretch => 0, // Stretch is default, use top
_ => throw new ArgumentException(nameof(VerticalContentAlignment))
};
-
+
var sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale))
.WithX(sourceX)
.WithY(sourceY);
-
- DrawingContext.PushedState? pushedState =
- IsCornerRadiusUsed ? context.PushClip(CornerRadiusClip) : null;
- context.DrawImage(source, sourceRect, destRect);
- pushedState?.Dispose();
+
+ if (IsCornerRadiusUsed)
+ {
+ using (context.PushClip(CornerRadiusClip))
+ {
+ context.DrawImage(source, sourceRect, destRect);
+ }
+ }
+ else
+ {
+ context.DrawImage(source, sourceRect, destRect);
+ }
}
else
{
diff --git a/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml
similarity index 90%
rename from StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml
rename to StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml
index 47ab4045..a18717d9 100644
--- a/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml
@@ -6,6 +6,7 @@
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
+ xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
x:DataType="vmInference:BatchSizeCardViewModel">
@@ -19,7 +20,7 @@
+
diff --git a/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs
new file mode 100644
index 00000000..8a1b364d
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Drawing;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using DynamicData.Binding;
+using StabilityMatrix.Avalonia.ViewModels.Inference;
+using StabilityMatrix.Core.Attributes;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+[Transient]
+public class SelectImageCard : DropTargetTemplatedControlBase
+{
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ if (DataContext is not SelectImageCardViewModel vm)
+ return;
+
+ if (e.NameScope.Find("PART_BetterAdvancedImage") is not { } image)
+ return;
+
+ image
+ .WhenPropertyChanged(x => x.CurrentImage)
+ .Subscribe(propertyValue =>
+ {
+ if (propertyValue.Value?.Size is { } size)
+ {
+ vm.CurrentBitmapSize = new Size(Convert.ToInt32(size.Width), Convert.ToInt32(size.Height));
+ }
+ else
+ {
+ vm.CurrentBitmapSize = Size.Empty;
+ }
+ });
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml
similarity index 100%
rename from StabilityMatrix.Avalonia/Controls/SharpenCard.axaml
rename to StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml
diff --git a/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml.cs
similarity index 100%
rename from StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs
rename to StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml.cs
diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml
similarity index 97%
rename from StabilityMatrix.Avalonia/Controls/StackCard.axaml
rename to StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml
index 03b3c1d9..f2d3f7af 100644
--- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml
@@ -20,7 +20,7 @@
-
+
diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs
similarity index 85%
rename from StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs
rename to StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs
index 2341a96d..ec325ceb 100644
--- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs
+++ b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs
@@ -9,10 +9,10 @@ namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class StackCard : TemplatedControl
{
- public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register<
- StackCard,
- int
- >("Spacing", 8);
+ public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register(
+ "Spacing",
+ 4
+ );
public int Spacing
{
diff --git a/StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
similarity index 83%
rename from StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml
rename to StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
index f89576d8..95f96b76 100644
--- a/StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
@@ -11,7 +11,7 @@
x:DataType="vmInference:StackEditableCardViewModel">
-
+
@@ -58,23 +58,15 @@
Grid.Row="1"
Theme="{StaticResource DraggableListBoxTheme}"
ItemsSource="{Binding Cards}">
-
-
-
-
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml.cs
similarity index 100%
rename from StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml.cs
rename to StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml.cs
diff --git a/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml
new file mode 100644
index 00000000..19af6987
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs
new file mode 100644
index 00000000..0f01c83c
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs
@@ -0,0 +1,41 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using StabilityMatrix.Core.Attributes;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+[Transient]
+public class StackExpander : TemplatedControl
+{
+ public static readonly StyledProperty IsExpandedProperty = Expander
+ .IsExpandedProperty
+ .AddOwner();
+
+ public static readonly StyledProperty ExpandDirectionProperty = Expander
+ .ExpandDirectionProperty
+ .AddOwner();
+
+ public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register(
+ "Spacing",
+ 8
+ );
+
+ public ExpandDirection ExpandDirection
+ {
+ get => GetValue(ExpandDirectionProperty);
+ set => SetValue(ExpandDirectionProperty, value);
+ }
+
+ public bool IsExpanded
+ {
+ get => GetValue(IsExpandedProperty);
+ set => SetValue(IsExpandedProperty, value);
+ }
+
+ public int Spacing
+ {
+ get => GetValue(SpacingProperty);
+ set => SetValue(SpacingProperty, value);
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml
similarity index 100%
rename from StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml
rename to StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml
diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml.cs
similarity index 100%
rename from StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs
rename to StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml.cs
diff --git a/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs b/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs
index efb410da..667b22a5 100644
--- a/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs
+++ b/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs
@@ -12,8 +12,10 @@ public class LineDashFrame : Frame
{
protected override Type StyleKeyOverride { get; } = typeof(Frame);
- public static readonly StyledProperty StrokeProperty =
- AvaloniaProperty.Register("Stroke");
+ public static readonly StyledProperty StrokeProperty = AvaloniaProperty.Register<
+ LineDashFrame,
+ ISolidColorBrush
+ >("Stroke");
public ISolidColorBrush Stroke
{
@@ -21,8 +23,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeProperty, value);
}
- public static readonly StyledProperty StrokeThicknessProperty =
- AvaloniaProperty.Register("StrokeThickness");
+ public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register<
+ LineDashFrame,
+ double
+ >("StrokeThickness");
public double StrokeThickness
{
@@ -30,8 +34,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeThicknessProperty, value);
}
- public static readonly StyledProperty StrokeDashLineProperty =
- AvaloniaProperty.Register("StrokeDashLine");
+ public static readonly StyledProperty StrokeDashLineProperty = AvaloniaProperty.Register<
+ LineDashFrame,
+ double
+ >("StrokeDashLine");
public double StrokeDashLine
{
@@ -39,8 +45,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashLineProperty, value);
}
- public static readonly StyledProperty StrokeDashSpaceProperty =
- AvaloniaProperty.Register("StrokeDashSpace");
+ public static readonly StyledProperty StrokeDashSpaceProperty = AvaloniaProperty.Register<
+ LineDashFrame,
+ double
+ >("StrokeDashSpace");
public double StrokeDashSpace
{
@@ -48,8 +56,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashSpaceProperty, value);
}
- public static readonly StyledProperty FillProperty =
- AvaloniaProperty.Register("Fill");
+ public static readonly StyledProperty FillProperty = AvaloniaProperty.Register<
+ LineDashFrame,
+ ISolidColorBrush
+ >("Fill");
public ISolidColorBrush Fill
{
@@ -82,17 +92,12 @@ public class LineDashFrame : Frame
///
public override void Render(DrawingContext context)
{
- base.Render(context);
-
var width = Bounds.Width;
var height = Bounds.Height;
context.DrawRectangle(Fill, null, new Rect(0, 0, width, height));
- var dashPen = new Pen(Stroke, StrokeThickness)
- {
- DashStyle = new DashStyle(GetDashArray(width), 0)
- };
+ var dashPen = new Pen(Stroke, StrokeThickness) { DashStyle = new DashStyle(GetDashArray(width), 0) };
context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0));
context.DrawLine(dashPen, new Point(0, height), new Point(width, height));
diff --git a/StabilityMatrix.Avalonia/Controls/ProgressRing.cs b/StabilityMatrix.Avalonia/Controls/ProgressRing.cs
index f1a5b56c..cb09bc31 100644
--- a/StabilityMatrix.Avalonia/Controls/ProgressRing.cs
+++ b/StabilityMatrix.Avalonia/Controls/ProgressRing.cs
@@ -1,8 +1,10 @@
-using System.Diagnostics.CodeAnalysis;
+using System;
+using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
namespace StabilityMatrix.Avalonia.Controls;
@@ -13,34 +15,11 @@ namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class ProgressRing : RangeBase
{
- public static readonly StyledProperty IsIndeterminateProperty =
- ProgressBar.IsIndeterminateProperty.AddOwner();
+ private Arc? fillArc;
- public static readonly StyledProperty PreserveAspectProperty =
- AvaloniaProperty.Register(nameof(PreserveAspect), true);
-
- public static readonly StyledProperty ValueAngleProperty =
- AvaloniaProperty.Register(nameof(ValueAngle));
-
- public static readonly StyledProperty StartAngleProperty =
- AvaloniaProperty.Register(nameof(StartAngle));
-
- public static readonly StyledProperty EndAngleProperty =
- AvaloniaProperty.Register(nameof(EndAngle), 360);
-
- static ProgressRing()
- {
- MinimumProperty.Changed.AddClassHandler(OnMinimumPropertyChanged);
- MaximumProperty.Changed.AddClassHandler(OnMaximumPropertyChanged);
- ValueProperty.Changed.AddClassHandler(OnValuePropertyChanged);
- MaximumProperty.Changed.AddClassHandler(OnStartAnglePropertyChanged);
- MaximumProperty.Changed.AddClassHandler(OnEndAnglePropertyChanged);
- }
-
- public ProgressRing()
- {
- UpdatePseudoClasses(IsIndeterminate, PreserveAspect);
- }
+ public static readonly StyledProperty IsIndeterminateProperty = ProgressBar
+ .IsIndeterminateProperty
+ .AddOwner();
public bool IsIndeterminate
{
@@ -48,35 +27,90 @@ public class ProgressRing : RangeBase
set => SetValue(IsIndeterminateProperty, value);
}
+ public static readonly StyledProperty PreserveAspectProperty = AvaloniaProperty.Register(
+ nameof(PreserveAspect),
+ true
+ );
+
public bool PreserveAspect
{
get => GetValue(PreserveAspectProperty);
set => SetValue(PreserveAspectProperty, value);
}
- public double ValueAngle
+ public static readonly StyledProperty StrokeThicknessProperty = Shape
+ .StrokeThicknessProperty
+ .AddOwner();
+
+ public double StrokeThickness
{
- get => GetValue(ValueAngleProperty);
- private set => SetValue(ValueAngleProperty, value);
+ get => GetValue(StrokeThicknessProperty);
+ set => SetValue(StrokeThicknessProperty, value);
}
+ public static readonly StyledProperty StartAngleProperty = AvaloniaProperty.Register(
+ nameof(StartAngle)
+ );
+
public double StartAngle
{
get => GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
}
+ public static readonly StyledProperty SweepAngleProperty = AvaloniaProperty.Register(
+ nameof(SweepAngle)
+ );
+
+ public double SweepAngle
+ {
+ get => GetValue(SweepAngleProperty);
+ set => SetValue(SweepAngleProperty, value);
+ }
+
+ public static readonly StyledProperty EndAngleProperty = AvaloniaProperty.Register(
+ nameof(EndAngle),
+ 360
+ );
+
public double EndAngle
{
get => GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
+ static ProgressRing()
+ {
+ AffectsRender(SweepAngleProperty, StartAngleProperty, EndAngleProperty);
+
+ ValueProperty.Changed.AddClassHandler(OnValuePropertyChanged);
+ SweepAngleProperty.Changed.AddClassHandler(OnSweepAnglePropertyChanged);
+ }
+
+ public ProgressRing()
+ {
+ UpdatePseudoClasses(IsIndeterminate, PreserveAspect);
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ fillArc = e.NameScope.Find("PART_Fill");
+ if (fillArc is not null)
+ {
+ fillArc.StartAngle = StartAngle;
+ fillArc.SweepAngle = SweepAngle;
+ }
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
var e = change as AvaloniaPropertyChangedEventArgs;
- if (e is null) return;
+ if (e is null)
+ return;
if (e.Property == IsIndeterminateProperty)
{
@@ -88,9 +122,7 @@ public class ProgressRing : RangeBase
}
}
- private void UpdatePseudoClasses(
- bool? isIndeterminate,
- bool? preserveAspect)
+ private void UpdatePseudoClasses(bool? isIndeterminate, bool? preserveAspect)
{
if (isIndeterminate.HasValue)
{
@@ -103,28 +135,19 @@ public class ProgressRing : RangeBase
}
}
- private static void OnMinimumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
- {
- sender.Minimum = (double) e.NewValue!;
- }
-
- private static void OnMaximumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
- {
- sender.Maximum = (double) e.NewValue!;
- }
-
private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
- sender.ValueAngle = ((double) e.NewValue! - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum);
- }
-
- private static void OnStartAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
- {
- sender.StartAngle = (double) e.NewValue!;
+ sender.SweepAngle =
+ ((double)e.NewValue! - sender.Minimum)
+ * (sender.EndAngle - sender.StartAngle)
+ / (sender.Maximum - sender.Minimum);
}
- private static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
+ private static void OnSweepAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
- sender.EndAngle = (double) e.NewValue!;
+ if (sender.fillArc is { } arc)
+ {
+ arc.SweepAngle = Math.Round(e.GetNewValue());
+ }
}
}
diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs
new file mode 100644
index 00000000..a03243fb
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.PropertyGrid.Services;
+using JetBrains.Annotations;
+using PropertyModels.ComponentModel;
+using StabilityMatrix.Core.Extensions;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+///
+[PublicAPI]
+public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid
+{
+ protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid);
+
+ public static readonly StyledProperty> ExcludedCategoriesProperty = AvaloniaProperty.Register<
+ BetterPropertyGrid,
+ IEnumerable
+ >("ExcludedCategories");
+
+ public IEnumerable ExcludedCategories
+ {
+ get => GetValue(ExcludedCategoriesProperty);
+ set => SetValue(ExcludedCategoriesProperty, value);
+ }
+
+ public static readonly StyledProperty> IncludedCategoriesProperty = AvaloniaProperty.Register<
+ BetterPropertyGrid,
+ IEnumerable
+ >("IncludedCategories");
+
+ public IEnumerable IncludedCategories
+ {
+ get => GetValue(IncludedCategoriesProperty);
+ set => SetValue(IncludedCategoriesProperty, value);
+ }
+
+ static BetterPropertyGrid()
+ {
+ // Register factories
+ CellEditFactoryService.Default.AddFactory(new ToggleSwitchCellEditFactory());
+
+ // Initialize localization and name resolver
+ LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService());
+
+ ExcludedCategoriesProperty
+ .Changed
+ .AddClassHandler(
+ (grid, args) =>
+ {
+ if (args.NewValue is IEnumerable excludedCategories)
+ {
+ grid.FilterExcludeCategories(excludedCategories);
+ }
+ }
+ );
+
+ IncludedCategoriesProperty
+ .Changed
+ .AddClassHandler(
+ (grid, args) =>
+ {
+ if (args.NewValue is IEnumerable includedCategories)
+ {
+ grid.FilterIncludeCategories(includedCategories);
+ }
+ }
+ );
+ }
+
+ public void FilterExcludeCategories(IEnumerable excludedCategories)
+ {
+ // Get internal property `ViewModel` of internal type `PropertyGridViewModel`
+ var gridVm = this.GetProtectedProperty("ViewModel")!;
+ // Get public property `CategoryFilter`
+ var categoryFilter = gridVm.GetProtectedProperty("CategoryFilter")!;
+
+ categoryFilter.BeginUpdate();
+
+ // Uncheck All, then check all except All
+ categoryFilter.UnCheck(categoryFilter.All);
+
+ foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All))
+ {
+ categoryFilter.Check(mask);
+ }
+
+ // Uncheck excluded categories
+ foreach (var mask in excludedCategories)
+ {
+ categoryFilter.UnCheck(mask);
+ }
+
+ categoryFilter.EndUpdate();
+ }
+
+ public void FilterIncludeCategories(IEnumerable includeCategories)
+ {
+ // Get internal property `ViewModel` of internal type `PropertyGridViewModel`
+ var gridVm = this.GetProtectedProperty("ViewModel")!;
+ // Get public property `CategoryFilter`
+ var categoryFilter = gridVm.GetProtectedProperty("CategoryFilter")!;
+
+ categoryFilter.BeginUpdate();
+
+ // Uncheck non-included categories
+ foreach (var mask in categoryFilter.Masks.Where(m => !includeCategories.Contains(m)))
+ {
+ categoryFilter.UnCheck(mask);
+ }
+
+ categoryFilter.UnCheck(categoryFilter.All);
+
+ // Check included categories
+ foreach (var mask in includeCategories)
+ {
+ categoryFilter.Check(mask);
+ }
+
+ categoryFilter.EndUpdate();
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs
new file mode 100644
index 00000000..d074dd06
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Globalization;
+using PropertyModels.Localilzation;
+using StabilityMatrix.Avalonia.Languages;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+internal class PropertyGridCultureData : ICultureData
+{
+ ///
+ public bool Reload() => false;
+
+ ///
+ public CultureInfo Culture => Cultures.Current ?? Cultures.Default;
+
+ ///
+ public Uri Path => new("");
+
+ ///
+ public string this[string key]
+ {
+ get
+ {
+ if (Resources.ResourceManager.GetString(key) is { } result)
+ {
+ return result;
+ }
+
+ return key;
+ }
+ }
+
+ ///
+ public bool IsLoaded => true;
+}
diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs
new file mode 100644
index 00000000..dfc27ade
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs
@@ -0,0 +1,36 @@
+using System;
+using PropertyModels.ComponentModel;
+using PropertyModels.Localilzation;
+using StabilityMatrix.Avalonia.Languages;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+///
+/// Implements using static .
+///
+internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService
+{
+ ///
+ public ICultureData CultureData { get; } = new PropertyGridCultureData();
+
+ ///
+ public string this[string key] => CultureData[key];
+
+ ///
+ public event EventHandler? OnCultureChanged;
+
+ ///
+ public ILocalizationService[] GetExtraServices() => Array.Empty();
+
+ ///
+ public void AddExtraService(ILocalizationService service) { }
+
+ ///
+ public void RemoveExtraService(ILocalizationService service) { }
+
+ ///
+ public ICultureData[] GetCultures() => new[] { CultureData };
+
+ ///
+ public void SelectCulture(string cultureName) { }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs
new file mode 100644
index 00000000..28232635
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs
@@ -0,0 +1,60 @@
+using Avalonia.Controls;
+using Avalonia.PropertyGrid.Controls;
+using Avalonia.PropertyGrid.Controls.Factories;
+using Avalonia.PropertyGrid.Localization;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+internal class ToggleSwitchCellEditFactory : AbstractCellEditFactory
+{
+ // make this extend factor only effect on TestExtendPropertyGrid
+ public override bool Accept(object accessToken)
+ {
+ return accessToken is BetterPropertyGrid;
+ }
+
+ public override Control? HandleNewProperty(PropertyCellContext context)
+ {
+ var propertyDescriptor = context.Property;
+ var target = context.Target;
+
+ if (propertyDescriptor.PropertyType != typeof(bool))
+ {
+ return null;
+ }
+
+ var control = new ToggleSwitch();
+ control.SetLocalizeBinding(ToggleSwitch.OnContentProperty, "On");
+ control.SetLocalizeBinding(ToggleSwitch.OffContentProperty, "Off");
+
+ control.IsCheckedChanged += (s, e) =>
+ {
+ SetAndRaise(context, control, control.IsChecked);
+ };
+
+ return control;
+ }
+
+ public override bool HandlePropertyChanged(PropertyCellContext context)
+ {
+ var propertyDescriptor = context.Property;
+ var target = context.Target;
+ var control = context.CellEdit;
+
+ if (propertyDescriptor.PropertyType != typeof(bool))
+ {
+ return false;
+ }
+
+ ValidateProperty(control, propertyDescriptor, target);
+
+ if (control is ToggleSwitch ts)
+ {
+ ts.IsChecked = (bool)(propertyDescriptor.GetValue(target) ?? false);
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml
deleted file mode 100644
index b6dd0a2b..00000000
--- a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs
deleted file mode 100644
index 726cf414..00000000
--- a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using StabilityMatrix.Core.Attributes;
-
-namespace StabilityMatrix.Avalonia.Controls;
-
-[Transient]
-public class SelectImageCard : DropTargetTemplatedControlBase { }
diff --git a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml b/StabilityMatrix.Avalonia/Controls/StackExpander.axaml
deleted file mode 100644
index 9e7af392..00000000
--- a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs b/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
deleted file mode 100644
index 38208891..00000000
--- a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using Avalonia;
-using Avalonia.Controls.Primitives;
-using StabilityMatrix.Core.Attributes;
-
-namespace StabilityMatrix.Avalonia.Controls;
-
-[Transient]
-public class StackExpander : TemplatedControl
-{
- public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register<
- StackCard,
- int
- >("Spacing", 8);
-
- public int Spacing
- {
- get => GetValue(SpacingProperty);
- set => SetValue(SpacingProperty, value);
- }
-}
diff --git a/StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs b/StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs
new file mode 100644
index 00000000..3eaf7090
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace StabilityMatrix.Avalonia.Converters;
+
+public class CustomStringFormatConverter([StringSyntax("CompositeFormat")] string format) : IValueConverter
+ where T : IFormatProvider, new()
+{
+ ///
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value is null ? null : string.Format(new T(), format, value);
+ }
+
+ ///
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value is null ? null : throw new NotImplementedException();
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs b/StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs
new file mode 100644
index 00000000..086b1133
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace StabilityMatrix.Avalonia.Converters;
+
+///
+/// Converts an index to index + 1
+///
+public class IndexPlusOneConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is int i)
+ {
+ return i + 1;
+ }
+
+ return value;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is int i)
+ {
+ return i - 1;
+ }
+
+ return value;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs b/StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs
new file mode 100644
index 00000000..18dba05f
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs
@@ -0,0 +1,41 @@
+using System;
+using Size = StabilityMatrix.Core.Helper.Size;
+
+namespace StabilityMatrix.Avalonia.Converters;
+
+public class MemoryBytesFormatter : ICustomFormatter, IFormatProvider
+{
+ ///
+ public object? GetFormat(Type? formatType)
+ {
+ return formatType == typeof(ICustomFormatter) ? this : null;
+ }
+
+ ///
+ public string Format(string? format, object? arg, IFormatProvider? formatProvider)
+ {
+ if (format == null || !format.Trim().StartsWith('M'))
+ {
+ if (arg is IFormattable formatArg)
+ {
+ return formatArg.ToString(format, formatProvider);
+ }
+
+ return arg?.ToString() ?? string.Empty;
+ }
+
+ var value = Convert.ToUInt64(arg);
+
+ var result = format.Trim().EndsWith("10", StringComparison.OrdinalIgnoreCase)
+ ? Size.FormatBase10Bytes(value)
+ : Size.FormatBytes(value);
+
+ // Strip i if not Mi
+ if (!format.Trim().Contains('I', StringComparison.OrdinalIgnoreCase))
+ {
+ result = result.Replace("i", string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return result;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverter.cs b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverter.cs
new file mode 100644
index 00000000..43379900
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverter.cs
@@ -0,0 +1,84 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Numerics;
+using Avalonia.Data.Converters;
+
+namespace StabilityMatrix.Avalonia.Converters;
+
+///
+/// Converts a possibly boxed nullable value type to its default value
+///
+public class NullableDefaultNumericConverter : IValueConverter
+ where TSource : unmanaged, INumber
+ where TTarget : unmanaged, INumber
+{
+ public ReturnBehavior NanHandling { get; set; } = ReturnBehavior.DefaultValue;
+
+ ///
+ /// Unboxes a nullable value type
+ ///
+ private TSource Unbox(TTarget? value)
+ {
+ if (!value.HasValue)
+ {
+ return default;
+ }
+
+ if (TTarget.IsNaN(value.Value))
+ {
+ return NanHandling switch
+ {
+ ReturnBehavior.DefaultValue => default,
+ ReturnBehavior.Throw => throw new InvalidCastException("Cannot convert NaN to a numeric type"),
+ _
+ => throw new InvalidEnumArgumentException(
+ nameof(NanHandling),
+ (int)NanHandling,
+ typeof(ReturnBehavior)
+ )
+ };
+ }
+
+ return (TSource)System.Convert.ChangeType(value.Value, typeof(TSource));
+ }
+
+ ///
+ /// Convert a value type to a nullable value type
+ ///
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (targetType != typeof(TTarget?) && !targetType.IsAssignableTo(typeof(TTarget)))
+ {
+ // ReSharper disable once LocalizableElement
+ throw new ArgumentException(
+ $"Convert Target type {targetType.Name} must be assignable to {typeof(TTarget).Name}"
+ );
+ }
+
+ return (TTarget?)System.Convert.ChangeType(value, typeof(TTarget));
+ }
+
+ ///
+ /// Convert a nullable value type to a value type
+ ///
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (!targetType.IsAssignableTo(typeof(TSource)))
+ {
+ // ReSharper disable once LocalizableElement
+ throw new ArgumentException(
+ $"ConvertBack Target type {targetType.Name} must be assignable to {typeof(TSource).Name}"
+ );
+ }
+
+ return Unbox((TTarget?)value);
+ }
+
+ public enum ReturnBehavior
+ {
+ DefaultValue,
+ Throw
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverters.cs b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverters.cs
new file mode 100644
index 00000000..662b2601
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverters.cs
@@ -0,0 +1,6 @@
+namespace StabilityMatrix.Avalonia.Converters;
+
+public static class NullableDefaultNumericConverters
+{
+ public static readonly NullableDefaultNumericConverter IntToDecimal = new();
+}
diff --git a/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs b/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs
index e4ed2455..6402747f 100644
--- a/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs
+++ b/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs
@@ -1,4 +1,5 @@
-using Avalonia.Data.Converters;
+using System;
+using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
@@ -7,4 +8,9 @@ public static class StringFormatConverters
private static StringFormatValueConverter? _decimalConverter;
public static StringFormatValueConverter Decimal =>
_decimalConverter ??= new StringFormatValueConverter("{0:D}", null);
+
+ private static readonly Lazy MemoryBytesConverterLazy =
+ new(() => new CustomStringFormatConverter("{0:M}"));
+
+ public static IValueConverter MemoryBytes => MemoryBytesConverterLazy.Value;
}
diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
index 87de1f7c..469ff676 100644
--- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs
+++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
+using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
@@ -21,11 +23,11 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
-using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Avalonia.ViewModels.Inference.Video;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
+using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@@ -43,6 +45,8 @@ using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
+using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
+using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.HuggingFacePageViewModel;
namespace StabilityMatrix.Avalonia.DesignData;
@@ -77,10 +81,7 @@ public static class DesignData
Id = activePackageId,
DisplayName = "My Installed Package",
PackageName = "stable-diffusion-webui",
- Version = new InstalledPackageVersion
- {
- InstalledReleaseVersion = "v1.0.0"
- },
+ Version = new InstalledPackageVersion { InstalledReleaseVersion = "v1.0.0" },
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
@@ -92,8 +93,7 @@ public static class DesignData
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
- InstalledCommitSha =
- "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
+ InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
@@ -126,7 +126,8 @@ public static class DesignData
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton();
+ .AddSingleton()
+ .AddSingleton();
// Placeholder services that nobody should need during design time
services
@@ -149,6 +150,7 @@ public static class DesignData
var modelFinder = Services.GetRequiredService();
var packageFactory = Services.GetRequiredService();
var notificationService = Services.GetRequiredService();
+ var modelImportService = Services.GetRequiredService();
LaunchOptionsViewModel = Services.GetRequiredService();
LaunchOptionsViewModel.Cards = new[]
@@ -185,7 +187,7 @@ public static class DesignData
CheckpointsPageViewModel.CheckpointFoldersCache,
new CheckpointFolder[]
{
- new(settingsManager, downloadService, modelFinder, notificationService)
+ new(settingsManager, downloadService, modelFinder, notificationService, modelImportService)
{
DirectoryPath = "Models/StableDiffusion",
DisplayedCheckpointFiles = new ObservableCollectionExtended()
@@ -212,14 +214,10 @@ public static class DesignData
TrainedWords = ["aurora", "lightning"]
}
},
- new()
- {
- FilePath = "~/Models/Lora/model.safetensors",
- Title = "Some model"
- },
+ new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" },
},
},
- new(settingsManager, downloadService, modelFinder, notificationService)
+ new(settingsManager, downloadService, modelFinder, notificationService, modelImportService)
{
Title = "Lora",
DirectoryPath = "Packages/Lora",
@@ -300,56 +298,55 @@ public static class DesignData
);
}*/
- CheckpointBrowserViewModel.ModelCards =
- new ObservableCollection
+ CivitAiBrowserViewModel.ModelCards = new ObservableCollection
+ {
+ dialogFactory.Get(vm =>
{
- dialogFactory.Get