JT
11 months ago
committed by
GitHub
177 changed files with 7668 additions and 1512 deletions
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
<PropertyGroup> |
||||
<TargetFramework>net8.0</TargetFramework> |
||||
<LangVersion>latest</LangVersion> |
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
<Nullable>enable</Nullable> |
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> |
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting> |
||||
</PropertyGroup> |
||||
<ItemGroup> |
||||
<PackageReference Include="Avalonia" Version="11.0.5" /> |
||||
<PackageReference Include="SkiaSharp" Version="2.88.6" /> |
||||
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,10 @@
|
||||
namespace Avalonia.Gif |
||||
{ |
||||
internal enum BgWorkerCommand |
||||
{ |
||||
Null, |
||||
Play, |
||||
Pause, |
||||
Dispose |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
namespace Avalonia.Gif |
||||
{ |
||||
internal enum BgWorkerState |
||||
{ |
||||
Null, |
||||
Start, |
||||
Running, |
||||
Paused, |
||||
Complete, |
||||
Dispose |
||||
} |
||||
} |
@ -0,0 +1,10 @@
|
||||
namespace Avalonia.Gif.Decoding |
||||
{ |
||||
internal enum BlockTypes |
||||
{ |
||||
Empty = 0, |
||||
Extension = 0x21, |
||||
ImageDescriptor = 0x2C, |
||||
Trailer = 0x3B, |
||||
} |
||||
} |
@ -0,0 +1,8 @@
|
||||
namespace Avalonia.Gif.Decoding |
||||
{ |
||||
internal enum ExtensionType |
||||
{ |
||||
GraphicsControl = 0xF9, |
||||
Application = 0xFF |
||||
} |
||||
} |
@ -0,0 +1,10 @@
|
||||
namespace Avalonia.Gif.Decoding |
||||
{ |
||||
public enum FrameDisposal |
||||
{ |
||||
Unknown = 0, |
||||
Leave = 1, |
||||
Background = 2, |
||||
Restore = 3 |
||||
} |
||||
} |
@ -0,0 +1,36 @@
|
||||
using System.Runtime.InteropServices; |
||||
|
||||
namespace Avalonia.Gif |
||||
{ |
||||
[StructLayout(LayoutKind.Explicit)] |
||||
public readonly struct GifColor |
||||
{ |
||||
[FieldOffset(3)] |
||||
public readonly byte A; |
||||
|
||||
[FieldOffset(2)] |
||||
public readonly byte R; |
||||
|
||||
[FieldOffset(1)] |
||||
public readonly byte G; |
||||
|
||||
[FieldOffset(0)] |
||||
public readonly byte B; |
||||
|
||||
/// <summary> |
||||
/// A struct that represents a ARGB color and is aligned as |
||||
/// a BGRA bytefield in memory. |
||||
/// </summary> |
||||
/// <param name="r">Red</param> |
||||
/// <param name="g">Green</param> |
||||
/// <param name="b">Blue</param> |
||||
/// <param name="a">Alpha</param> |
||||
public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue) |
||||
{ |
||||
A = a; |
||||
R = r; |
||||
G = g; |
||||
B = b; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,653 @@
|
||||
// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer |
||||
// as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer) |
||||
// Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) |
||||
// Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved. |
||||
|
||||
// The rest of the source file is licensed under MIT License. |
||||
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. |
||||
|
||||
using System; |
||||
using System.Buffers; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Runtime.InteropServices; |
||||
using System.Text; |
||||
using System.Threading; |
||||
using Avalonia; |
||||
using Avalonia.Media.Imaging; |
||||
using static Avalonia.Gif.Extensions.StreamExtensions; |
||||
|
||||
namespace Avalonia.Gif.Decoding |
||||
{ |
||||
public sealed class GifDecoder : IDisposable |
||||
{ |
||||
private static readonly ReadOnlyMemory<byte> G87AMagic = "GIF87a"u8.ToArray().AsMemory(); |
||||
|
||||
private static readonly ReadOnlyMemory<byte> G89AMagic = "GIF89a"u8.ToArray().AsMemory(); |
||||
|
||||
private static readonly ReadOnlyMemory<byte> NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory(); |
||||
|
||||
private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10); |
||||
private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100); |
||||
private static readonly GifColor TransparentColor = new(0, 0, 0, 0); |
||||
private static readonly int MaxTempBuf = 768; |
||||
private static readonly int MaxStackSize = 4096; |
||||
private static readonly int MaxBits = 4097; |
||||
|
||||
private readonly Stream _fileStream; |
||||
private readonly CancellationToken _currentCtsToken; |
||||
private readonly bool _hasFrameBackups; |
||||
|
||||
private int _gctSize, |
||||
_bgIndex, |
||||
_prevFrame = -1, |
||||
_backupFrame = -1; |
||||
private bool _gctUsed; |
||||
|
||||
private GifRect _gifDimensions; |
||||
|
||||
// private ulong _globalColorTable; |
||||
private readonly int _backBufferBytes; |
||||
private GifColor[] _bitmapBackBuffer; |
||||
|
||||
private short[] _prefixBuf; |
||||
private byte[] _suffixBuf; |
||||
private byte[] _pixelStack; |
||||
private byte[] _indexBuf; |
||||
private byte[] _backupFrameIndexBuf; |
||||
private volatile bool _hasNewFrame; |
||||
|
||||
public GifHeader Header { get; private set; } |
||||
|
||||
public readonly List<GifFrame> Frames = new(); |
||||
|
||||
public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height); |
||||
|
||||
public GifDecoder(Stream fileStream, CancellationToken currentCtsToken) |
||||
{ |
||||
_fileStream = fileStream; |
||||
_currentCtsToken = currentCtsToken; |
||||
|
||||
ProcessHeaderData(); |
||||
ProcessFrameData(); |
||||
|
||||
Header.IterationCount = Header.Iterations switch |
||||
{ |
||||
-1 => new GifRepeatBehavior { Count = 1 }, |
||||
0 => new GifRepeatBehavior { LoopForever = true }, |
||||
> 0 => new GifRepeatBehavior { Count = Header.Iterations }, |
||||
_ => Header.IterationCount |
||||
}; |
||||
|
||||
var pixelCount = _gifDimensions.TotalPixels; |
||||
|
||||
_hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore); |
||||
|
||||
_bitmapBackBuffer = new GifColor[pixelCount]; |
||||
_indexBuf = new byte[pixelCount]; |
||||
|
||||
if (_hasFrameBackups) |
||||
_backupFrameIndexBuf = new byte[pixelCount]; |
||||
|
||||
_prefixBuf = new short[MaxStackSize]; |
||||
_suffixBuf = new byte[MaxStackSize]; |
||||
_pixelStack = new byte[MaxStackSize + 1]; |
||||
|
||||
_backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor)); |
||||
} |
||||
|
||||
public void Dispose() |
||||
{ |
||||
Frames.Clear(); |
||||
|
||||
_bitmapBackBuffer = null; |
||||
_prefixBuf = null; |
||||
_suffixBuf = null; |
||||
_pixelStack = null; |
||||
_indexBuf = null; |
||||
_backupFrameIndexBuf = null; |
||||
} |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
private int PixCoord(int x, int y) => x + y * _gifDimensions.Width; |
||||
|
||||
static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) }; |
||||
|
||||
private void ClearImage() |
||||
{ |
||||
Array.Fill(_bitmapBackBuffer, TransparentColor); |
||||
//ClearArea(_gifDimensions); |
||||
|
||||
_prevFrame = -1; |
||||
_backupFrame = -1; |
||||
} |
||||
|
||||
public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false) |
||||
{ |
||||
if (_currentCtsToken.IsCancellationRequested) |
||||
return; |
||||
|
||||
if (fIndex < 0 | fIndex >= Frames.Count) |
||||
return; |
||||
|
||||
if (_prevFrame == fIndex) |
||||
return; |
||||
|
||||
if (fIndex == 0 || forceClear || fIndex < _prevFrame) |
||||
ClearImage(); |
||||
|
||||
DisposePreviousFrame(); |
||||
|
||||
_prevFrame++; |
||||
|
||||
// render intermediate frame |
||||
for (int idx = _prevFrame; idx < fIndex; ++idx) |
||||
{ |
||||
var prevFrame = Frames[idx]; |
||||
|
||||
if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore) |
||||
continue; |
||||
|
||||
if (prevFrame.FrameDisposalMethod == FrameDisposal.Background) |
||||
{ |
||||
ClearArea(prevFrame.Dimensions); |
||||
continue; |
||||
} |
||||
|
||||
RenderFrameAt(idx, writeableBitmap); |
||||
} |
||||
|
||||
RenderFrameAt(fIndex, writeableBitmap); |
||||
} |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap) |
||||
{ |
||||
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf); |
||||
|
||||
var curFrame = Frames[idx]; |
||||
DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB); |
||||
|
||||
if (_hasFrameBackups & curFrame.ShouldBackup) |
||||
{ |
||||
Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels); |
||||
_backupFrame = idx; |
||||
} |
||||
|
||||
DrawFrame(curFrame, _indexBuf); |
||||
|
||||
_prevFrame = idx; |
||||
_hasNewFrame = true; |
||||
|
||||
using var lockedBitmap = writeableBitmap.Lock(); |
||||
WriteBackBufToFb(lockedBitmap.Address); |
||||
|
||||
ArrayPool<byte>.Shared.Return(tmpB); |
||||
} |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
private void DrawFrame(GifFrame curFrame, Memory<byte> frameIndexSpan) |
||||
{ |
||||
var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable; |
||||
|
||||
var cX = curFrame.Dimensions.X; |
||||
var cY = curFrame.Dimensions.Y; |
||||
var cH = curFrame.Dimensions.Height; |
||||
var cW = curFrame.Dimensions.Width; |
||||
var tC = curFrame.TransparentColorIndex; |
||||
var hT = curFrame.HasTransparency; |
||||
|
||||
if (curFrame.IsInterlaced) |
||||
{ |
||||
for (var i = 0; i < 4; i++) |
||||
{ |
||||
var curPass = Pass[i]; |
||||
var y = curPass.Start; |
||||
while (y < cH) |
||||
{ |
||||
DrawRow(y); |
||||
y += curPass.Step; |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
for (var i = 0; i < cH; i++) |
||||
DrawRow(i); |
||||
} |
||||
|
||||
//for (var row = 0; row < cH; row++) |
||||
void DrawRow(int row) |
||||
{ |
||||
// Get the starting point of the current row on frame's index stream. |
||||
var indexOffset = row * cW; |
||||
|
||||
// Get the target backbuffer offset from the frames coords. |
||||
var targetOffset = PixCoord(cX, row + cY); |
||||
var len = _bitmapBackBuffer.Length; |
||||
|
||||
for (var i = 0; i < cW; i++) |
||||
{ |
||||
var indexColor = frameIndexSpan.Span[indexOffset + i]; |
||||
|
||||
if (activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length) |
||||
return; |
||||
|
||||
if (!(hT & indexColor == tC)) |
||||
_bitmapBackBuffer[targetOffset] = activeColorTable[indexColor]; |
||||
|
||||
targetOffset++; |
||||
} |
||||
} |
||||
} |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
private void DisposePreviousFrame() |
||||
{ |
||||
if (_prevFrame == -1) |
||||
return; |
||||
|
||||
var prevFrame = Frames[_prevFrame]; |
||||
|
||||
switch (prevFrame.FrameDisposalMethod) |
||||
{ |
||||
case FrameDisposal.Background: |
||||
ClearArea(prevFrame.Dimensions); |
||||
break; |
||||
case FrameDisposal.Restore: |
||||
if (_hasFrameBackups && _backupFrame != -1) |
||||
DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf); |
||||
else |
||||
ClearArea(prevFrame.Dimensions); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
private void ClearArea(GifRect area) |
||||
{ |
||||
for (var y = 0; y < area.Height; y++) |
||||
{ |
||||
var targetOffset = PixCoord(area.X, y + area.Y); |
||||
for (var x = 0; x < area.Width; x++) |
||||
_bitmapBackBuffer[targetOffset + x] = TransparentColor; |
||||
} |
||||
} |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span<byte> indexSpan, byte[] tempBuf) |
||||
{ |
||||
_fileStream.Position = curFrame.LzwStreamPosition; |
||||
var totalPixels = curFrame.Dimensions.TotalPixels; |
||||
|
||||
// Initialize GIF data stream decoder. |
||||
var dataSize = curFrame.LzwMinCodeSize; |
||||
var clear = 1 << dataSize; |
||||
var endOfInformation = clear + 1; |
||||
var available = clear + 2; |
||||
var oldCode = -1; |
||||
var codeSize = dataSize + 1; |
||||
var codeMask = (1 << codeSize) - 1; |
||||
|
||||
for (var code = 0; code < clear; code++) |
||||
{ |
||||
_prefixBuf[code] = 0; |
||||
_suffixBuf[code] = (byte)code; |
||||
} |
||||
|
||||
// Decode GIF pixel stream. |
||||
int bits, |
||||
first, |
||||
top, |
||||
pixelIndex; |
||||
var datum = bits = first = top = pixelIndex = 0; |
||||
|
||||
while (pixelIndex < totalPixels) |
||||
{ |
||||
var blockSize = _fileStream.ReadBlock(tempBuf); |
||||
|
||||
if (blockSize == 0) |
||||
break; |
||||
|
||||
var blockPos = 0; |
||||
|
||||
while (blockPos < blockSize) |
||||
{ |
||||
datum += tempBuf[blockPos] << bits; |
||||
blockPos++; |
||||
|
||||
bits += 8; |
||||
|
||||
while (bits >= codeSize) |
||||
{ |
||||
// Get the next code. |
||||
var code = datum & codeMask; |
||||
datum >>= codeSize; |
||||
bits -= codeSize; |
||||
|
||||
// Interpret the code |
||||
if (code == clear) |
||||
{ |
||||
// Reset decoder. |
||||
codeSize = dataSize + 1; |
||||
codeMask = (1 << codeSize) - 1; |
||||
available = clear + 2; |
||||
oldCode = -1; |
||||
continue; |
||||
} |
||||
|
||||
// Check for explicit end-of-stream |
||||
if (code == endOfInformation) |
||||
return; |
||||
|
||||
if (oldCode == -1) |
||||
{ |
||||
indexSpan[pixelIndex++] = _suffixBuf[code]; |
||||
oldCode = code; |
||||
first = code; |
||||
continue; |
||||
} |
||||
|
||||
var inCode = code; |
||||
if (code >= available) |
||||
{ |
||||
_pixelStack[top++] = (byte)first; |
||||
code = oldCode; |
||||
|
||||
if (top == MaxBits) |
||||
ThrowException(); |
||||
} |
||||
|
||||
while (code >= clear) |
||||
{ |
||||
if (code >= MaxBits || code == _prefixBuf[code]) |
||||
ThrowException(); |
||||
|
||||
_pixelStack[top++] = _suffixBuf[code]; |
||||
code = _prefixBuf[code]; |
||||
|
||||
if (top == MaxBits) |
||||
ThrowException(); |
||||
} |
||||
|
||||
first = _suffixBuf[code]; |
||||
_pixelStack[top++] = (byte)first; |
||||
|
||||
// Add new code to the dictionary |
||||
if (available < MaxStackSize) |
||||
{ |
||||
_prefixBuf[available] = (short)oldCode; |
||||
_suffixBuf[available] = (byte)first; |
||||
available++; |
||||
|
||||
if ((available & codeMask) == 0 && available < MaxStackSize) |
||||
{ |
||||
codeSize++; |
||||
codeMask += available; |
||||
} |
||||
} |
||||
|
||||
oldCode = inCode; |
||||
|
||||
// Drain the pixel stack. |
||||
do |
||||
{ |
||||
indexSpan[pixelIndex++] = _pixelStack[--top]; |
||||
} while (top > 0); |
||||
} |
||||
} |
||||
} |
||||
|
||||
while (pixelIndex < totalPixels) |
||||
indexSpan[pixelIndex++] = 0; // clear missing pixels |
||||
|
||||
void ThrowException() => throw new LzwDecompressionException(); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Directly copies the <see cref="GifColor"/> struct array to a bitmap IntPtr. |
||||
/// </summary> |
||||
private void WriteBackBufToFb(IntPtr targetPointer) |
||||
{ |
||||
if (_currentCtsToken.IsCancellationRequested) |
||||
return; |
||||
|
||||
if (!(_hasNewFrame & _bitmapBackBuffer != null)) |
||||
return; |
||||
|
||||
unsafe |
||||
{ |
||||
fixed (void* src = &_bitmapBackBuffer[0]) |
||||
Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes); |
||||
_hasNewFrame = false; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Processes GIF Header. |
||||
/// </summary> |
||||
private void ProcessHeaderData() |
||||
{ |
||||
var str = _fileStream; |
||||
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf); |
||||
var tempBuf = tmpB.AsSpan(); |
||||
|
||||
var _ = str.Read(tmpB, 0, 6); |
||||
|
||||
if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span)) |
||||
throw new InvalidGifStreamException("Not a GIF stream."); |
||||
|
||||
if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span))) |
||||
throw new InvalidGifStreamException( |
||||
"Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray()) |
||||
); |
||||
|
||||
ProcessScreenDescriptor(tmpB); |
||||
|
||||
Header = new GifHeader |
||||
{ |
||||
Dimensions = _gifDimensions, |
||||
HasGlobalColorTable = _gctUsed, |
||||
// GlobalColorTableCacheID = _globalColorTable, |
||||
GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize), |
||||
GlobalColorTableSize = _gctSize, |
||||
BackgroundColorIndex = _bgIndex, |
||||
HeaderSize = _fileStream.Position |
||||
}; |
||||
|
||||
ArrayPool<byte>.Shared.Return(tmpB); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Parses colors from file stream to target color table. |
||||
/// </summary> |
||||
private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors) |
||||
{ |
||||
var nBytes = 3 * nColors; |
||||
var target = new GifColor[nColors]; |
||||
|
||||
var n = stream.Read(rawBufSpan, 0, nBytes); |
||||
|
||||
if (n < nBytes) |
||||
throw new InvalidOperationException("Wrong color table bytes."); |
||||
|
||||
int i = 0, |
||||
j = 0; |
||||
|
||||
while (i < nColors) |
||||
{ |
||||
var r = rawBufSpan[j++]; |
||||
var g = rawBufSpan[j++]; |
||||
var b = rawBufSpan[j++]; |
||||
target[i++] = new GifColor(r, g, b); |
||||
} |
||||
|
||||
return target; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Parses screen and other GIF descriptors. |
||||
/// </summary> |
||||
private void ProcessScreenDescriptor(byte[] tempBuf) |
||||
{ |
||||
var width = _fileStream.ReadUShortS(tempBuf); |
||||
var height = _fileStream.ReadUShortS(tempBuf); |
||||
|
||||
var packed = _fileStream.ReadByteS(tempBuf); |
||||
|
||||
_gctUsed = (packed & 0x80) != 0; |
||||
_gctSize = 2 << (packed & 7); |
||||
_bgIndex = _fileStream.ReadByteS(tempBuf); |
||||
|
||||
_gifDimensions = new GifRect(0, 0, width, height); |
||||
_fileStream.Skip(1); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Parses all frame data. |
||||
/// </summary> |
||||
private void ProcessFrameData() |
||||
{ |
||||
_fileStream.Position = Header.HeaderSize; |
||||
|
||||
var tempBuf = ArrayPool<byte>.Shared.Rent(MaxTempBuf); |
||||
|
||||
var terminate = false; |
||||
var curFrame = 0; |
||||
|
||||
Frames.Add(new GifFrame()); |
||||
|
||||
do |
||||
{ |
||||
var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf); |
||||
|
||||
switch (blockType) |
||||
{ |
||||
case BlockTypes.Empty: |
||||
break; |
||||
|
||||
case BlockTypes.Extension: |
||||
ProcessExtensions(ref curFrame, tempBuf); |
||||
break; |
||||
|
||||
case BlockTypes.ImageDescriptor: |
||||
ProcessImageDescriptor(ref curFrame, tempBuf); |
||||
_fileStream.SkipBlocks(tempBuf); |
||||
break; |
||||
|
||||
case BlockTypes.Trailer: |
||||
Frames.RemoveAt(Frames.Count - 1); |
||||
terminate = true; |
||||
break; |
||||
|
||||
default: |
||||
_fileStream.SkipBlocks(tempBuf); |
||||
break; |
||||
} |
||||
|
||||
// Break the loop when the stream is not valid anymore. |
||||
if (_fileStream.Position >= _fileStream.Length & terminate == false) |
||||
throw new InvalidProgramException("Reach the end of the filestream without trailer block."); |
||||
} while (!terminate); |
||||
|
||||
ArrayPool<byte>.Shared.Return(tempBuf); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Parses GIF Image Descriptor Block. |
||||
/// </summary> |
||||
private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf) |
||||
{ |
||||
var str = _fileStream; |
||||
var currentFrame = Frames[curFrame]; |
||||
|
||||
// Parse frame dimensions. |
||||
var frameX = str.ReadUShortS(tempBuf); |
||||
var frameY = str.ReadUShortS(tempBuf); |
||||
var frameW = str.ReadUShortS(tempBuf); |
||||
var frameH = str.ReadUShortS(tempBuf); |
||||
|
||||
frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX); |
||||
frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY); |
||||
|
||||
currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH); |
||||
|
||||
// Unpack interlace and lct info. |
||||
var packed = str.ReadByteS(tempBuf); |
||||
currentFrame.IsInterlaced = (packed & 0x40) != 0; |
||||
currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0; |
||||
currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1); |
||||
|
||||
if (currentFrame.IsLocalColorTableUsed) |
||||
currentFrame.LocalColorTable = ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize); |
||||
|
||||
currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf); |
||||
currentFrame.LzwStreamPosition = str.Position; |
||||
|
||||
curFrame += 1; |
||||
Frames.Add(new GifFrame()); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Parses GIF Extension Blocks. |
||||
/// </summary> |
||||
private void ProcessExtensions(ref int curFrame, byte[] tempBuf) |
||||
{ |
||||
var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf); |
||||
|
||||
switch (extType) |
||||
{ |
||||
case ExtensionType.GraphicsControl: |
||||
|
||||
_fileStream.ReadBlock(tempBuf); |
||||
var currentFrame = Frames[curFrame]; |
||||
var packed = tempBuf[0]; |
||||
|
||||
currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2); |
||||
|
||||
if ( |
||||
currentFrame.FrameDisposalMethod != FrameDisposal.Restore |
||||
&& currentFrame.FrameDisposalMethod != FrameDisposal.Background |
||||
) |
||||
currentFrame.ShouldBackup = true; |
||||
|
||||
currentFrame.HasTransparency = (packed & 1) != 0; |
||||
|
||||
currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10); |
||||
|
||||
if (currentFrame.FrameDelay <= FrameDelayThreshold) |
||||
currentFrame.FrameDelay = FrameDelayDefault; |
||||
|
||||
currentFrame.TransparentColorIndex = tempBuf[3]; |
||||
break; |
||||
|
||||
case ExtensionType.Application: |
||||
var blockLen = _fileStream.ReadBlock(tempBuf); |
||||
var _ = tempBuf.AsSpan(0, blockLen); |
||||
var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length); |
||||
|
||||
if (blockHeader.SequenceEqual(NetscapeMagic.Span)) |
||||
{ |
||||
var count = 1; |
||||
|
||||
while (count > 0) |
||||
count = _fileStream.ReadBlock(tempBuf); |
||||
|
||||
var iterationCount = SpanToShort(tempBuf.AsSpan(1)); |
||||
|
||||
Header.Iterations = iterationCount; |
||||
} |
||||
else |
||||
_fileStream.SkipBlocks(tempBuf); |
||||
|
||||
break; |
||||
|
||||
default: |
||||
_fileStream.SkipBlocks(tempBuf); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@
|
||||
namespace Avalonia.Gif.Decoding |
||||
{ |
||||
public class GifRepeatBehavior |
||||
{ |
||||
public bool LoopForever { get; set; } |
||||
public int? Count { get; set; } |
||||
} |
||||
} |
@ -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) { } |
||||
} |
||||
} |
@ -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) { } |
||||
} |
||||
} |
@ -0,0 +1,81 @@
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.IO; |
||||
using System.Runtime.CompilerServices; |
||||
|
||||
namespace Avalonia.Gif.Extensions |
||||
{ |
||||
[DebuggerStepThrough] |
||||
internal static class StreamExtensions |
||||
{ |
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
public static ushort SpanToShort(Span<byte> b) => (ushort)(b[0] | (b[1] << 8)); |
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
public static void Skip(this Stream stream, long count) |
||||
{ |
||||
stream.Position += count; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Read a Gif block from stream while advancing the position. |
||||
/// </summary> |
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
public static int ReadBlock(this Stream stream, byte[] tempBuf) |
||||
{ |
||||
stream.Read(tempBuf, 0, 1); |
||||
|
||||
var blockLength = (int)tempBuf[0]; |
||||
|
||||
if (blockLength > 0) |
||||
stream.Read(tempBuf, 0, blockLength); |
||||
|
||||
// Guard against infinite loop. |
||||
if (stream.Position >= stream.Length) |
||||
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); |
||||
|
||||
return blockLength; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Skips GIF blocks until it encounters an empty block. |
||||
/// </summary> |
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
public static void SkipBlocks(this Stream stream, byte[] tempBuf) |
||||
{ |
||||
int blockLength; |
||||
do |
||||
{ |
||||
stream.Read(tempBuf, 0, 1); |
||||
|
||||
blockLength = tempBuf[0]; |
||||
stream.Position += blockLength; |
||||
|
||||
// Guard against infinite loop. |
||||
if (stream.Position >= stream.Length) |
||||
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); |
||||
} while (blockLength > 0); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer. |
||||
/// </summary> |
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
public static ushort ReadUShortS(this Stream stream, byte[] tempBuf) |
||||
{ |
||||
stream.Read(tempBuf, 0, 2); |
||||
return SpanToShort(tempBuf); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer. |
||||
/// </summary> |
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
public static byte ReadByteS(this Stream stream, byte[] tempBuf) |
||||
{ |
||||
stream.Read(tempBuf, 0, 1); |
||||
var finalVal = tempBuf[0]; |
||||
return finalVal; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,297 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Numerics; |
||||
using Avalonia; |
||||
using Avalonia.Animation; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Logging; |
||||
using Avalonia.Media; |
||||
using Avalonia.Rendering.Composition; |
||||
using Avalonia.VisualTree; |
||||
|
||||
namespace Avalonia.Gif |
||||
{ |
||||
public class GifImage : Control |
||||
{ |
||||
public static readonly StyledProperty<string> SourceUriRawProperty = AvaloniaProperty.Register< |
||||
GifImage, |
||||
string |
||||
>("SourceUriRaw"); |
||||
|
||||
public static readonly StyledProperty<Uri> SourceUriProperty = AvaloniaProperty.Register<GifImage, Uri>( |
||||
"SourceUri" |
||||
); |
||||
|
||||
public static readonly StyledProperty<Stream> SourceStreamProperty = AvaloniaProperty.Register< |
||||
GifImage, |
||||
Stream |
||||
>("SourceStream"); |
||||
|
||||
public static readonly StyledProperty<IterationCount> IterationCountProperty = AvaloniaProperty.Register< |
||||
GifImage, |
||||
IterationCount |
||||
>("IterationCount", IterationCount.Infinite); |
||||
|
||||
private IGifInstance? _gifInstance; |
||||
|
||||
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty = AvaloniaProperty.Register< |
||||
GifImage, |
||||
StretchDirection |
||||
>("StretchDirection"); |
||||
|
||||
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<GifImage, Stretch>( |
||||
"Stretch" |
||||
); |
||||
|
||||
private CompositionCustomVisual? _customVisual; |
||||
|
||||
private object? _initialSource = null; |
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
||||
{ |
||||
switch (change.Property.Name) |
||||
{ |
||||
case nameof(SourceUriRaw): |
||||
case nameof(SourceUri): |
||||
case nameof(SourceStream): |
||||
SourceChanged(change); |
||||
break; |
||||
case nameof(Stretch): |
||||
case nameof(StretchDirection): |
||||
InvalidateArrange(); |
||||
InvalidateMeasure(); |
||||
Update(); |
||||
break; |
||||
case nameof(IterationCount): |
||||
IterationCountChanged(change); |
||||
break; |
||||
case nameof(Bounds): |
||||
Update(); |
||||
break; |
||||
} |
||||
|
||||
base.OnPropertyChanged(change); |
||||
} |
||||
|
||||
public string SourceUriRaw |
||||
{ |
||||
get => GetValue(SourceUriRawProperty); |
||||
set => SetValue(SourceUriRawProperty, value); |
||||
} |
||||
|
||||
public Uri SourceUri |
||||
{ |
||||
get => GetValue(SourceUriProperty); |
||||
set => SetValue(SourceUriProperty, value); |
||||
} |
||||
|
||||
public Stream SourceStream |
||||
{ |
||||
get => GetValue(SourceStreamProperty); |
||||
set => SetValue(SourceStreamProperty, value); |
||||
} |
||||
|
||||
public IterationCount IterationCount |
||||
{ |
||||
get => GetValue(IterationCountProperty); |
||||
set => SetValue(IterationCountProperty, value); |
||||
} |
||||
|
||||
public StretchDirection StretchDirection |
||||
{ |
||||
get => GetValue(StretchDirectionProperty); |
||||
set => SetValue(StretchDirectionProperty, value); |
||||
} |
||||
|
||||
public Stretch Stretch |
||||
{ |
||||
get => GetValue(StretchProperty); |
||||
set => SetValue(StretchProperty, value); |
||||
} |
||||
|
||||
private static void IterationCountChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
var image = e.Sender as GifImage; |
||||
if (image is null || e.NewValue is not IterationCount iterationCount) |
||||
return; |
||||
|
||||
image.IterationCount = iterationCount; |
||||
} |
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
||||
{ |
||||
var compositor = ElementComposition.GetElementVisual(this)?.Compositor; |
||||
if (compositor == null || _customVisual?.Compositor == compositor) |
||||
return; |
||||
_customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); |
||||
ElementComposition.SetElementChildVisual(this, _customVisual); |
||||
_customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); |
||||
|
||||
if (_initialSource is not null) |
||||
{ |
||||
UpdateGifInstance(_initialSource); |
||||
_initialSource = null; |
||||
} |
||||
|
||||
Update(); |
||||
base.OnAttachedToVisualTree(e); |
||||
} |
||||
|
||||
private void Update() |
||||
{ |
||||
if (_customVisual is null || _gifInstance is null) |
||||
return; |
||||
|
||||
var dpi = this.GetVisualRoot()?.RenderScaling ?? 1.0; |
||||
var sourceSize = _gifInstance.GifPixelSize.ToSize(dpi); |
||||
var viewPort = new Rect(Bounds.Size); |
||||
|
||||
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); |
||||
var scaledSize = sourceSize * scale; |
||||
var destRect = viewPort.CenterRect(new Rect(scaledSize)).Intersect(viewPort); |
||||
|
||||
if (Stretch == Stretch.None) |
||||
{ |
||||
_customVisual.Size = new Vector2((float)sourceSize.Width, (float)sourceSize.Height); |
||||
} |
||||
else |
||||
{ |
||||
_customVisual.Size = new Vector2((float)destRect.Size.Width, (float)destRect.Size.Height); |
||||
} |
||||
|
||||
_customVisual.Offset = new Vector3((float)destRect.Position.X, (float)destRect.Position.Y, 0); |
||||
} |
||||
|
||||
private class CustomVisualHandler : CompositionCustomVisualHandler |
||||
{ |
||||
private TimeSpan _animationElapsed; |
||||
private TimeSpan? _lastServerTime; |
||||
private IGifInstance? _currentInstance; |
||||
private bool _running; |
||||
|
||||
public static readonly object StopMessage = new(), |
||||
StartMessage = new(); |
||||
|
||||
public override void OnMessage(object message) |
||||
{ |
||||
if (message == StartMessage) |
||||
{ |
||||
_running = true; |
||||
_lastServerTime = null; |
||||
RegisterForNextAnimationFrameUpdate(); |
||||
} |
||||
else if (message == StopMessage) |
||||
{ |
||||
_running = false; |
||||
} |
||||
else if (message is IGifInstance instance) |
||||
{ |
||||
_currentInstance?.Dispose(); |
||||
_currentInstance = instance; |
||||
} |
||||
} |
||||
|
||||
public override void OnAnimationFrameUpdate() |
||||
{ |
||||
if (!_running) |
||||
return; |
||||
Invalidate(); |
||||
RegisterForNextAnimationFrameUpdate(); |
||||
} |
||||
|
||||
public override void OnRender(ImmediateDrawingContext drawingContext) |
||||
{ |
||||
if (_running) |
||||
{ |
||||
if (_lastServerTime.HasValue) |
||||
_animationElapsed += (CompositionNow - _lastServerTime.Value); |
||||
_lastServerTime = CompositionNow; |
||||
} |
||||
|
||||
try |
||||
{ |
||||
if (_currentInstance is null || _currentInstance.IsDisposed) |
||||
return; |
||||
|
||||
var bitmap = _currentInstance.ProcessFrameTime(_animationElapsed); |
||||
if (bitmap is not null) |
||||
{ |
||||
drawingContext.DrawBitmap( |
||||
bitmap, |
||||
new Rect(_currentInstance.GifPixelSize.ToSize(1)), |
||||
GetRenderBounds() |
||||
); |
||||
} |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
Logger.Sink?.Log(LogEventLevel.Error, "GifImage Renderer ", this, e.ToString()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Measures the control. |
||||
/// </summary> |
||||
/// <param name="availableSize">The available size.</param> |
||||
/// <returns>The desired size of the control.</returns> |
||||
protected override Size MeasureOverride(Size availableSize) |
||||
{ |
||||
var result = new Size(); |
||||
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; |
||||
if (_gifInstance != null) |
||||
{ |
||||
result = Stretch.CalculateSize( |
||||
availableSize, |
||||
_gifInstance.GifPixelSize.ToSize(scaling), |
||||
StretchDirection |
||||
); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/// <inheritdoc/> |
||||
protected override Size ArrangeOverride(Size finalSize) |
||||
{ |
||||
if (_gifInstance is null) |
||||
return new Size(); |
||||
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; |
||||
var sourceSize = _gifInstance.GifPixelSize.ToSize(scaling); |
||||
var result = Stretch.CalculateSize(finalSize, sourceSize); |
||||
return result; |
||||
} |
||||
|
||||
private void SourceChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
if ( |
||||
e.NewValue is null |
||||
|| (e.NewValue is string value && !Uri.IsWellFormedUriString(value, UriKind.Absolute)) |
||||
) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
if (_customVisual is null) |
||||
{ |
||||
_initialSource = e.NewValue; |
||||
return; |
||||
} |
||||
|
||||
UpdateGifInstance(e.NewValue); |
||||
|
||||
InvalidateArrange(); |
||||
InvalidateMeasure(); |
||||
Update(); |
||||
} |
||||
|
||||
private void UpdateGifInstance(object source) |
||||
{ |
||||
_gifInstance?.Dispose(); |
||||
_gifInstance = new WebpInstance(source); |
||||
// _gifInstance = new GifInstance(source); |
||||
_gifInstance.IterationCount = IterationCount; |
||||
_customVisual?.SendHandlerMessage(_gifInstance); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,147 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using Avalonia; |
||||
using Avalonia.Animation; |
||||
using Avalonia.Gif.Decoding; |
||||
using Avalonia.Media.Imaging; |
||||
using Avalonia.Platform; |
||||
|
||||
namespace Avalonia.Gif |
||||
{ |
||||
public class GifInstance : IGifInstance |
||||
{ |
||||
public IterationCount IterationCount { get; set; } |
||||
public bool AutoStart { get; private set; } = true; |
||||
private readonly GifDecoder _gifDecoder; |
||||
private readonly WriteableBitmap? _targetBitmap; |
||||
private TimeSpan _totalTime; |
||||
private readonly List<TimeSpan> _frameTimes; |
||||
private uint _iterationCount; |
||||
private int _currentFrameIndex; |
||||
private readonly List<ulong> _colorTableIdList; |
||||
|
||||
public CancellationTokenSource CurrentCts { get; } |
||||
|
||||
internal GifInstance(object newValue) |
||||
: this( |
||||
newValue switch |
||||
{ |
||||
Stream s => s, |
||||
Uri u => GetStreamFromUri(u), |
||||
string str => GetStreamFromString(str), |
||||
_ => throw new InvalidDataException("Unsupported source object") |
||||
} |
||||
) { } |
||||
|
||||
public GifInstance(string uri) |
||||
: this(GetStreamFromString(uri)) { } |
||||
|
||||
public GifInstance(Uri uri) |
||||
: this(GetStreamFromUri(uri)) { } |
||||
|
||||
public GifInstance(Stream currentStream) |
||||
{ |
||||
if (!currentStream.CanSeek) |
||||
throw new InvalidDataException("The provided stream is not seekable."); |
||||
|
||||
if (!currentStream.CanRead) |
||||
throw new InvalidOperationException("Can't read the stream provided."); |
||||
|
||||
currentStream.Seek(0, SeekOrigin.Begin); |
||||
|
||||
CurrentCts = new CancellationTokenSource(); |
||||
|
||||
_gifDecoder = new GifDecoder(currentStream, CurrentCts.Token); |
||||
var pixSize = new PixelSize(_gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height); |
||||
|
||||
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); |
||||
GifPixelSize = pixSize; |
||||
|
||||
_totalTime = TimeSpan.Zero; |
||||
|
||||
_frameTimes = _gifDecoder |
||||
.Frames |
||||
.Select(frame => |
||||
{ |
||||
_totalTime = _totalTime.Add(frame.FrameDelay); |
||||
return _totalTime; |
||||
}) |
||||
.ToList(); |
||||
|
||||
_gifDecoder.RenderFrame(0, _targetBitmap); |
||||
} |
||||
|
||||
private static Stream GetStreamFromString(string str) |
||||
{ |
||||
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res)) |
||||
{ |
||||
throw new InvalidCastException("The string provided can't be converted to URI."); |
||||
} |
||||
|
||||
return GetStreamFromUri(res); |
||||
} |
||||
|
||||
private static Stream GetStreamFromUri(Uri uri) |
||||
{ |
||||
var uriString = uri.OriginalString.Trim(); |
||||
|
||||
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares")) |
||||
{ |
||||
return new FileStream(uriString, FileMode.Open, FileAccess.Read); |
||||
} |
||||
|
||||
return AssetLoader.Open(uri); |
||||
} |
||||
|
||||
public int GifFrameCount => _frameTimes.Count; |
||||
|
||||
public PixelSize GifPixelSize { get; } |
||||
|
||||
public void Dispose() |
||||
{ |
||||
IsDisposed = true; |
||||
CurrentCts.Cancel(); |
||||
_targetBitmap?.Dispose(); |
||||
} |
||||
|
||||
public bool IsDisposed { get; private set; } |
||||
|
||||
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed) |
||||
{ |
||||
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
if (CurrentCts.IsCancellationRequested || _targetBitmap is null) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
var elapsedTicks = stopwatchElapsed.Ticks; |
||||
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks); |
||||
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x); |
||||
var currentFrame = _frameTimes.IndexOf(targetFrame); |
||||
if (currentFrame == -1) |
||||
currentFrame = 0; |
||||
|
||||
if (_currentFrameIndex == currentFrame) |
||||
return _targetBitmap; |
||||
|
||||
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks); |
||||
|
||||
return ProcessFrameIndex(currentFrame); |
||||
} |
||||
|
||||
internal WriteableBitmap ProcessFrameIndex(int frameIndex) |
||||
{ |
||||
_gifDecoder.RenderFrame(frameIndex, _targetBitmap); |
||||
_currentFrameIndex = frameIndex; |
||||
|
||||
return _targetBitmap; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
|
||||
using Avalonia.Animation; |
||||
using Avalonia.Media.Imaging; |
||||
|
||||
namespace Avalonia.Gif; |
||||
|
||||
public interface IGifInstance : IDisposable |
||||
{ |
||||
IterationCount IterationCount { get; set; } |
||||
bool AutoStart { get; } |
||||
CancellationTokenSource CurrentCts { get; } |
||||
int GifFrameCount { get; } |
||||
PixelSize GifPixelSize { get; } |
||||
bool IsDisposed { get; } |
||||
WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed); |
||||
} |
@ -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) { } |
||||
} |
||||
} |
@ -0,0 +1,180 @@
|
||||
using Avalonia.Animation; |
||||
using Avalonia.Media.Imaging; |
||||
using Avalonia.Platform; |
||||
using SkiaSharp; |
||||
|
||||
namespace Avalonia.Gif; |
||||
|
||||
public class WebpInstance : IGifInstance |
||||
{ |
||||
public IterationCount IterationCount { get; set; } |
||||
public bool AutoStart { get; private set; } = true; |
||||
|
||||
private readonly WriteableBitmap? _targetBitmap; |
||||
private TimeSpan _totalTime; |
||||
private readonly List<TimeSpan> _frameTimes; |
||||
private uint _iterationCount; |
||||
private int _currentFrameIndex; |
||||
|
||||
private SKCodec? _codec; |
||||
|
||||
public CancellationTokenSource CurrentCts { get; } |
||||
|
||||
internal WebpInstance(object newValue) |
||||
: this( |
||||
newValue switch |
||||
{ |
||||
Stream s => s, |
||||
Uri u => GetStreamFromUri(u), |
||||
string str => GetStreamFromString(str), |
||||
_ => throw new InvalidDataException("Unsupported source object") |
||||
} |
||||
) { } |
||||
|
||||
public WebpInstance(string uri) |
||||
: this(GetStreamFromString(uri)) { } |
||||
|
||||
public WebpInstance(Uri uri) |
||||
: this(GetStreamFromUri(uri)) { } |
||||
|
||||
public WebpInstance(Stream currentStream) |
||||
{ |
||||
if (!currentStream.CanSeek) |
||||
throw new InvalidDataException("The provided stream is not seekable."); |
||||
|
||||
if (!currentStream.CanRead) |
||||
throw new InvalidOperationException("Can't read the stream provided."); |
||||
|
||||
currentStream.Seek(0, SeekOrigin.Begin); |
||||
|
||||
CurrentCts = new CancellationTokenSource(); |
||||
|
||||
var managedStream = new SKManagedStream(currentStream); |
||||
_codec = SKCodec.Create(managedStream); |
||||
|
||||
var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height); |
||||
|
||||
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); |
||||
GifPixelSize = pixSize; |
||||
|
||||
_totalTime = TimeSpan.Zero; |
||||
|
||||
_frameTimes = _codec |
||||
.FrameInfo |
||||
.Select(frame => |
||||
{ |
||||
_totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration)); |
||||
return _totalTime; |
||||
}) |
||||
.ToList(); |
||||
|
||||
RenderFrame(_codec, _targetBitmap, 0); |
||||
} |
||||
|
||||
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index) |
||||
{ |
||||
codec.GetFrameInfo(index, out var frameInfo); |
||||
|
||||
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height); |
||||
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType); |
||||
|
||||
using var frameBuffer = targetBitmap.Lock(); |
||||
|
||||
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index)); |
||||
|
||||
if (result != SKCodecResult.Success) |
||||
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}."); |
||||
} |
||||
|
||||
private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex) |
||||
{ |
||||
codec.GetFrameInfo(index, out var frameInfo); |
||||
|
||||
var info = new SKImageInfo(codec.Info.Width, codec.Info.Height); |
||||
var decodeInfo = info.WithAlphaType(frameInfo.AlphaType); |
||||
|
||||
using var frameBuffer = targetBitmap.Lock(); |
||||
|
||||
var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex)); |
||||
|
||||
if (result != SKCodecResult.Success) |
||||
throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}."); |
||||
} |
||||
|
||||
private static Stream GetStreamFromString(string str) |
||||
{ |
||||
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res)) |
||||
{ |
||||
throw new InvalidCastException("The string provided can't be converted to URI."); |
||||
} |
||||
|
||||
return GetStreamFromUri(res); |
||||
} |
||||
|
||||
private static Stream GetStreamFromUri(Uri uri) |
||||
{ |
||||
var uriString = uri.OriginalString.Trim(); |
||||
|
||||
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares")) |
||||
{ |
||||
return new FileStream(uriString, FileMode.Open, FileAccess.Read); |
||||
} |
||||
|
||||
return AssetLoader.Open(uri); |
||||
} |
||||
|
||||
public int GifFrameCount => _frameTimes.Count; |
||||
|
||||
public PixelSize GifPixelSize { get; } |
||||
|
||||
public void Dispose() |
||||
{ |
||||
IsDisposed = true; |
||||
CurrentCts.Cancel(); |
||||
_targetBitmap?.Dispose(); |
||||
_codec?.Dispose(); |
||||
} |
||||
|
||||
public bool IsDisposed { get; private set; } |
||||
|
||||
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed) |
||||
{ |
||||
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
if (CurrentCts.IsCancellationRequested || _targetBitmap is null) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
var elapsedTicks = stopwatchElapsed.Ticks; |
||||
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks); |
||||
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x); |
||||
var currentFrame = _frameTimes.IndexOf(targetFrame); |
||||
if (currentFrame == -1) |
||||
currentFrame = 0; |
||||
|
||||
if (_currentFrameIndex == currentFrame) |
||||
return _targetBitmap; |
||||
|
||||
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks); |
||||
|
||||
return ProcessFrameIndex(currentFrame); |
||||
} |
||||
|
||||
internal WriteableBitmap ProcessFrameIndex(int frameIndex) |
||||
{ |
||||
if (_codec is null) |
||||
throw new InvalidOperationException("The codec is null."); |
||||
|
||||
if (_targetBitmap is null) |
||||
throw new InvalidOperationException("The target bitmap is null."); |
||||
|
||||
RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex); |
||||
_currentFrameIndex = frameIndex; |
||||
|
||||
return _targetBitmap; |
||||
} |
||||
} |
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
<plist version="1.0"> |
||||
<dict> |
||||
<key>com.apple.security.cs.allow-jit</key> |
||||
<true/> |
||||
</dict> |
||||
</plist> |
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
<plist version="1.0"> |
||||
<dict> |
||||
<key>com.apple.security.cs.allow-jit</key> |
||||
<true/> |
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> |
||||
<true/> |
||||
<key>com.apple.security.cs.disable-library-validation</key> |
||||
<true/> |
||||
</dict> |
||||
</plist> |
@ -0,0 +1,26 @@
|
||||
#!/bin/sh |
||||
|
||||
while getopts v: flag |
||||
do |
||||
case "${flag}" in |
||||
v) version=${OPTARG};; |
||||
*) echo "Invalid option";; |
||||
esac |
||||
done |
||||
|
||||
dotnet \ |
||||
msbuild \ |
||||
StabilityMatrix.Avalonia \ |
||||
-t:BundleApp \ |
||||
-p:RuntimeIdentifier=osx-arm64 \ |
||||
-p:UseAppHost=true \ |
||||
-p:Configuration=Release \ |
||||
-p:CFBundleShortVersionString="$version" \ |
||||
-p:SelfContained=true \ |
||||
-p:CFBundleName="Stability Matrix" \ |
||||
-p:CFBundleDisplayName="Stability Matrix" \ |
||||
-p:CFBundleVersion="$version" \ |
||||
-p:PublishDir="$(pwd)/out/osx-arm64/bin" \ |
||||
|
||||
# Copy the app out of bin |
||||
cp -r ./out/osx-arm64/bin/Stability\ Matrix.app ./out/osx-arm64/Stability\ Matrix.app |
@ -0,0 +1,62 @@
|
||||
#!/bin/sh |
||||
|
||||
echo "Signing file: $1" |
||||
|
||||
# Setup keychain in CI |
||||
if [ -n "$CI" ]; then |
||||
# Turn our base64-encoded certificate back to a regular .p12 file |
||||
|
||||
echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12 |
||||
|
||||
# We need to create a new keychain, otherwise using the certificate will prompt |
||||
# with a UI dialog asking for the certificate password, which we can't |
||||
# use in a headless CI environment |
||||
|
||||
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain |
||||
security default-keychain -s build.keychain |
||||
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain |
||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign |
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain |
||||
fi |
||||
|
||||
# Sign all files |
||||
PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P ) |
||||
ENTITLEMENTS="$PARENT_PATH/EmbeddedEntitlements.entitlements" |
||||
|
||||
echo "Using entitlements file: $ENTITLEMENTS" |
||||
|
||||
# App |
||||
if [ "$1" == "*.app" ]; then |
||||
echo "[INFO] Signing app contents" |
||||
|
||||
find "$1/Contents/MacOS/"|while read fname; do |
||||
if [[ -f $fname ]]; then |
||||
echo "[INFO] Signing $fname" |
||||
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname" |
||||
fi |
||||
done |
||||
|
||||
echo "[INFO] Signing app file" |
||||
|
||||
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v |
||||
# Directory |
||||
elif [ -d "$1" ]; then |
||||
echo "[INFO] Signing directory contents" |
||||
|
||||
find "$1"|while read fname; do |
||||
if [[ -f $fname ]] && [[ ! $fname =~ /(*.(py|msg|enc))/ ]]; then |
||||
echo "[INFO] Signing $fname" |
||||
|
||||
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname" |
||||
fi |
||||
done |
||||
# File |
||||
elif [ -f "$1" ]; then |
||||
echo "[INFO] Signing file" |
||||
|
||||
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v |
||||
# Not matched |
||||
else |
||||
echo "[ERROR] Unknown file type" |
||||
exit 1 |
||||
fi |
@ -0,0 +1,37 @@
|
||||
#!/bin/sh |
||||
|
||||
echo "Signing file: $1" |
||||
|
||||
# Setup keychain in CI |
||||
if [ -n "$CI" ]; then |
||||
# Turn our base64-encoded certificate back to a regular .p12 file |
||||
|
||||
echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12 |
||||
|
||||
# We need to create a new keychain, otherwise using the certificate will prompt |
||||
# with a UI dialog asking for the certificate password, which we can't |
||||
# use in a headless CI environment |
||||
|
||||
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain |
||||
security default-keychain -s build.keychain |
||||
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain |
||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign |
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain |
||||
fi |
||||
|
||||
# Sign all files |
||||
PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P ) |
||||
ENTITLEMENTS="$PARENT_PATH/AppEntitlements.entitlements" |
||||
|
||||
echo "Using entitlements file: $ENTITLEMENTS" |
||||
|
||||
find "$1/Contents/MacOS/"|while read fname; do |
||||
if [[ -f $fname ]]; then |
||||
echo "[INFO] Signing $fname" |
||||
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname" |
||||
fi |
||||
done |
||||
|
||||
echo "[INFO] Signing app file" |
||||
|
||||
codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v |
@ -0,0 +1,32 @@
|
||||
#!/bin/sh |
||||
|
||||
echo "Notarizing file: $1" |
||||
|
||||
# Store the notarization credentials so that we can prevent a UI password dialog |
||||
# from blocking the CI |
||||
|
||||
echo "Create keychain profile" |
||||
xcrun notarytool store-credentials "notarytool-profile" \ |
||||
--apple-id "$MACOS_NOTARIZATION_APPLE_ID" \ |
||||
--team-id "$MACOS_NOTARIZATION_TEAM_ID" \ |
||||
--password "$MACOS_NOTARIZATION_PWD" |
||||
|
||||
# We can't notarize an app bundle directly, but we need to compress it as an archive. |
||||
# Therefore, we create a zip file containing our app bundle, so that we can send it to the |
||||
# notarization service |
||||
|
||||
echo "Creating temp notarization archive" |
||||
ditto -c -k --keepParent "$1" "notarization.zip" |
||||
|
||||
# Here we send the notarization request to the Apple's Notarization service, waiting for the result. |
||||
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App |
||||
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if |
||||
# you're curious |
||||
|
||||
echo "Notarize app" |
||||
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait |
||||
|
||||
# Finally, we need to "attach the staple" to our executable, which will allow our app to be |
||||
# validated by macOS even when an internet connection is not available. |
||||
echo "Attach staple" |
||||
xcrun stapler staple "$1" |
Binary file not shown.
@ -0,0 +1,41 @@
|
||||
using System; |
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Controls.Templates; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
public class BetterComboBox : ComboBox |
||||
{ |
||||
// protected override Type StyleKeyOverride { get; } = typeof(CheckBox); |
||||
|
||||
public static readonly DirectProperty<BetterComboBox, IDataTemplate?> SelectionBoxItemTemplateProperty = |
||||
AvaloniaProperty.RegisterDirect<BetterComboBox, IDataTemplate?>( |
||||
nameof(SelectionBoxItemTemplate), |
||||
v => v.SelectionBoxItemTemplate, |
||||
(x, v) => x.SelectionBoxItemTemplate = v |
||||
); |
||||
|
||||
private IDataTemplate? _selectionBoxItemTemplate; |
||||
|
||||
public IDataTemplate? SelectionBoxItemTemplate |
||||
{ |
||||
get => _selectionBoxItemTemplate; |
||||
private set => SetAndRaise(SelectionBoxItemTemplateProperty, ref _selectionBoxItemTemplate, value); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||
{ |
||||
base.OnApplyTemplate(e); |
||||
|
||||
if (e.NameScope.Find<ContentControl>("ContentPresenter") is { } contentPresenter) |
||||
{ |
||||
if (SelectionBoxItemTemplate is { } template) |
||||
{ |
||||
contentPresenter.ContentTemplate = template; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Templates; |
||||
using Avalonia.Metadata; |
||||
using JetBrains.Annotations; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
/// <summary> |
||||
/// Selector for objects implementing <see cref="ITemplateKey{T}"/> |
||||
/// </summary> |
||||
[PublicAPI] |
||||
public class DataTemplateSelector<TKey> : IDataTemplate |
||||
where TKey : notnull |
||||
{ |
||||
/// <summary> |
||||
/// Key that is used when no other key matches |
||||
/// </summary> |
||||
public TKey? DefaultKey { get; set; } |
||||
|
||||
[Content] |
||||
public Dictionary<TKey, IDataTemplate> Templates { get; } = new(); |
||||
|
||||
public bool Match(object? data) => data is ITemplateKey<TKey>; |
||||
|
||||
/// <inheritdoc /> |
||||
public Control Build(object? data) |
||||
{ |
||||
if (data is not ITemplateKey<TKey> key) |
||||
throw new ArgumentException(null, nameof(data)); |
||||
|
||||
if (Templates.TryGetValue(key.TemplateKey, out var template)) |
||||
{ |
||||
return template.Build(data)!; |
||||
} |
||||
|
||||
if (DefaultKey is not null && Templates.TryGetValue(DefaultKey, out var defaultTemplate)) |
||||
{ |
||||
return defaultTemplate.Build(data)!; |
||||
} |
||||
|
||||
throw new ArgumentException(null, nameof(data)); |
||||
} |
||||
} |
@ -0,0 +1,122 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
||||
x:DataType="video:SvdImgToVidConditioningViewModel" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:video="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Video" |
||||
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"> |
||||
<Design.PreviewWith> |
||||
<Grid MinWidth="400"> |
||||
<controls:VideoGenerationSettingsCard DataContext="{x:Static mocks:DesignData.SvdImgToVidConditioningViewModel}" /> |
||||
</Grid> |
||||
</Design.PreviewWith> |
||||
|
||||
<Style Selector="controls|VideoGenerationSettingsCard"> |
||||
<!-- Set Defaults --> |
||||
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
||||
<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<controls:Card Padding="8" HorizontalAlignment="{TemplateBinding HorizontalAlignment}"> |
||||
<Grid Margin="4" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, *"> |
||||
<TextBlock |
||||
Grid.Row="0" |
||||
Grid.Column="0" |
||||
Margin="0,0,8,0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_Frames}" /> |
||||
<controls1:NumberBox |
||||
Grid.Row="0" |
||||
Grid.Column="1" |
||||
SelectionHighlightColor="Transparent" |
||||
Value="{Binding NumFrames}" |
||||
Margin="8,0,0,0" |
||||
SimpleNumberFormat="F0" |
||||
SmallChange="1" |
||||
HorizontalAlignment="Stretch" |
||||
SpinButtonPlacementMode="Inline"/> |
||||
|
||||
<TextBlock |
||||
Grid.Row="1" |
||||
Grid.Column="0" |
||||
Margin="0,8,8,0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_Fps}" /> |
||||
<controls1:NumberBox |
||||
Grid.Row="1" |
||||
Grid.Column="1" |
||||
SelectionHighlightColor="Transparent" |
||||
Value="{Binding Fps}" |
||||
Margin="8,8,0,0" |
||||
SimpleNumberFormat="F0" |
||||
SmallChange="1" |
||||
HorizontalAlignment="Stretch" |
||||
SpinButtonPlacementMode="Inline"/> |
||||
|
||||
<TextBlock |
||||
Grid.Row="2" |
||||
Grid.Column="0" |
||||
Margin="0,8,8,0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_MinCfg}" /> |
||||
<controls1:NumberBox |
||||
Margin="8,8,0,0" |
||||
Grid.Row="2" |
||||
Grid.Column="1" |
||||
SelectionHighlightColor="Transparent" |
||||
Value="{Binding MinCfg}" |
||||
SimpleNumberFormat="F0" |
||||
SmallChange="1" |
||||
HorizontalAlignment="Stretch" |
||||
SpinButtonPlacementMode="Inline"/> |
||||
|
||||
<TextBlock |
||||
Margin="0,8,8,0" |
||||
Grid.Row="3" |
||||
Grid.Column="0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_MotionBucketId}" /> |
||||
<controls1:NumberBox |
||||
Margin="8,8,0,0" |
||||
Grid.Row="3" |
||||
Grid.Column="1" |
||||
SelectionHighlightColor="Transparent" |
||||
Value="{Binding MotionBucketId}" |
||||
SimpleNumberFormat="F0" |
||||
SmallChange="1" |
||||
HorizontalAlignment="Stretch" |
||||
SpinButtonPlacementMode="Inline"/> |
||||
|
||||
<StackPanel Grid.Column="0" |
||||
Grid.ColumnSpan="2" |
||||
Grid.Row="4" |
||||
Margin="0,16,0,0"> |
||||
<Grid ColumnDefinitions="*,Auto"> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_AugmentationLevel}" /> |
||||
<controls1:NumberBox |
||||
Grid.Column="1" |
||||
Margin="4,0,0,0" |
||||
ValidationMode="InvalidInputOverwritten" |
||||
SmallChange="0.01" |
||||
SimpleNumberFormat="F2" |
||||
Value="{Binding AugmentationLevel}" |
||||
HorizontalAlignment="Stretch" |
||||
MinWidth="100" |
||||
SpinButtonPlacementMode="Compact"/> |
||||
</Grid> |
||||
<Slider |
||||
Minimum="0" |
||||
Maximum="10" |
||||
Value="{Binding AugmentationLevel}" |
||||
TickFrequency="1" |
||||
Margin="0,0,0,-4" |
||||
TickPlacement="BottomRight"/> |
||||
</StackPanel> |
||||
</Grid> |
||||
</controls:Card> |
||||
</ControlTemplate> |
||||
</Setter> |
||||
</Style> |
||||
</Styles> |
@ -0,0 +1,7 @@
|
||||
using Avalonia.Controls.Primitives; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
[Transient] |
||||
public class VideoGenerationSettingsCard : TemplatedControl { } |
@ -0,0 +1,98 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
||||
x:DataType="video:VideoOutputSettingsCardViewModel" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:video="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference.Video" |
||||
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"> |
||||
<Design.PreviewWith> |
||||
<Grid MinWidth="400"> |
||||
<controls:VideoOutputSettingsCard |
||||
DataContext="{x:Static mocks:DesignData.SvdImgToVidConditioningViewModel}" /> |
||||
</Grid> |
||||
</Design.PreviewWith> |
||||
|
||||
<Style Selector="controls|VideoOutputSettingsCard"> |
||||
<!-- Set Defaults --> |
||||
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
||||
<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<controls:Card Padding="8" HorizontalAlignment="{TemplateBinding HorizontalAlignment}"> |
||||
<Grid Margin="4" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, *"> |
||||
<TextBlock |
||||
Grid.Row="0" |
||||
Grid.Column="0" |
||||
Grid.ColumnSpan="2" |
||||
Text="Video Output Settings" |
||||
FontSize="16" |
||||
FontWeight="DemiBold" |
||||
Margin="0,0,0,16" |
||||
/> |
||||
<TextBlock |
||||
Grid.Row="1" |
||||
Grid.Column="0" |
||||
Margin="0,0,8,0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_Fps}" /> |
||||
<controls1:NumberBox |
||||
Grid.Row="1" |
||||
Grid.Column="1" |
||||
SelectionHighlightColor="Transparent" |
||||
Value="{Binding Fps}" |
||||
Margin="8,0,0,0" |
||||
SimpleNumberFormat="F0" |
||||
SmallChange="1" |
||||
HorizontalAlignment="Stretch" |
||||
SpinButtonPlacementMode="Inline" /> |
||||
|
||||
<TextBlock |
||||
Grid.Row="2" |
||||
Grid.Column="0" |
||||
Margin="0,8,8,0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_Lossless}" /> |
||||
<CheckBox |
||||
Grid.Row="2" |
||||
Grid.Column="1" |
||||
IsChecked="{Binding Lossless}" |
||||
Margin="8,8,0,0" |
||||
HorizontalAlignment="Stretch" /> |
||||
|
||||
<TextBlock |
||||
Grid.Row="3" |
||||
Grid.Column="0" |
||||
Margin="0,8,8,0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_VideoQuality}" /> |
||||
<controls1:NumberBox |
||||
Margin="8,8,0,0" |
||||
Grid.Row="3" |
||||
Grid.Column="1" |
||||
SelectionHighlightColor="Transparent" |
||||
Value="{Binding Quality}" |
||||
SimpleNumberFormat="F0" |
||||
SmallChange="1" |
||||
Maximum="100" |
||||
HorizontalAlignment="Stretch" |
||||
SpinButtonPlacementMode="Inline" /> |
||||
|
||||
<TextBlock |
||||
Margin="0,8,8,0" |
||||
Grid.Row="4" |
||||
Grid.Column="0" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_VideoOutputMethod}" /> |
||||
<ComboBox |
||||
Grid.Row="4" |
||||
Grid.Column="1" |
||||
Margin="8,8,0,0" |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AvailableMethods}" |
||||
SelectedIndex="{Binding SelectedMethod}" /> |
||||
</Grid> |
||||
</controls:Card> |
||||
</ControlTemplate> |
||||
</Setter> |
||||
</Style> |
||||
</Styles> |
@ -0,0 +1,7 @@
|
||||
using Avalonia.Controls.Primitives; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
[Transient] |
||||
public class VideoOutputSettingsCard : TemplatedControl { } |
@ -0,0 +1,33 @@
|
||||
using System.Diagnostics.Contracts; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Text.RegularExpressions; |
||||
using System.Windows.Input; |
||||
using StabilityMatrix.Core.Extensions; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public partial record CommandItem |
||||
{ |
||||
public ICommand Command { get; init; } |
||||
|
||||
public string DisplayName { get; init; } |
||||
|
||||
public CommandItem(ICommand command, [CallerArgumentExpression("command")] string? commandName = null) |
||||
{ |
||||
Command = command; |
||||
DisplayName = commandName == null ? "" : ProcessName(commandName); |
||||
} |
||||
|
||||
[Pure] |
||||
private static string ProcessName(string name) |
||||
{ |
||||
name = name.StripEnd("Command"); |
||||
|
||||
name = SpaceTitleCaseRegex().Replace(name, "$1 $2"); |
||||
|
||||
return name; |
||||
} |
||||
|
||||
[GeneratedRegex("([a-z])_?([A-Z])")]
|
||||
private static partial Regex SpaceTitleCaseRegex(); |
||||
} |
@ -0,0 +1,11 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
/// <summary> |
||||
/// Implements a template key for <see cref="DataTemplateSelector"/> |
||||
/// </summary> |
||||
public interface ITemplateKey<out T> |
||||
{ |
||||
T TemplateKey { get; } |
||||
} |
@ -0,0 +1,8 @@
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public enum ImageSourceTemplateType |
||||
{ |
||||
Default, |
||||
Image, |
||||
WebpAnimation |
||||
} |
@ -0,0 +1,11 @@
|
||||
using System.Text.Json.Serialization; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models.Inference; |
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))] |
||||
public enum VideoOutputMethod |
||||
{ |
||||
Fastest, |
||||
Default, |
||||
Slowest, |
||||
} |
@ -0,0 +1,10 @@
|
||||
using StabilityMatrix.Core.Models.Packages; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public record PackageManagerNavigationOptions |
||||
{ |
||||
public bool OpenInstallerDialog { get; init; } |
||||
|
||||
public BasePackage? InstallerSelectedPackage { get; init; } |
||||
} |
@ -0,0 +1,254 @@
|
||||
<ResourceDictionary |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" |
||||
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" |
||||
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> |
||||
|
||||
<Design.PreviewWith> |
||||
<Panel Width="450" Height="600"> |
||||
<StackPanel Margin="8" Spacing="4" Width="250"> |
||||
<controls:BetterComboBox |
||||
HorizontalAlignment="Stretch" |
||||
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}" |
||||
SelectedIndex="0" /> |
||||
|
||||
<controls:BetterComboBox |
||||
HorizontalAlignment="Stretch" |
||||
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}" |
||||
SelectedIndex="0" |
||||
Theme="{DynamicResource BetterComboBoxHybridModelTheme}" /> |
||||
</StackPanel> |
||||
</Panel> |
||||
</Design.PreviewWith> |
||||
|
||||
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||
<ControlTheme |
||||
x:Key="BetterComboBoxItemHybridModelTheme" |
||||
BasedOn="{StaticResource {x:Type ComboBoxItem}}" |
||||
TargetType="ComboBoxItem"> |
||||
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" /> |
||||
<Setter Property="ToolTip.Tip"> |
||||
<Template> |
||||
<sg:SpacedGrid |
||||
x:DataType="models:HybridModelFile" |
||||
ColumnDefinitions="Auto,*" |
||||
ColumnSpacing="6" |
||||
RowSpacing="0"> |
||||
<!-- Image --> |
||||
<controls:BetterAdvancedImage |
||||
Width="64" |
||||
Height="96" |
||||
CornerRadius="6" |
||||
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}" |
||||
Stretch="UniformToFill" |
||||
StretchDirection="Both" /> |
||||
<StackPanel |
||||
Grid.Column="1" |
||||
MaxWidth="300" |
||||
VerticalAlignment="Stretch"> |
||||
<!-- Title --> |
||||
<TextBlock |
||||
Margin="0,0,0,4" |
||||
HorizontalAlignment="Left" |
||||
FontSize="14" |
||||
FontWeight="Medium" |
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" |
||||
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}" |
||||
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
<!-- Version --> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
HorizontalAlignment="Left" |
||||
FontSize="13" |
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}" |
||||
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}" |
||||
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
<!-- Path --> |
||||
<TextBlock |
||||
HorizontalAlignment="Left" |
||||
FontSize="13" |
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}" |
||||
Text="{Binding FileName}" |
||||
TextWrapping="Wrap" /> |
||||
</StackPanel> |
||||
</sg:SpacedGrid> |
||||
</Template> |
||||
</Setter> |
||||
</ControlTheme> |
||||
|
||||
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||
<ControlTheme |
||||
x:Key="{x:Type controls:BetterComboBox}" |
||||
BasedOn="{StaticResource {x:Type ComboBox}}" |
||||
TargetType="controls:BetterComboBox" /> |
||||
|
||||
<ControlTheme |
||||
x:Key="BetterComboBoxHybridModelTheme" |
||||
BasedOn="{StaticResource {x:Type controls:BetterComboBox}}" |
||||
TargetType="controls:BetterComboBox"> |
||||
|
||||
<ControlTheme.Resources> |
||||
<controls:HybridModelTemplateSelector x:Key="HybridModelTemplateSelector"> |
||||
<DataTemplate x:Key="{x:Static models:HybridModelType.Downloadable}" DataType="models:HybridModelFile"> |
||||
<Grid ColumnDefinitions="*,Auto"> |
||||
<TextBlock Foreground="{DynamicResource ThemeGreyColor}" Text="{Binding ShortDisplayName}" /> |
||||
<Button |
||||
Grid.Column="1" |
||||
Margin="8,0,0,0" |
||||
Padding="0" |
||||
Classes="transparent-full"> |
||||
<fluentIcons:SymbolIcon |
||||
VerticalAlignment="Center" |
||||
FontSize="18" |
||||
Foreground="{DynamicResource ThemeGreyColor}" |
||||
IsFilled="True" |
||||
Symbol="CloudArrowDown" /> |
||||
</Button> |
||||
</Grid> |
||||
</DataTemplate> |
||||
|
||||
<DataTemplate x:Key="{x:Static models:HybridModelType.Local}" DataType="models:HybridModelFile"> |
||||
<sg:SpacedGrid |
||||
HorizontalAlignment="Stretch" |
||||
ColumnDefinitions="Auto,*" |
||||
ColumnSpacing="8" |
||||
TextBlock.TextTrimming="CharacterEllipsis" |
||||
TextBlock.TextWrapping="NoWrap"> |
||||
<controls:BetterAdvancedImage |
||||
Grid.RowSpan="2" |
||||
Width="42" |
||||
Height="42" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
Source="{Binding Local.PreviewImageFullPathGlobal}" |
||||
Stretch="UniformToFill" |
||||
StretchDirection="Both"> |
||||
<controls:BetterAdvancedImage.Clip> |
||||
<EllipseGeometry Rect="0,0,42,42" /> |
||||
</controls:BetterAdvancedImage.Clip> |
||||
</controls:BetterAdvancedImage> |
||||
|
||||
<!-- Text --> |
||||
<sg:SpacedGrid |
||||
Grid.Row="1" |
||||
Grid.Column="1" |
||||
RowDefinitions="Auto,Auto,Auto" |
||||
RowSpacing="1"> |
||||
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" /> |
||||
<TextBlock |
||||
Grid.Row="1" |
||||
FontSize="12" |
||||
FontWeight="Regular" |
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" |
||||
Text="{Binding Local.DisplayModelVersion}" |
||||
TextTrimming="CharacterEllipsis" /> |
||||
<TextBlock |
||||
Grid.Row="2" |
||||
FontSize="11" |
||||
FontWeight="Normal" |
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}" |
||||
Text="{Binding Local.DisplayModelFileName}" /> |
||||
</sg:SpacedGrid> |
||||
</sg:SpacedGrid> |
||||
</DataTemplate> |
||||
|
||||
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile"> |
||||
<StackPanel> |
||||
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" /> |
||||
</StackPanel> |
||||
</DataTemplate> |
||||
</controls:HybridModelTemplateSelector> |
||||
|
||||
<controls:HybridModelTemplateSelector x:Key="HybridModelSelectionBoxTemplateSelector"> |
||||
<DataTemplate x:Key="{x:Static models:HybridModelType.Downloadable}" DataType="models:HybridModelFile"> |
||||
<Grid ColumnDefinitions="*,Auto"> |
||||
<TextBlock Foreground="{DynamicResource ThemeGreyColor}" Text="{Binding ShortDisplayName}" /> |
||||
<Button |
||||
Grid.Column="1" |
||||
Margin="8,0,0,0" |
||||
Padding="0" |
||||
Classes="transparent-full"> |
||||
<fluentIcons:SymbolIcon |
||||
VerticalAlignment="Center" |
||||
FontSize="18" |
||||
Foreground="{DynamicResource ThemeGreyColor}" |
||||
IsFilled="True" |
||||
Symbol="CloudArrowDown" /> |
||||
</Button> |
||||
</Grid> |
||||
</DataTemplate> |
||||
|
||||
<DataTemplate x:Key="{x:Static models:HybridModelType.Local}" DataType="models:HybridModelFile"> |
||||
<sg:SpacedGrid |
||||
HorizontalAlignment="Stretch" |
||||
ColumnDefinitions="Auto,*" |
||||
ColumnSpacing="8" |
||||
TextBlock.TextTrimming="CharacterEllipsis" |
||||
TextBlock.TextWrapping="NoWrap"> |
||||
<controls:BetterAdvancedImage |
||||
Grid.RowSpan="2" |
||||
Width="36" |
||||
Height="36" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
Source="{Binding Local.PreviewImageFullPathGlobal}" |
||||
Stretch="UniformToFill" |
||||
StretchDirection="Both"> |
||||
<controls:BetterAdvancedImage.Clip> |
||||
<EllipseGeometry Rect="0,0,36,36" /> |
||||
</controls:BetterAdvancedImage.Clip> |
||||
</controls:BetterAdvancedImage> |
||||
|
||||
<!-- Text --> |
||||
<sg:SpacedGrid |
||||
Grid.Row="1" |
||||
Grid.Column="1" |
||||
RowDefinitions="Auto,Auto" |
||||
RowSpacing="1"> |
||||
|
||||
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" /> |
||||
<TextBlock |
||||
Grid.Row="1" |
||||
FontSize="12" |
||||
FontWeight="Regular" |
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" |
||||
Text="{Binding Local.DisplayModelVersion}" |
||||
TextTrimming="CharacterEllipsis" /> |
||||
</sg:SpacedGrid> |
||||
</sg:SpacedGrid> |
||||
</DataTemplate> |
||||
|
||||
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile"> |
||||
<StackPanel> |
||||
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" /> |
||||
</StackPanel> |
||||
</DataTemplate> |
||||
</controls:HybridModelTemplateSelector> |
||||
</ControlTheme.Resources> |
||||
|
||||
<Setter Property="TextBlock.TextWrapping" Value="NoWrap" /> |
||||
<Setter Property="SelectionBoxItemTemplate" Value="{StaticResource HybridModelSelectionBoxTemplateSelector}" /> |
||||
<Setter Property="ItemTemplate" Value="{StaticResource HybridModelTemplateSelector}" /> |
||||
<Setter Property="ItemContainerTheme" Value="{StaticResource BetterComboBoxItemHybridModelTheme}" /> |
||||
|
||||
<Style Selector="^ /template/ Popup#PART_Popup"> |
||||
<Setter Property="Width" Value="400" /> |
||||
<Setter Property="Placement" Value="Bottom" /> |
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" /> |
||||
<Setter Property="Effect"> |
||||
<DropShadowEffect |
||||
BlurRadius="32" |
||||
Opacity="0.6" |
||||
Color="#FF000000" /> |
||||
</Setter> |
||||
</Style> |
||||
|
||||
</ControlTheme> |
||||
|
||||
</ResourceDictionary> |
@ -0,0 +1,13 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Models.Packages.Extensions; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
public partial class ExtensionViewModel() : ViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
private bool isSelected; |
||||
|
||||
public ExtensionBase Extension { get; init; } |
||||
} |
@ -0,0 +1,245 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.ComponentModel.DataAnnotations; |
||||
using System.Linq; |
||||
using System.Text.Json.Nodes; |
||||
using System.Text.Json.Serialization; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using NLog; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Models.Inference; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.ViewModels.Inference.Video; |
||||
using StabilityMatrix.Avalonia.Views.Inference; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api.Comfy; |
||||
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Inference; |
||||
|
||||
[View(typeof(InferenceImageToVideoView), persistent: true)] |
||||
[ManagedService] |
||||
[Transient] |
||||
public partial class InferenceImageToVideoViewModel |
||||
: InferenceGenerationViewModelBase, |
||||
IParametersLoadableState |
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
|
||||
private readonly INotificationService notificationService; |
||||
private readonly IModelIndexService modelIndexService; |
||||
|
||||
[JsonIgnore] |
||||
public StackCardViewModel StackCardViewModel { get; } |
||||
|
||||
[JsonPropertyName("Model")] |
||||
public ImgToVidModelCardViewModel ModelCardViewModel { get; } |
||||
|
||||
[JsonPropertyName("Sampler")] |
||||
public SamplerCardViewModel SamplerCardViewModel { get; } |
||||
|
||||
[JsonPropertyName("BatchSize")] |
||||
public BatchSizeCardViewModel BatchSizeCardViewModel { get; } |
||||
|
||||
[JsonPropertyName("Seed")] |
||||
public SeedCardViewModel SeedCardViewModel { get; } |
||||
|
||||
[JsonPropertyName("ImageLoader")] |
||||
public SelectImageCardViewModel SelectImageCardViewModel { get; } |
||||
|
||||
[JsonPropertyName("Conditioning")] |
||||
public SvdImgToVidConditioningViewModel SvdImgToVidConditioningViewModel { get; } |
||||
|
||||
[JsonPropertyName("VideoOutput")] |
||||
public VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel { get; } |
||||
|
||||
public InferenceImageToVideoViewModel( |
||||
INotificationService notificationService, |
||||
IInferenceClientManager inferenceClientManager, |
||||
ISettingsManager settingsManager, |
||||
ServiceManager<ViewModelBase> vmFactory, |
||||
IModelIndexService modelIndexService |
||||
) |
||||
: base(vmFactory, inferenceClientManager, notificationService, settingsManager) |
||||
{ |
||||
this.notificationService = notificationService; |
||||
this.modelIndexService = modelIndexService; |
||||
|
||||
// Get sub view models from service manager |
||||
|
||||
SeedCardViewModel = vmFactory.Get<SeedCardViewModel>(); |
||||
SeedCardViewModel.GenerateNewSeed(); |
||||
|
||||
ModelCardViewModel = vmFactory.Get<ImgToVidModelCardViewModel>(); |
||||
|
||||
SamplerCardViewModel = vmFactory.Get<SamplerCardViewModel>(samplerCard => |
||||
{ |
||||
samplerCard.IsDimensionsEnabled = true; |
||||
samplerCard.IsCfgScaleEnabled = true; |
||||
samplerCard.IsSamplerSelectionEnabled = true; |
||||
samplerCard.IsSchedulerSelectionEnabled = true; |
||||
samplerCard.CfgScale = 2.5d; |
||||
samplerCard.SelectedSampler = ComfySampler.Euler; |
||||
samplerCard.SelectedScheduler = ComfyScheduler.Karras; |
||||
samplerCard.IsDenoiseStrengthEnabled = true; |
||||
samplerCard.DenoiseStrength = 1.0f; |
||||
}); |
||||
|
||||
BatchSizeCardViewModel = vmFactory.Get<BatchSizeCardViewModel>(); |
||||
|
||||
SelectImageCardViewModel = vmFactory.Get<SelectImageCardViewModel>(); |
||||
SvdImgToVidConditioningViewModel = vmFactory.Get<SvdImgToVidConditioningViewModel>(); |
||||
VideoOutputSettingsCardViewModel = vmFactory.Get<VideoOutputSettingsCardViewModel>(); |
||||
|
||||
StackCardViewModel = vmFactory.Get<StackCardViewModel>(); |
||||
StackCardViewModel.AddCards( |
||||
ModelCardViewModel, |
||||
SvdImgToVidConditioningViewModel, |
||||
SamplerCardViewModel, |
||||
SeedCardViewModel, |
||||
VideoOutputSettingsCardViewModel, |
||||
BatchSizeCardViewModel |
||||
); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
protected override void BuildPrompt(BuildPromptEventArgs args) |
||||
{ |
||||
base.BuildPrompt(args); |
||||
|
||||
var builder = args.Builder; |
||||
|
||||
builder.Connections.Seed = args.SeedOverride switch |
||||
{ |
||||
{ } seed => Convert.ToUInt64(seed), |
||||
_ => Convert.ToUInt64(SeedCardViewModel.Seed) |
||||
}; |
||||
|
||||
// Load models |
||||
ModelCardViewModel.ApplyStep(args); |
||||
|
||||
// Setup latent from image |
||||
var imageLoad = builder.Nodes.AddTypedNode( |
||||
new ComfyNodeBuilder.LoadImage |
||||
{ |
||||
Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"), |
||||
Image = |
||||
SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference") |
||||
?? throw new ValidationException() |
||||
} |
||||
); |
||||
builder.Connections.Primary = imageLoad.Output1; |
||||
builder.Connections.PrimarySize = SelectImageCardViewModel.CurrentBitmapSize; |
||||
|
||||
// Setup img2vid stuff |
||||
// Set width & height from SamplerCard |
||||
SvdImgToVidConditioningViewModel.Width = SamplerCardViewModel.Width; |
||||
SvdImgToVidConditioningViewModel.Height = SamplerCardViewModel.Height; |
||||
SvdImgToVidConditioningViewModel.ApplyStep(args); |
||||
|
||||
// Setup Sampler and Refiner if enabled |
||||
SamplerCardViewModel.ApplyStep(args); |
||||
|
||||
// Animated webp output |
||||
VideoOutputSettingsCardViewModel.ApplyStep(args); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
protected override IEnumerable<ImageSource> GetInputImages() |
||||
{ |
||||
if (SelectImageCardViewModel.ImageSource is { } image) |
||||
{ |
||||
yield return image; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
protected override async Task GenerateImageImpl( |
||||
GenerateOverrides overrides, |
||||
CancellationToken cancellationToken |
||||
) |
||||
{ |
||||
if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// If enabled, randomize the seed |
||||
var seedCard = StackCardViewModel.GetCard<SeedCardViewModel>(); |
||||
if (overrides is not { UseCurrentSeed: true } && seedCard.IsRandomizeEnabled) |
||||
{ |
||||
seedCard.GenerateNewSeed(); |
||||
} |
||||
|
||||
var batches = BatchSizeCardViewModel.BatchCount; |
||||
|
||||
var batchArgs = new List<ImageGenerationEventArgs>(); |
||||
|
||||
for (var i = 0; i < batches; i++) |
||||
{ |
||||
var seed = seedCard.Seed + i; |
||||
|
||||
var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed }; |
||||
BuildPrompt(buildPromptArgs); |
||||
|
||||
var generationArgs = new ImageGenerationEventArgs |
||||
{ |
||||
Client = ClientManager.Client, |
||||
Nodes = buildPromptArgs.Builder.ToNodeDictionary(), |
||||
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), |
||||
Parameters = SaveStateToParameters(new GenerationParameters()), |
||||
Project = InferenceProjectDocument.FromLoadable(this), |
||||
// Only clear output images on the first batch |
||||
ClearOutputImages = i == 0 |
||||
}; |
||||
|
||||
batchArgs.Add(generationArgs); |
||||
} |
||||
|
||||
// Run batches |
||||
foreach (var args in batchArgs) |
||||
{ |
||||
await RunGeneration(args, cancellationToken); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void LoadStateFromParameters(GenerationParameters parameters) |
||||
{ |
||||
SamplerCardViewModel.LoadStateFromParameters(parameters); |
||||
ModelCardViewModel.LoadStateFromParameters(parameters); |
||||
SvdImgToVidConditioningViewModel.LoadStateFromParameters(parameters); |
||||
VideoOutputSettingsCardViewModel.LoadStateFromParameters(parameters); |
||||
|
||||
SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public GenerationParameters SaveStateToParameters(GenerationParameters parameters) |
||||
{ |
||||
parameters = SamplerCardViewModel.SaveStateToParameters(parameters); |
||||
parameters = ModelCardViewModel.SaveStateToParameters(parameters); |
||||
parameters = SvdImgToVidConditioningViewModel.SaveStateToParameters(parameters); |
||||
parameters = VideoOutputSettingsCardViewModel.SaveStateToParameters(parameters); |
||||
|
||||
parameters.Seed = (ulong)SeedCardViewModel.Seed; |
||||
|
||||
return parameters; |
||||
} |
||||
|
||||
// Migration for v2 deserialization |
||||
public override void LoadStateFromJsonObject(JsonObject state, int version) |
||||
{ |
||||
if (version > 2) |
||||
{ |
||||
LoadStateFromJsonObject(state); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,35 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Models.Inference; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video; |
||||
|
||||
[View(typeof(ModelCard))] |
||||
[ManagedService] |
||||
[Transient] |
||||
public class ImgToVidModelCardViewModel : ModelCardViewModel |
||||
{ |
||||
public ImgToVidModelCardViewModel(IInferenceClientManager clientManager) |
||||
: base(clientManager) |
||||
{ |
||||
DisableSettings = true; |
||||
} |
||||
|
||||
public override void ApplyStep(ModuleApplyStepEventArgs e) |
||||
{ |
||||
var imgToVidLoader = e.Nodes.AddTypedNode( |
||||
new ComfyNodeBuilder.ImageOnlyCheckpointLoader |
||||
{ |
||||
Name = "ImageOnlyCheckpointLoader", |
||||
CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected") |
||||
} |
||||
); |
||||
|
||||
e.Builder.Connections.Base.Model = imgToVidLoader.Output1; |
||||
e.Builder.Connections.BaseClipVision = imgToVidLoader.Output2; |
||||
e.Builder.Connections.Base.VAE = imgToVidLoader.Output3; |
||||
} |
||||
} |
@ -0,0 +1,104 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Models.Inference; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; |
||||
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video; |
||||
|
||||
[View(typeof(VideoGenerationSettingsCard))] |
||||
[ManagedService] |
||||
[Transient] |
||||
public partial class SvdImgToVidConditioningViewModel |
||||
: LoadableViewModelBase, |
||||
IParametersLoadableState, |
||||
IComfyStep |
||||
{ |
||||
[ObservableProperty] |
||||
private int width = 1024; |
||||
|
||||
[ObservableProperty] |
||||
private int height = 576; |
||||
|
||||
[ObservableProperty] |
||||
private int numFrames = 14; |
||||
|
||||
[ObservableProperty] |
||||
private int motionBucketId = 127; |
||||
|
||||
[ObservableProperty] |
||||
private int fps = 6; |
||||
|
||||
[ObservableProperty] |
||||
private double augmentationLevel; |
||||
|
||||
[ObservableProperty] |
||||
private double minCfg = 1.0d; |
||||
|
||||
public void LoadStateFromParameters(GenerationParameters parameters) |
||||
{ |
||||
Width = parameters.Width; |
||||
Height = parameters.Height; |
||||
NumFrames = parameters.FrameCount; |
||||
MotionBucketId = parameters.MotionBucketId; |
||||
Fps = parameters.Fps; |
||||
AugmentationLevel = parameters.AugmentationLevel; |
||||
MinCfg = parameters.MinCfg; |
||||
} |
||||
|
||||
public GenerationParameters SaveStateToParameters(GenerationParameters parameters) |
||||
{ |
||||
return parameters with |
||||
{ |
||||
FrameCount = NumFrames, |
||||
MotionBucketId = MotionBucketId, |
||||
Fps = Fps, |
||||
AugmentationLevel = AugmentationLevel, |
||||
MinCfg = MinCfg, |
||||
}; |
||||
} |
||||
|
||||
public void ApplyStep(ModuleApplyStepEventArgs e) |
||||
{ |
||||
// do VideoLinearCFGGuidance stuff first |
||||
var cfgGuidanceNode = e.Nodes.AddTypedNode( |
||||
new ComfyNodeBuilder.VideoLinearCFGGuidance |
||||
{ |
||||
Name = e.Nodes.GetUniqueName("LinearCfgGuidance"), |
||||
Model = |
||||
e.Builder.Connections.Base.Model ?? throw new ValidationException("Model not selected"), |
||||
MinCfg = MinCfg |
||||
} |
||||
); |
||||
|
||||
e.Builder.Connections.Base.Model = cfgGuidanceNode.Output; |
||||
|
||||
// then do the SVD stuff |
||||
var svdImgToVidConditioningNode = e.Nodes.AddTypedNode( |
||||
new ComfyNodeBuilder.SVD_img2vid_Conditioning |
||||
{ |
||||
ClipVision = e.Builder.Connections.BaseClipVision!, |
||||
InitImage = e.Builder.GetPrimaryAsImage(), |
||||
Vae = e.Builder.Connections.Base.VAE!, |
||||
Name = e.Nodes.GetUniqueName("SvdImgToVidConditioning"), |
||||
Width = Width, |
||||
Height = Height, |
||||
VideoFrames = NumFrames, |
||||
MotionBucketId = MotionBucketId, |
||||
Fps = Fps, |
||||
AugmentationLevel = AugmentationLevel |
||||
} |
||||
); |
||||
|
||||
e.Builder.Connections.Base.Conditioning = new ConditioningConnections( |
||||
svdImgToVidConditioningNode.Output1, |
||||
svdImgToVidConditioningNode.Output2 |
||||
); |
||||
e.Builder.Connections.Primary = svdImgToVidConditioningNode.Output3; |
||||
} |
||||
} |
@ -0,0 +1,94 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Models.Inference; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video; |
||||
|
||||
[View(typeof(VideoOutputSettingsCard))] |
||||
[ManagedService] |
||||
[Transient] |
||||
public partial class VideoOutputSettingsCardViewModel |
||||
: LoadableViewModelBase, |
||||
IParametersLoadableState, |
||||
IComfyStep |
||||
{ |
||||
[ObservableProperty] |
||||
private double fps = 6; |
||||
|
||||
[ObservableProperty] |
||||
private bool lossless = true; |
||||
|
||||
[ObservableProperty] |
||||
private int quality = 85; |
||||
|
||||
[ObservableProperty] |
||||
private VideoOutputMethod selectedMethod = VideoOutputMethod.Default; |
||||
|
||||
[ObservableProperty] |
||||
private List<VideoOutputMethod> availableMethods = Enum.GetValues<VideoOutputMethod>().ToList(); |
||||
|
||||
public void LoadStateFromParameters(GenerationParameters parameters) |
||||
{ |
||||
Fps = parameters.OutputFps; |
||||
Lossless = parameters.Lossless; |
||||
Quality = parameters.VideoQuality; |
||||
|
||||
if (string.IsNullOrWhiteSpace(parameters.VideoOutputMethod)) |
||||
return; |
||||
|
||||
SelectedMethod = Enum.TryParse<VideoOutputMethod>(parameters.VideoOutputMethod, true, out var method) |
||||
? method |
||||
: VideoOutputMethod.Default; |
||||
} |
||||
|
||||
public GenerationParameters SaveStateToParameters(GenerationParameters parameters) |
||||
{ |
||||
return parameters with |
||||
{ |
||||
OutputFps = Fps, |
||||
Lossless = Lossless, |
||||
VideoQuality = Quality, |
||||
VideoOutputMethod = SelectedMethod.ToString(), |
||||
}; |
||||
} |
||||
|
||||
public void ApplyStep(ModuleApplyStepEventArgs e) |
||||
{ |
||||
if (e.Builder.Connections.Primary is null) |
||||
throw new ArgumentException("No Primary"); |
||||
|
||||
var image = e.Builder.Connections.Primary.Match( |
||||
_ => |
||||
e.Builder.GetPrimaryAsImage( |
||||
e.Builder.Connections.PrimaryVAE |
||||
?? e.Builder.Connections.Refiner.VAE |
||||
?? e.Builder.Connections.Base.VAE |
||||
?? throw new ArgumentException("No Primary, Refiner, or Base VAE") |
||||
), |
||||
image => image |
||||
); |
||||
|
||||
var outputStep = e.Nodes.AddTypedNode( |
||||
new ComfyNodeBuilder.SaveAnimatedWEBP |
||||
{ |
||||
Name = e.Nodes.GetUniqueName("SaveAnimatedWEBP"), |
||||
Images = image, |
||||
FilenamePrefix = "InferenceVideo", |
||||
Fps = Fps, |
||||
Lossless = Lossless, |
||||
Quality = Quality, |
||||
Method = SelectedMethod.ToString().ToLowerInvariant() |
||||
} |
||||
); |
||||
|
||||
e.Builder.Connections.OutputNodes.Add(outputStep); |
||||
} |
||||
} |
@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using DynamicData; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
using StabilityMatrix.Avalonia.ViewModels.PackageManager; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using Symbol = FluentIcons.Common.Symbol; |
||||
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(NewPackageManagerPage))] |
||||
[Singleton] |
||||
public partial class NewPackageManagerViewModel : PageViewModelBase |
||||
{ |
||||
public override string Title => "Packages"; |
||||
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true }; |
||||
|
||||
public IReadOnlyList<PageViewModelBase> SubPages { get; } |
||||
|
||||
[ObservableProperty] |
||||
private ObservableCollection<PageViewModelBase> currentPagePath = []; |
||||
|
||||
[ObservableProperty] |
||||
private PageViewModelBase? currentPage; |
||||
|
||||
public NewPackageManagerViewModel(ServiceManager<ViewModelBase> vmFactory) |
||||
{ |
||||
SubPages = new PageViewModelBase[] |
||||
{ |
||||
vmFactory.Get<PackageManagerViewModel>(), |
||||
vmFactory.Get<PackageInstallBrowserViewModel>(), |
||||
}; |
||||
|
||||
CurrentPagePath.AddRange(SubPages); |
||||
|
||||
CurrentPage = SubPages[0]; |
||||
} |
||||
|
||||
partial void OnCurrentPageChanged(PageViewModelBase? value) |
||||
{ |
||||
if (value is null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
if (value is PackageManagerViewModel) |
||||
{ |
||||
CurrentPagePath.Clear(); |
||||
CurrentPagePath.Add(value); |
||||
} |
||||
else if (value is PackageInstallDetailViewModel) |
||||
{ |
||||
CurrentPagePath.Add(value); |
||||
} |
||||
else |
||||
{ |
||||
CurrentPagePath.Clear(); |
||||
CurrentPagePath.AddRange(new[] { SubPages[0], value }); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,143 @@
|
||||
using System; |
||||
using System.Reactive.Linq; |
||||
using Avalonia.Threading; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using DynamicData; |
||||
using DynamicData.Alias; |
||||
using DynamicData.Binding; |
||||
using FluentAvalonia.UI.Controls; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Avalonia.Animations; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.Views.PackageManager; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Helper.Factory; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Packages; |
||||
using StabilityMatrix.Core.Python; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; |
||||
|
||||
[View(typeof(PackageInstallBrowserView))] |
||||
[Transient, ManagedService] |
||||
public partial class PackageInstallBrowserViewModel : PageViewModelBase |
||||
{ |
||||
private readonly INavigationService<NewPackageManagerViewModel> packageNavigationService; |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly INotificationService notificationService; |
||||
private readonly ILogger<PackageInstallDetailViewModel> logger; |
||||
private readonly IPyRunner pyRunner; |
||||
private readonly IPrerequisiteHelper prerequisiteHelper; |
||||
|
||||
[ObservableProperty] |
||||
private bool showIncompatiblePackages; |
||||
|
||||
[ObservableProperty] |
||||
private string searchFilter = string.Empty; |
||||
|
||||
private SourceCache<BasePackage, string> packageSource = new(p => p.GithubUrl); |
||||
|
||||
public IObservableCollection<BasePackage> InferencePackages { get; } = |
||||
new ObservableCollectionExtended<BasePackage>(); |
||||
|
||||
public IObservableCollection<BasePackage> TrainingPackages { get; } = |
||||
new ObservableCollectionExtended<BasePackage>(); |
||||
|
||||
public PackageInstallBrowserViewModel( |
||||
IPackageFactory packageFactory, |
||||
INavigationService<NewPackageManagerViewModel> packageNavigationService, |
||||
ISettingsManager settingsManager, |
||||
INotificationService notificationService, |
||||
ILogger<PackageInstallDetailViewModel> logger, |
||||
IPyRunner pyRunner, |
||||
IPrerequisiteHelper prerequisiteHelper |
||||
) |
||||
{ |
||||
this.packageNavigationService = packageNavigationService; |
||||
this.settingsManager = settingsManager; |
||||
this.notificationService = notificationService; |
||||
this.logger = logger; |
||||
this.pyRunner = pyRunner; |
||||
this.prerequisiteHelper = prerequisiteHelper; |
||||
|
||||
var incompatiblePredicate = this.WhenPropertyChanged(vm => vm.ShowIncompatiblePackages) |
||||
.Select(_ => new Func<BasePackage, bool>(p => p.IsCompatible || ShowIncompatiblePackages)) |
||||
.AsObservable(); |
||||
|
||||
var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchFilter) |
||||
.Select( |
||||
_ => |
||||
new Func<BasePackage, bool>( |
||||
p => p.DisplayName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase) |
||||
) |
||||
) |
||||
.AsObservable(); |
||||
|
||||
packageSource |
||||
.Connect() |
||||
.DeferUntilLoaded() |
||||
.Filter(incompatiblePredicate) |
||||
.Filter(searchPredicate) |
||||
.Where(p => p is { PackageType: PackageType.SdInference }) |
||||
.Sort( |
||||
SortExpressionComparer<BasePackage> |
||||
.Ascending(p => p.InstallerSortOrder) |
||||
.ThenByAscending(p => p.DisplayName) |
||||
) |
||||
.Bind(InferencePackages) |
||||
.Subscribe(); |
||||
|
||||
packageSource |
||||
.Connect() |
||||
.DeferUntilLoaded() |
||||
.Filter(incompatiblePredicate) |
||||
.Filter(searchPredicate) |
||||
.Where(p => p is { PackageType: PackageType.SdTraining }) |
||||
.Sort( |
||||
SortExpressionComparer<BasePackage> |
||||
.Ascending(p => p.InstallerSortOrder) |
||||
.ThenByAscending(p => p.DisplayName) |
||||
) |
||||
.Bind(TrainingPackages) |
||||
.Subscribe(); |
||||
|
||||
packageSource.EditDiff( |
||||
packageFactory.GetAllAvailablePackages(), |
||||
(a, b) => a.GithubUrl == b.GithubUrl |
||||
); |
||||
} |
||||
|
||||
public override string Title => "Add Package"; |
||||
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Add }; |
||||
|
||||
public void OnPackageSelected(BasePackage? package) |
||||
{ |
||||
if (package is null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var vm = new PackageInstallDetailViewModel( |
||||
package, |
||||
settingsManager, |
||||
notificationService, |
||||
logger, |
||||
pyRunner, |
||||
prerequisiteHelper, |
||||
packageNavigationService |
||||
); |
||||
|
||||
Dispatcher.UIThread.Post( |
||||
() => packageNavigationService.NavigateTo(vm, BetterSlideNavigationTransition.PageSlideFromRight), |
||||
DispatcherPriority.Send |
||||
); |
||||
} |
||||
|
||||
public void ClearSearchQuery() |
||||
{ |
||||
SearchFilter = string.Empty; |
||||
} |
||||
} |
@ -0,0 +1,254 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Notifications; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using FluentAvalonia.UI.Controls; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Avalonia.Extensions; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.PackageModification; |
||||
using StabilityMatrix.Core.Models.Packages; |
||||
using StabilityMatrix.Core.Python; |
||||
using StabilityMatrix.Core.Services; |
||||
using PackageInstallDetailView = StabilityMatrix.Avalonia.Views.PackageManager.PackageInstallDetailView; |
||||
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; |
||||
|
||||
[View(typeof(PackageInstallDetailView))] |
||||
public partial class PackageInstallDetailViewModel( |
||||
BasePackage package, |
||||
ISettingsManager settingsManager, |
||||
INotificationService notificationService, |
||||
ILogger<PackageInstallDetailViewModel> logger, |
||||
IPyRunner pyRunner, |
||||
IPrerequisiteHelper prerequisiteHelper, |
||||
INavigationService<NewPackageManagerViewModel> packageNavigationService |
||||
) : PageViewModelBase |
||||
{ |
||||
public BasePackage SelectedPackage { get; } = package; |
||||
public override string Title { get; } = package.DisplayName; |
||||
public override IconSource IconSource => new SymbolIconSource(); |
||||
|
||||
public string FullInstallPath => Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); |
||||
public bool ShowReleaseMode => SelectedPackage.ShouldIgnoreReleases == false; |
||||
|
||||
public string ReleaseLabelText => IsReleaseMode ? Resources.Label_Version : Resources.Label_Branch; |
||||
|
||||
public bool ShowTorchVersionOptions => SelectedTorchVersion != TorchVersion.None; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(FullInstallPath))] |
||||
private string installName = package.DisplayName; |
||||
|
||||
[ObservableProperty] |
||||
private bool showDuplicateWarning; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(ReleaseLabelText))] |
||||
private bool isReleaseMode; |
||||
|
||||
[ObservableProperty] |
||||
private IEnumerable<PackageVersion> availableVersions = new List<PackageVersion>(); |
||||
|
||||
[ObservableProperty] |
||||
private PackageVersion? selectedVersion; |
||||
|
||||
[ObservableProperty] |
||||
private SharedFolderMethod selectedSharedFolderMethod; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(ShowTorchVersionOptions))] |
||||
private TorchVersion selectedTorchVersion; |
||||
|
||||
[ObservableProperty] |
||||
private ObservableCollection<GitCommit>? availableCommits; |
||||
|
||||
[ObservableProperty] |
||||
private GitCommit? selectedCommit; |
||||
|
||||
private PackageVersionOptions? allOptions; |
||||
|
||||
public override async Task OnLoadedAsync() |
||||
{ |
||||
if (Design.IsDesignMode) |
||||
return; |
||||
|
||||
OnInstallNameChanged(InstallName); |
||||
|
||||
allOptions = await SelectedPackage.GetAllVersionOptions(); |
||||
if (ShowReleaseMode) |
||||
{ |
||||
IsReleaseMode = true; |
||||
} |
||||
else |
||||
{ |
||||
UpdateVersions(); |
||||
await UpdateCommits(SelectedPackage.MainBranch); |
||||
} |
||||
|
||||
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion(); |
||||
SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task Install() |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(InstallName)) |
||||
{ |
||||
notificationService.Show( |
||||
new Notification( |
||||
"Package name is empty", |
||||
"Please enter a name for the package", |
||||
NotificationType.Error |
||||
) |
||||
); |
||||
return; |
||||
} |
||||
|
||||
var setPackageInstallingStep = new SetPackageInstallingStep(settingsManager, InstallName); |
||||
|
||||
var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); |
||||
if (Directory.Exists(installLocation)) |
||||
{ |
||||
var installPath = new DirectoryPath(installLocation); |
||||
await installPath.DeleteVerboseAsync(logger); |
||||
} |
||||
|
||||
var prereqStep = new SetupPrerequisitesStep(prerequisiteHelper, pyRunner); |
||||
|
||||
var downloadOptions = new DownloadPackageVersionOptions(); |
||||
var installedVersion = new InstalledPackageVersion(); |
||||
if (IsReleaseMode) |
||||
{ |
||||
downloadOptions.VersionTag = |
||||
SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); |
||||
downloadOptions.IsLatest = AvailableVersions?.First().TagName == downloadOptions.VersionTag; |
||||
downloadOptions.IsPrerelease = SelectedVersion.IsPrerelease; |
||||
|
||||
installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag; |
||||
installedVersion.IsPrerelease = SelectedVersion.IsPrerelease; |
||||
} |
||||
else |
||||
{ |
||||
downloadOptions.CommitHash = |
||||
SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null"); |
||||
downloadOptions.BranchName = |
||||
SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); |
||||
downloadOptions.IsLatest = AvailableCommits?.First().Sha == SelectedCommit.Sha; |
||||
|
||||
installedVersion.InstalledBranch = |
||||
SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); |
||||
installedVersion.InstalledCommitSha = downloadOptions.CommitHash; |
||||
} |
||||
|
||||
var downloadStep = new DownloadPackageVersionStep(SelectedPackage, installLocation, downloadOptions); |
||||
var installStep = new InstallPackageStep( |
||||
SelectedPackage, |
||||
SelectedTorchVersion, |
||||
SelectedSharedFolderMethod, |
||||
downloadOptions, |
||||
installLocation |
||||
); |
||||
|
||||
var setupModelFoldersStep = new SetupModelFoldersStep( |
||||
SelectedPackage, |
||||
SelectedSharedFolderMethod, |
||||
installLocation |
||||
); |
||||
|
||||
var package = new InstalledPackage |
||||
{ |
||||
DisplayName = InstallName, |
||||
LibraryPath = Path.Combine("Packages", InstallName), |
||||
Id = Guid.NewGuid(), |
||||
PackageName = SelectedPackage.Name, |
||||
Version = installedVersion, |
||||
LaunchCommand = SelectedPackage.LaunchCommand, |
||||
LastUpdateCheck = DateTimeOffset.Now, |
||||
PreferredTorchVersion = SelectedTorchVersion, |
||||
PreferredSharedFolderMethod = SelectedSharedFolderMethod |
||||
}; |
||||
|
||||
var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package); |
||||
|
||||
var steps = new List<IPackageStep> |
||||
{ |
||||
setPackageInstallingStep, |
||||
prereqStep, |
||||
downloadStep, |
||||
installStep, |
||||
setupModelFoldersStep, |
||||
addInstalledPackageStep |
||||
}; |
||||
|
||||
var runner = new PackageModificationRunner { ShowDialogOnStart = true }; |
||||
EventManager.Instance.OnPackageInstallProgressAdded(runner); |
||||
await runner.ExecuteSteps(steps.ToList()); |
||||
|
||||
if (!runner.Failed) |
||||
{ |
||||
EventManager.Instance.OnInstalledPackagesChanged(); |
||||
notificationService.Show( |
||||
"Package Install Complete", |
||||
$"{InstallName} installed successfully", |
||||
NotificationType.Success |
||||
); |
||||
} |
||||
} |
||||
|
||||
private void UpdateVersions() |
||||
{ |
||||
AvailableVersions = |
||||
IsReleaseMode && ShowReleaseMode ? allOptions.AvailableVersions : allOptions.AvailableBranches; |
||||
|
||||
SelectedVersion = !IsReleaseMode |
||||
? AvailableVersions?.FirstOrDefault(x => x.TagName == SelectedPackage.MainBranch) |
||||
?? AvailableVersions?.FirstOrDefault() |
||||
: AvailableVersions?.FirstOrDefault(); |
||||
} |
||||
|
||||
private async Task UpdateCommits(string branchName) |
||||
{ |
||||
var commits = await SelectedPackage.GetAllCommits(branchName); |
||||
if (commits != null) |
||||
{ |
||||
AvailableCommits = new ObservableCollection<GitCommit>(commits); |
||||
SelectedCommit = AvailableCommits.FirstOrDefault(); |
||||
} |
||||
} |
||||
|
||||
partial void OnInstallNameChanged(string? value) |
||||
{ |
||||
ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any( |
||||
p => p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}" |
||||
); |
||||
} |
||||
|
||||
partial void OnIsReleaseModeChanged(bool value) |
||||
{ |
||||
UpdateVersions(); |
||||
} |
||||
|
||||
partial void OnSelectedVersionChanged(PackageVersion? value) |
||||
{ |
||||
if (IsReleaseMode) |
||||
return; |
||||
|
||||
UpdateCommits(value?.TagName ?? SelectedPackage.MainBranch).SafeFireAndForget(); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue