Переглянути джерело

UpdateInstaller rewritten to Avalonia UI

Krzysztof Krysiński 2 роки тому
батько
коміт
04b3baa8a0
54 змінених файлів з 2416 додано та 269 видалено
  1. 15 0
      src/AvaloniaGif/AvaloniaGif.csproj
  2. 10 0
      src/AvaloniaGif/BgWorkerCommand.cs
  3. 12 0
      src/AvaloniaGif/BgWorkerState.cs
  4. 10 0
      src/AvaloniaGif/Decoding/BlockTypes.cs
  5. 8 0
      src/AvaloniaGif/Decoding/ExtensionType.cs
  6. 10 0
      src/AvaloniaGif/Decoding/FrameDisposal.cs
  7. 36 0
      src/AvaloniaGif/Decoding/GifColor.cs
  8. 658 0
      src/AvaloniaGif/Decoding/GifDecoder.cs
  9. 17 0
      src/AvaloniaGif/Decoding/GifFrame.cs
  10. 19 0
      src/AvaloniaGif/Decoding/GifHeader.cs
  11. 45 0
      src/AvaloniaGif/Decoding/GifRect.cs
  12. 8 0
      src/AvaloniaGif/Decoding/GifRepeatBehavior.cs
  13. 28 0
      src/AvaloniaGif/Decoding/InvalidGifStreamException.cs
  14. 28 0
      src/AvaloniaGif/Decoding/LzwDecompressionException.cs
  15. 82 0
      src/AvaloniaGif/Extensions/StreamExtensions.cs
  16. 228 0
      src/AvaloniaGif/GifImage.cs
  17. 154 0
      src/AvaloniaGif/GifInstance.cs
  18. 25 0
      src/AvaloniaGif/InvalidGifStreamException.cs
  19. 0 0
      src/PixiEditor.UI.Common/Assets/PixiEditorLogo.png
  20. BIN
      src/PixiEditor.UI.Common/Assets/Processing.gif
  21. 14 0
      src/PixiEditor.UI.Common/Controls/ProgressBar.axaml
  22. 42 0
      src/PixiEditor.UI.Common/Controls/TextBlock.axaml
  23. 21 0
      src/PixiEditor.UI.Common/DesignGuidelines.md
  24. 27 0
      src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj
  25. 6 0
      src/PixiEditor.UI.Common/Styles/PixiEditor.Controls.axaml
  26. 15 0
      src/PixiEditor.UI.Common/Styles/PixiEditor.axaml
  27. 9 0
      src/PixiEditor.UI.Common/Themes/PixiEditorTheme.cs
  28. 454 0
      src/PixiEditor.UpdateInstaller/.gitignore
  29. 0 8
      src/PixiEditor.UpdateInstaller/App.xaml
  30. 0 10
      src/PixiEditor.UpdateInstaller/App.xaml.cs
  31. 0 14
      src/PixiEditor.UpdateInstaller/AssemblyInfo.cs
  32. 6 0
      src/PixiEditor.UpdateInstaller/Directory.Build.props
  33. 0 24
      src/PixiEditor.UpdateInstaller/MainWindow.xaml
  34. 0 42
      src/PixiEditor.UpdateInstaller/MainWindow.xaml.cs
  35. 24 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/PixiEditor.UpdateInstaller.Desktop.csproj
  36. 23 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/Program.cs
  37. 18 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/app.manifest
  38. 0 27
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  39. 48 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.sln
  40. 19 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml
  41. 29 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml.cs
  42. BIN
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Assets/avalonia-logo.ico
  43. 2 2
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Extensions.cs
  44. 32 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  45. 8 11
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/MainViewModel.cs
  46. 7 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/ViewModelBase.cs
  47. 20 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml
  48. 17 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml.cs
  49. 13 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml
  50. 52 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml.cs
  51. 0 16
      src/PixiEditor.UpdateInstaller/ViewModelBase.cs
  52. 0 76
      src/PixiEditor.UpdateInstaller/app.manifest
  53. 3 1
      src/PixiEditor.UpdateModule/UpdateInstaller.cs
  54. 114 38
      src/PixiEditor.sln

+ 15 - 0
src/AvaloniaGif/AvaloniaGif.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+	<PropertyGroup>
+		<TargetFramework>netstandard2.1</TargetFramework>
+		<LangVersion>latest</LangVersion> 
+		<AllowUnsafeBlocks>true</AllowUnsafeBlocks> 
+		<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+  </PropertyGroup>
+  <ItemGroup>
+    <InternalsVisibleTo Include="AvaloniaGif.Test" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Avalonia" Version="11.0.0-preview7" />
+  </ItemGroup>
+
+</Project>

+ 10 - 0
src/AvaloniaGif/BgWorkerCommand.cs

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

+ 12 - 0
src/AvaloniaGif/BgWorkerState.cs

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

+ 10 - 0
src/AvaloniaGif/Decoding/BlockTypes.cs

@@ -0,0 +1,10 @@
+namespace AvaloniaGif.Decoding
+{
+    internal enum BlockTypes
+    {
+        EMPTY = 0,
+        EXTENSION = 0x21,
+        IMAGE_DESCRIPTOR = 0x2C,
+        TRAILER = 0x3B,
+    }
+}

+ 8 - 0
src/AvaloniaGif/Decoding/ExtensionType.cs

@@ -0,0 +1,8 @@
+namespace AvaloniaGif.Decoding
+{
+    internal enum ExtensionType
+    {
+        GRAPHICS_CONTROL = 0xF9,
+        APPLICATION = 0xFF
+    }
+}

+ 10 - 0
src/AvaloniaGif/Decoding/FrameDisposal.cs

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

+ 36 - 0
src/AvaloniaGif/Decoding/GifColor.cs

@@ -0,0 +1,36 @@
+using System.Runtime.InteropServices;
+
+namespace AvaloniaGif
+{
+    [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;
+        }
+    }
+}

+ 658 - 0
src/AvaloniaGif/Decoding/GifDecoder.cs

@@ -0,0 +1,658 @@
+// 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 AvaloniaGif.Extensions.StreamExtensions;
+
+namespace AvaloniaGif.Decoding
+{
+    public sealed class GifDecoder : IDisposable
+    {
+        private static readonly ReadOnlyMemory<byte> G87AMagic
+            = Encoding.ASCII.GetBytes("GIF87a").AsMemory();
+
+        private static readonly ReadOnlyMemory<byte> G89AMagic
+            = Encoding.ASCII.GetBytes("GIF89a").AsMemory();
+
+        private static readonly ReadOnlyMemory<byte> NetscapeMagic
+            = Encoding.ASCII.GetBytes("NETSCAPE2.0").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.IMAGE_DESCRIPTOR:
+                        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.GRAPHICS_CONTROL:
+
+                    _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;
+            }
+        }
+    }
+}

+ 17 - 0
src/AvaloniaGif/Decoding/GifFrame.cs

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

+ 19 - 0
src/AvaloniaGif/Decoding/GifHeader.cs

@@ -0,0 +1,19 @@
+// Licensed under the MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+namespace AvaloniaGif.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;
+    }
+}

+ 45 - 0
src/AvaloniaGif/Decoding/GifRect.cs

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

+ 8 - 0
src/AvaloniaGif/Decoding/GifRepeatBehavior.cs

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

+ 28 - 0
src/AvaloniaGif/Decoding/InvalidGifStreamException.cs

@@ -0,0 +1,28 @@
+// Licensed under the MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+using System;
+using System.Runtime.Serialization;
+
+namespace AvaloniaGif.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)
+        {
+        }
+    }
+}

+ 28 - 0
src/AvaloniaGif/Decoding/LzwDecompressionException.cs

@@ -0,0 +1,28 @@
+// Licensed under the MIT License.
+// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
+
+using System;
+using System.Runtime.Serialization;
+
+namespace AvaloniaGif.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)
+        {
+        }
+    }
+}

+ 82 - 0
src/AvaloniaGif/Extensions/StreamExtensions.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace AvaloniaGif.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;
+        }
+    }
+}

+ 228 - 0
src/AvaloniaGif/GifImage.cs

@@ -0,0 +1,228 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+
+namespace AvaloniaGif
+{
+    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 GifInstance gifInstance;
+
+        public static readonly StyledProperty<bool> AutoStartProperty =
+            AvaloniaProperty.Register<GifImage, bool>("AutoStart");
+
+        public static readonly StyledProperty<StretchDirection> StretchDirectionProperty =
+            AvaloniaProperty.Register<GifImage, StretchDirection>("StretchDirection");
+
+        public static readonly StyledProperty<Stretch> StretchProperty =
+            AvaloniaProperty.Register<GifImage, Stretch>("Stretch");
+
+        private RenderTargetBitmap backingRTB;
+        private bool _hasNewSource;
+        private object? _newSource;
+        private Stopwatch _stopwatch;
+
+        static GifImage()
+        {
+            SourceUriRawProperty.Changed.Subscribe(SourceChanged);
+            SourceUriProperty.Changed.Subscribe(SourceChanged);
+            SourceStreamProperty.Changed.Subscribe(SourceChanged);
+            IterationCountProperty.Changed.Subscribe(IterationCountChanged);
+            AutoStartProperty.Changed.Subscribe(AutoStartChanged);
+            AffectsRender<GifImage>(SourceStreamProperty, SourceUriProperty, SourceUriRawProperty, StretchProperty);
+            AffectsArrange<GifImage>(SourceStreamProperty, SourceUriProperty, SourceUriRawProperty, StretchProperty);
+            AffectsMeasure<GifImage>(SourceStreamProperty, SourceUriProperty, SourceUriRawProperty, StretchProperty);
+        }
+
+        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 bool AutoStart
+        {
+            get => GetValue(AutoStartProperty);
+            set => SetValue(AutoStartProperty, value);
+        }
+
+        public StretchDirection StretchDirection
+        {
+            get => GetValue(StretchDirectionProperty);
+            set => SetValue(StretchDirectionProperty, value);
+        }
+
+        public Stretch Stretch
+        {
+            get => GetValue(StretchProperty);
+            set => SetValue(StretchProperty, value);
+        }
+
+        private static void AutoStartChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var image = e.Sender as GifImage;
+            if (image == null)
+                return;
+        }
+
+        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;
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            Dispatcher.UIThread.Post(InvalidateMeasure, DispatcherPriority.Background);
+            Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
+            
+            if (_hasNewSource)
+            {
+                StopAndDispose();
+                gifInstance = new GifInstance(_newSource);
+                gifInstance.IterationCount = IterationCount;
+                backingRTB = new RenderTargetBitmap(gifInstance.GifPixelSize, new Vector(96, 96));
+                _hasNewSource = false;
+
+                _stopwatch ??= new Stopwatch();
+                _stopwatch.Reset();
+
+
+                return;
+            }
+
+            if (gifInstance is null || (gifInstance.CurrentCts?.IsCancellationRequested ?? true))
+            {
+                return;
+            }
+
+            if (!_stopwatch.IsRunning)
+            {
+                _stopwatch.Start();
+            }
+
+            var currentFrame = gifInstance.ProcessFrameTime(_stopwatch.Elapsed);
+
+            if (currentFrame is { } source && backingRTB is { })
+            {
+                using var ctx = backingRTB.CreateDrawingContext();
+                var ts = new Rect(source.Size);
+                ctx.DrawBitmap(source.PlatformImpl, 1, ts, ts);
+            }
+
+            if (backingRTB is not null && Bounds.Width > 0 && Bounds.Height > 0)
+            {
+                var viewPort = new Rect(Bounds.Size);
+                var sourceSize = backingRTB.Size;
+
+                var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
+                var scaledSize = sourceSize * scale;
+                var destRect = viewPort
+                    .CenterRect(new Rect(scaledSize))
+                    .Intersect(viewPort);
+
+                var sourceRect = new Rect(sourceSize)
+                    .CenterRect(new Rect(destRect.Size / scale));
+
+                var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this);
+
+                context.DrawImage(backingRTB, sourceRect, destRect, interpolationMode);
+            }
+        }
+
+        /// <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 source = backingRTB;
+            var result = new Size();
+
+            if (source != null)
+            {
+                result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection);
+            }
+
+            return result;
+        }
+
+        /// <inheritdoc/>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            var source = backingRTB;
+
+            if (source != null)
+            {
+                var sourceSize = source.Size;
+                var result = Stretch.CalculateSize(finalSize, sourceSize);
+                return result;
+            }
+
+            return new Size();
+        }
+
+        public void StopAndDispose()
+        {
+            gifInstance?.Dispose();
+            backingRTB?.Dispose();
+        }
+
+        private static void SourceChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var image = e.Sender as GifImage;
+
+            if (image == null)
+                return;
+
+            if (e.NewValue is null)
+            {
+                return;
+            }
+
+            image._hasNewSource = true;
+            image._newSource = e.NewValue;
+            Dispatcher.UIThread.Post(image.InvalidateVisual, DispatcherPriority.Background);
+        }
+    }
+}

+ 154 - 0
src/AvaloniaGif/GifInstance.cs

@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using AvaloniaGif.Decoding;
+using JetBrains.Annotations;
+
+namespace AvaloniaGif
+{
+    public class GifInstance : IDisposable
+    {
+        public IterationCount IterationCount { get; set; }
+        public bool AutoStart { get; private set; } = true;
+        private readonly GifDecoder _gifDecoder;
+        private readonly WriteableBitmap _targetBitmap;
+        private TimeSpan _totalTime;
+        private readonly List<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);
+
+            // Save the color table cache ID's to refresh them on cache while
+            // // the image is either stopped/paused.
+            // _colorTableIdList = _gifDecoder.Frames
+            //     .Where(p => p.IsLocalColorTableUsed)
+            //     .Select(p => p.LocalColorTableCacheID)
+            //     .ToList();
+
+            // if (_gifDecoder.Header.HasGlobalColorTable)
+            //     _colorTableIdList.Add(_gifDecoder.Header.GlobalColorTableCacheID);
+        }
+
+        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"))
+                throw new InvalidDataException(
+                    "The URI provided is not currently supported.");
+
+            var assetLocator = AvaloniaLocator.Current.GetService<IAssetLoader>();
+
+            if (assetLocator is null)
+                throw new InvalidDataException(
+                    "The resource URI was not found in the current assembly.");
+
+            return assetLocator.Open(uri);
+        }
+
+        public int GifFrameCount => _frameTimes.Count;
+
+        public PixelSize GifPixelSize { get; }
+
+        public void Dispose()
+        {
+            CurrentCts.Cancel();
+            _targetBitmap?.Dispose();
+        }
+
+        public WriteableBitmap ProcessFrameTime(TimeSpan stopwatchElapsed)
+        {
+            if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
+            {
+                return null;
+            }
+
+            if (CurrentCts.IsCancellationRequested)
+            {
+                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;
+        }
+    }
+}

+ 25 - 0
src/AvaloniaGif/InvalidGifStreamException.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace AvaloniaGif
+{
+    [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
src/PixiEditor.UpdateInstaller/Images/PixiEditorLogo.png → src/PixiEditor.UI.Common/Assets/PixiEditorLogo.png


BIN
src/PixiEditor.UI.Common/Assets/Processing.gif


+ 14 - 0
src/PixiEditor.UI.Common/Controls/ProgressBar.axaml

@@ -0,0 +1,14 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Design.PreviewWith>
+        <Border Padding="20">
+            <ProgressBar/>
+        </Border>
+    </Design.PreviewWith>
+
+    <Style Selector="ProgressBar">
+        <Setter Property="Foreground" Value="{DynamicResource AccentColor}"/>
+        <Setter Property="Background" Value="{DynamicResource Background1}"/>
+        <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}"/>
+    </Style>
+</Styles>

+ 42 - 0
src/PixiEditor.UI.Common/Controls/TextBlock.axaml

@@ -0,0 +1,42 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Design.PreviewWith>
+        <Border Padding="20">
+            <StackPanel Orientation="Vertical">
+                <TextBlock Classes="h1" Text="h1"/>
+                <TextBlock Classes="h2" Text="h2"/>
+                <TextBlock Classes="h3" Text="h3"/>
+                <TextBlock Classes="h4" Text="h4"/>
+            </StackPanel>
+        </Border>
+    </Design.PreviewWith>
+
+    <Style Selector="TextBlock">
+        <Setter Property="FontSize" Value="16"/>
+    </Style>
+
+    <Style Selector="TextBlock.h1">
+        <Setter Property="FontSize" Value="40"/>
+        <Setter Property="FontWeight" Value="Bold"/>
+    </Style>
+
+    <Style Selector="TextBlock.h2">
+        <Setter Property="FontSize" Value="32"/>
+        <Setter Property="FontWeight" Value="Bold"/>
+    </Style>
+
+    <Style Selector="TextBlock.h3">
+        <Setter Property="FontSize" Value="24"/>
+        <Setter Property="FontWeight" Value="Bold"/>
+    </Style>
+
+    <Style Selector="TextBlock.h4">
+        <Setter Property="FontSize" Value="20"/>
+        <Setter Property="FontWeight" Value="Bold"/>
+    </Style>
+
+    <Style Selector="TextBlock.h5">
+        <Setter Property="FontSize" Value="16"/>
+        <Setter Property="FontWeight" Value="Bold"/>
+    </Style>
+</Styles>

+ 21 - 0
src/PixiEditor.UI.Common/DesignGuidelines.md

@@ -0,0 +1,21 @@
+This project is dedicated for creating common PixiEditor styles and controls
+using AvaloniaUI framework. It is used in PixiEditor to create consistent look along
+all GUI applications.
+
+## Structure
+
+### Assets folder
+
+This folder contains all common media files, like images, icons, fonts, etc.
+
+### Styles folder
+
+This folder contains all common styles/themes.
+
+```PixiEditor.axaml``` is a default file that contains all common colors, geometry, etc.
+
+### Controls folder
+
+This folder contains all common controls, like buttons, sliders, etc.
+Each control file has a definition of a control and a style for it. Each style
+uses theme data taken from ```Styles/PixiEditor.axaml'``` file.

+ 27 - 0
src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj

@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <AvaloniaResource Include="Assets\PixiEditorLogo.png" />
+      <AvaloniaResource Include="Assets\Processing.gif" />
+      <None Remove="Assets\Fonts\Oxygen-Bold.ttf" />
+      <None Remove="Assets\Fonts\Oxygen-Light.ttf" />
+      <None Remove="Assets\Fonts\Oxygen-Regular.ttf" />
+      <None Remove="Assets\Fonts\Jost-VariableFont_wght.ttf" />
+      <None Remove="Assets\Fonts\Jost-Regular.ttf" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Avalonia" Version="11.0.0-preview7" />
+      <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview7" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <UpToDateCheckInput Remove="Animations\Processing.axaml" />
+    </ItemGroup>
+</Project>

+ 6 - 0
src/PixiEditor.UI.Common/Styles/PixiEditor.Controls.axaml

@@ -0,0 +1,6 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+    <StyleInclude Source="../Controls/ProgressBar.axaml"/>
+    <StyleInclude Source="../Controls/TextBlock.axaml"/>
+</Styles>

+ 15 - 0
src/PixiEditor.UI.Common/Styles/PixiEditor.axaml

@@ -0,0 +1,15 @@
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime">
+
+    <ResourceDictionary.ThemeDictionaries>
+        <ResourceDictionary x:Key="Dark">
+            <SolidColorBrush x:Key="Background0" Color="#252525" /> <!--This is the lowest elevation color-->
+            <SolidColorBrush x:Key="Background1" Color="#2D2D30" />
+            <SolidColorBrush x:Key="AccentColor" Color="#B00022"/>
+            <FontFamily x:Key="OxygenFont">avares://PixiEditor.UI.Common/Assets/Fonts/Oxygen-Regular.ttf#Oxygen</FontFamily>
+        </ResourceDictionary>
+    </ResourceDictionary.ThemeDictionaries>
+
+    <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
+</ResourceDictionary>

+ 9 - 0
src/PixiEditor.UI.Common/Themes/PixiEditorTheme.cs

@@ -0,0 +1,9 @@
+using Avalonia.Styling;
+using Avalonia.Themes.Fluent;
+
+namespace PixiEditor.UI.Common.Themes;
+
+public class PixiEditorTheme : FluentTheme
+{
+
+}

+ 454 - 0
src/PixiEditor.UpdateInstaller/.gitignore

@@ -0,0 +1,454 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json

+ 0 - 8
src/PixiEditor.UpdateInstaller/App.xaml

@@ -1,8 +0,0 @@
-<Application x:Class="PixiEditor.UpdateInstaller.App"
-             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             StartupUri="MainWindow.xaml">
-    <Application.Resources>
-
-    </Application.Resources>
-</Application>

+ 0 - 10
src/PixiEditor.UpdateInstaller/App.xaml.cs

@@ -1,10 +0,0 @@
-using System.Windows;
-
-namespace PixiEditor.UpdateInstaller;
-
-/// <summary>
-///     Interaction logic for App.xaml.
-/// </summary>
-public partial class App : Application
-{
-}

+ 0 - 14
src/PixiEditor.UpdateInstaller/AssemblyInfo.cs

@@ -1,14 +0,0 @@
-using System.Windows;
-
-[assembly: ThemeInfo(
-    ResourceDictionaryLocation.None,
-    ResourceDictionaryLocation.SourceAssembly)
-
-// ResourceDictionaryLocation.None - where theme specific resource dictionaries are located
-// (used if a resource is not found in the page,
-// or application resource dictionaries)
-
-// ResourceDictionaryLocation.SourceAssembly - where the generic resource dictionary is located
-// (used if a resource is not found in the page,
-// app, or any theme specific resource dictionaries)
-]

+ 6 - 0
src/PixiEditor.UpdateInstaller/Directory.Build.props

@@ -0,0 +1,6 @@
+<Project>
+  <PropertyGroup>
+    <Nullable>enable</Nullable>
+    <AvaloniaVersion>11.0.0-preview7</AvaloniaVersion>
+  </PropertyGroup>
+</Project>

+ 0 - 24
src/PixiEditor.UpdateInstaller/MainWindow.xaml

@@ -1,24 +0,0 @@
-<Window x:Class="PixiEditor.UpdateInstaller.MainWindow"
-        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-        mc:Ignorable="d"
-        Title="MainWindow" Height="350" Width="250" Background="#2D2D30" ResizeMode="NoResize"
-        WindowStyle="None" WindowStartupLocation="CenterScreen" Loaded="Window_Loaded">
-    <WindowChrome.WindowChrome>
-        <WindowChrome ResizeBorderThickness="6"
-                      CaptionHeight="30" />
-    </WindowChrome.WindowChrome>
-    <Grid>
-        <Grid.RowDefinitions>
-            <RowDefinition Height="130" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
-        <Image Source="Images/PixiEditorLogo.png" Width="75" Height="75" Grid.Row="0" />
-        <StackPanel Grid.Row="1" Margin="0,20,0,0">
-            <Label Content="Installing update" HorizontalAlignment="Center" Foreground="White" FontSize="20" />
-            <ProgressBar Margin="0,20,0,0" Height="20" Width="200" Value="{Binding ProgressValue}" />
-        </StackPanel>
-    </Grid>
-</Window>

+ 0 - 42
src/PixiEditor.UpdateInstaller/MainWindow.xaml.cs

@@ -1,42 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Threading.Tasks;
-using System.Windows;
-
-namespace PixiEditor.UpdateInstaller;
-
-/// <summary>
-///     Interaction logic for MainWindow.xaml.
-/// </summary>
-public partial class MainWindow
-{
-    public MainWindow()
-    {
-        InitializeComponent();
-        DataContext = new ViewModelMain();
-    }
-
-    private async void Window_Loaded(object sender, RoutedEventArgs e)
-    {
-        ViewModelMain vmm = (ViewModelMain)DataContext;
-        await Task.Run(() =>
-        {
-            try
-            {
-                vmm.InstallUpdate();
-            }
-            catch (Exception ex)
-            {
-                MessageBox.Show(ex.Message, "Update error", MessageBoxButton.OK, MessageBoxImage.Error);
-                File.AppendAllText("ErrorLog.txt", $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
-            }
-            finally
-            {
-                string pixiEditorExecutablePath = Directory.GetFiles(vmm.UpdateDirectory, "PixiEditor.exe")[0];
-                Process.Start(pixiEditorExecutablePath);
-            }
-        });
-        Close();
-    }
-}

+ 24 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/PixiEditor.UpdateInstaller.Desktop.csproj

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <OutputType>WinExe</OutputType>
+        <!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
+        One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.-->
+        <TargetFramework>net7.0</TargetFramework>
+        <Nullable>enable</Nullable>
+        <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
+    </PropertyGroup>
+
+    <PropertyGroup>
+        <ApplicationManifest>app.manifest</ApplicationManifest>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
+        <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
+        <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj" />
+    </ItemGroup>
+</Project>

+ 23 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/Program.cs

@@ -0,0 +1,23 @@
+using System;
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace PixiEditor.UpdateInstaller.New.Desktop;
+
+class Program
+{
+    // Initialization code. Don't use any Avalonia, third-party APIs or any
+    // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+    // yet and stuff might break.
+    [STAThread]
+    public static void Main(string[] args) => BuildAvaloniaApp()
+        .StartWithClassicDesktopLifetime(args);
+
+    // Avalonia configuration, don't remove; also used by visual designer.
+    public static AppBuilder BuildAvaloniaApp()
+        => AppBuilder.Configure<App>()
+            .UsePlatformDetect()
+            .WithInterFont()
+            .LogToTrace()
+            .UseReactiveUI();
+}

+ 18 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/app.manifest

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <!-- This manifest is used on Windows only.
+       Don't remove it as it might cause problems with window transparency and embeded controls.
+       For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
+  <assemblyIdentity version="1.0.0.0" name="PixiEditor.UpdateInstaller.New.Desktop"/>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- A list of the Windows versions that this application has been tested on
+           and is designed to work with. Uncomment the appropriate elements
+           and Windows will automatically select the most compatible environment. -->
+
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+    </application>
+  </compatibility>
+</assembly>

+ 0 - 27
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -1,27 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
-
-  <PropertyGroup>
-    <OutputType>WinExe</OutputType>
-    <TargetFramework>net7.0-windows</TargetFramework>
-    <UseWPF>true</UseWPF>
-    <ApplicationManifest>app.manifest</ApplicationManifest>
-    <Platforms>AnyCPU;x64;x86</Platforms>
-    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <None Remove="Images\PixiEditorLogo.png" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <PackageReference Include="Tools.InnoSetup" Version="6.2.1" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Resource Include="Images\PixiEditorLogo.png" />
-  </ItemGroup>
-</Project>

+ 48 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.sln

@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.3.32811.315
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateInstaller.New", "PixiEditor.UpdateInstaller.New\PixiEditor.UpdateInstaller.New.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateInstaller.New.Desktop", "PixiEditor.UpdateInstaller.New.Desktop\PixiEditor.UpdateInstaller.New.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}"
+	ProjectSection(SolutionItems) = preProject
+		Directory.Build.props = Directory.Build.props
+	EndProjectSection
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU
+		{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E}
+	EndGlobalSection
+EndGlobal

+ 19 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml

@@ -0,0 +1,19 @@
+<Application xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:themes="clr-namespace:PixiEditor.UI.Common.Themes;assembly=PixiEditor.UI.Common"
+             x:Class="PixiEditor.UpdateInstaller.New.App"
+             RequestedThemeVariant="Dark">
+             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
+
+    <Application.Styles>
+        <themes:PixiEditorTheme />
+        <StyleInclude Source="avares://PixiEditor.UI.Common/Styles/PixiEditor.Controls.axaml"/>
+    </Application.Styles>
+    <Application.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceInclude Source="avares://PixiEditor.UI.Common/Styles/PixiEditor.axaml"/>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </Application.Resources>
+</Application>

+ 29 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml.cs

@@ -0,0 +1,29 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using PixiEditor.UpdateInstaller.New.ViewModels;
+using PixiEditor.UpdateInstaller.New.Views;
+
+namespace PixiEditor.UpdateInstaller.New;
+
+public partial class App : Application
+{
+    public override void Initialize()
+    {
+        AvaloniaXamlLoader.Load(this);
+    }
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+        {
+            desktop.MainWindow = new MainWindow { DataContext = new MainViewModel() };
+        }
+        else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
+        {
+            singleViewPlatform.MainView = new MainView { DataContext = new MainViewModel() };
+        }
+
+        base.OnFrameworkInitializationCompleted();
+    }
+}

BIN
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Assets/avalonia-logo.ico


+ 2 - 2
src/PixiEditor.UpdateInstaller/Extensions.cs → src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Extensions.cs

@@ -3,7 +3,7 @@ using System.Diagnostics;
 using System.Runtime.InteropServices;
 using System.Text;
 
-namespace PixiEditor.UpdateInstaller;
+namespace PixiEditor.UpdateInstaller.New;
 
 public static class Extensions
 {
@@ -23,4 +23,4 @@ public static class Extensions
 
     [DllImport("kernel32.dll")]
     private static extern uint GetModuleFileName(IntPtr hModule, StringBuilder lpFilename, int nSize);
-}
+}

+ 32 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <Nullable>enable</Nullable>
+        <LangVersion>latest</LangVersion>
+        <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
+        <RootNamespace>PixiEditor.UpdateInstaller.New</RootNamespace>
+    </PropertyGroup>
+
+
+    <ItemGroup>
+        <AvaloniaResource Include="Assets\**" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
+        <PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
+        <PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
+        <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
+        <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
+        <PackageReference Include="MessageBox.Avalonia" Version="2.3.1-prev7.0" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\..\PixiEditor.UI.Common\PixiEditor.UI.Common.csproj" />
+      <ProjectReference Include="..\..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <Folder Include="Styles\" />
+    </ItemGroup>
+</Project>

+ 8 - 11
src/PixiEditor.UpdateInstaller/ViewModelMain.cs → src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/MainViewModel.cs

@@ -1,14 +1,15 @@
 using System;
 using System.IO;
 using PixiEditor.UpdateModule;
+using ReactiveUI;
 
-namespace PixiEditor.UpdateInstaller;
+namespace PixiEditor.UpdateInstaller.New.ViewModels;
 
-public class ViewModelMain : ViewModelBase
+public class MainViewModel : ViewModelBase
 {
     private float progressValue;
 
-    public ViewModelMain()
+    public MainViewModel()
     {
         Current = this;
 
@@ -20,7 +21,7 @@ public class ViewModelMain : ViewModelBase
         UpdateDirectory = updateDirectory;
     }
 
-    public ViewModelMain Current { get; private set; }
+    public MainViewModel Current { get; private set; }
 
     public UpdateModule.UpdateInstaller Installer { get; set; }
 
@@ -29,11 +30,7 @@ public class ViewModelMain : ViewModelBase
     public float ProgressValue
     {
         get => progressValue;
-        set
-        {
-            progressValue = value;
-            RaisePropertyChanged(nameof(ProgressValue));
-        }
+        set => this.RaiseAndSetIfChanged(ref this.progressValue, value);
     }
 
     public void InstallUpdate()
@@ -52,8 +49,8 @@ public class ViewModelMain : ViewModelBase
         }
     }
 
-    private void Installer_ProgressChanged(object sender, UpdateProgressChangedEventArgs e)
+    private void Installer_ProgressChanged(object? sender, UpdateProgressChangedEventArgs e)
     {
         ProgressValue = e.Progress;
     }
-}
+}

+ 7 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/ViewModelBase.cs

@@ -0,0 +1,7 @@
+using ReactiveUI;
+
+namespace PixiEditor.UpdateInstaller.New.ViewModels;
+
+public class ViewModelBase : ReactiveObject
+{
+}

+ 20 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml

@@ -0,0 +1,20 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:vm="clr-namespace:PixiEditor.UpdateInstaller.New.ViewModels"
+             mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="350"
+             x:Class="PixiEditor.UpdateInstaller.New.Views.MainView"
+             x:DataType="vm:MainViewModel">
+  <Design.DataContext>
+    <!-- This only sets the DataContext for the previewer in an IDE,
+         to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
+    <vm:MainViewModel />
+  </Design.DataContext>
+
+    <StackPanel Margin="40" Spacing="50">
+        <Image Width="64" Source="avares://PixiEditor.UI.Common/Assets/PixiEditorLogo.png"/>
+        <TextBlock Classes="h4" Text="Installing Update"/>
+        <ProgressBar Minimum="0" Maximum="100" Value="{Binding ProgressValue}" ShowProgressText="True"/>
+        </StackPanel>
+</UserControl>

+ 17 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainView.axaml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using PixiEditor.UpdateInstaller.New.ViewModels;
+
+namespace PixiEditor.UpdateInstaller.New.Views;
+
+public partial class MainView : UserControl
+{
+    public MainView()
+    {
+        InitializeComponent();
+    }
+}

+ 13 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml

@@ -0,0 +1,13 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:vm="using:PixiEditor.UpdateInstaller.New.ViewModels"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:views="clr-namespace:PixiEditor.UpdateInstaller.New.Views"
+        mc:Ignorable="d" d:DesignWidth="350" d:DesignHeight="250"
+        ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaTitleBarHeightHint="30" ExtendClientAreaToDecorationsHint="True"
+        Width="250" Height="350" CanResize="False" WindowStartupLocation="CenterScreen"
+        x:Class="PixiEditor.UpdateInstaller.New.Views.MainWindow" Loaded="Window_OnLoaded"
+        Icon="/Assets/avalonia-logo.ico" Background="{DynamicResource Background0}">
+        <views:MainView />
+</Window>

+ 52 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/Views/MainWindow.axaml.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using PixiEditor.UpdateInstaller.New.ViewModels;
+
+namespace PixiEditor.UpdateInstaller.New.Views;
+
+public partial class MainWindow : Window
+{
+    public MainWindow()
+    {
+        InitializeComponent();
+    }
+
+    private async void Window_OnLoaded(object? sender, RoutedEventArgs e)
+    {
+        MainViewModel vmm = (MainViewModel)DataContext;
+        await Task.Run(() =>
+        {
+            try
+            {
+                vmm.InstallUpdate();
+            }
+            catch (Exception ex)
+            {
+                File.AppendAllText("ErrorLog.txt", $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
+
+                Dispatcher.UIThread.Invoke(() =>
+                {
+                    var messageBoxStandardWindow = MessageBox.Avalonia.MessageBoxManager
+                        .GetMessageBoxStandardWindow("Update error", ex.Message);
+                    messageBoxStandardWindow.Show();
+                });
+            }
+            finally
+            {
+                var files = Directory.GetFiles(vmm.UpdateDirectory, "PixiEditor.exe");
+                if (files.Length > 0)
+                {
+                    string pixiEditorExecutablePath = files[0];
+                    Process.Start(pixiEditorExecutablePath);
+                }
+            }
+        });
+
+        this.Close();
+    }
+}

+ 0 - 16
src/PixiEditor.UpdateInstaller/ViewModelBase.cs

@@ -1,16 +0,0 @@
-using System.ComponentModel;
-
-namespace PixiEditor.UpdateInstaller;
-
-public class ViewModelBase : INotifyPropertyChanged
-{
-    public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
-
-    protected void RaisePropertyChanged(string property)
-    {
-        if (property != null)
-        {
-            PropertyChanged(this, new PropertyChangedEventArgs(property));
-        }
-    }
-}

+ 0 - 76
src/PixiEditor.UpdateInstaller/app.manifest

@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
-  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
-  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
-    <security>
-      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
-        <!-- UAC Manifest Options
-             If you want to change the Windows User Account Control level replace the 
-             requestedExecutionLevel node with one of the following.
-
-        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
-        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
-        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />
-
-            Specifying requestedExecutionLevel element will disable file and registry virtualization. 
-            Remove this element if your application requires this virtualization for backwards
-            compatibility.
-        -->
-        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
-      </requestedPrivileges>
-    </security>
-  </trustInfo>
-
-  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
-    <application>
-      <!-- A list of the Windows versions that this application has been tested on
-           and is designed to work with. Uncomment the appropriate elements
-           and Windows will automatically select the most compatible environment. -->
-
-      <!-- Windows Vista -->
-      <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
-
-      <!-- Windows 7 -->
-      <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
-
-      <!-- Windows 8 -->
-      <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
-
-      <!-- Windows 8.1 -->
-      <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
-
-      <!-- Windows 10 -->
-      <!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
-
-    </application>
-  </compatibility>
-
-  <!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
-       DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need 
-       to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should 
-       also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
-  <!--
-  <application xmlns="urn:schemas-microsoft-com:asm.v3">
-    <windowsSettings>
-      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
-    </windowsSettings>
-  </application>
-  -->
-
-  <!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
-  <!--
-  <dependency>
-    <dependentAssembly>
-      <assemblyIdentity
-          type="win32"
-          name="Microsoft.Windows.Common-Controls"
-          version="6.0.0.0"
-          processorArchitecture="*"
-          publicKeyToken="6595b64144ccf1df"
-          language="*"
-        />
-    </dependentAssembly>
-  </dependency>
-  -->
-
-</assembly>

+ 3 - 1
src/PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -2,6 +2,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.IO.Compression;
+using System.Threading;
 
 namespace PixiEditor.UpdateModule;
 
@@ -67,6 +68,7 @@ public class UpdateInstaller
             string targetFileName = Path.GetFileName(file);
             File.Copy(file, Path.Join(destinationDir, targetFileName), true);
             Progress += fileCopiedVal;
+            Thread.Sleep(1000);
         }
     }
-}
+}

+ 114 - 38
src/PixiEditor.sln

@@ -5,8 +5,6 @@ VisualStudioVersion = 17.1.31911.260
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor", "PixiEditor\PixiEditor.csproj", "{2CCDDE79-06CB-4771-AF85-7B25313EBA30}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateInstaller", "PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj", "{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateModule", "PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj", "{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorTests", "PixiEditorTests\PixiEditorTests.csproj", "{5193C1C1-8362-40FD-802B-E097E8C88082}"
@@ -40,6 +38,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorGen", "PixiEditor
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Builder", "PixiEditor.Builder\build\PixiEditor.Builder.csproj", "{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UpdateInstaller", "PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj", "{EA01879E-15E9-46C5-99E8-C38771E07A0C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UpdateInstaller.Desktop", "PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.Desktop\PixiEditor.UpdateInstaller.Desktop.csproj", "{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UI.Common", "PixiEditor.UI.Common\PixiEditor.UI.Common.csproj", "{712C73D1-D882-41AE-9F75-B43434F2B434}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -98,42 +102,6 @@ Global
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x64.Build.0 = DevRelease|x64
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x86.ActiveCfg = DevRelease|x86
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x86.Build.0 = DevRelease|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.ActiveCfg = Debug|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.Build.0 = Debug|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.ActiveCfg = Debug|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.Build.0 = Debug|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|x64.ActiveCfg = Debug|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|x64.Build.0 = Debug|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|x86.ActiveCfg = Debug|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|x86.Build.0 = Debug|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX|Any CPU.ActiveCfg = Release|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX|Any CPU.Build.0 = Release|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX|x64.ActiveCfg = Release|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX|x64.Build.0 = Release|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX|x86.ActiveCfg = Release|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX|x86.Build.0 = Release|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|Any CPU.Build.0 = Release|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.ActiveCfg = Release|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.Build.0 = Release|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x86.ActiveCfg = Release|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x86.Build.0 = Release|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|Any CPU.ActiveCfg = Steam|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|Any CPU.Build.0 = Steam|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x64.ActiveCfg = Steam|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x64.Build.0 = Steam|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x86.ActiveCfg = Steam|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x86.Build.0 = Steam|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.ActiveCfg = DevRelease|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.Build.0 = DevRelease|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x86.ActiveCfg = DevRelease|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x86.Build.0 = DevRelease|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x64.ActiveCfg = Debug|x64
@@ -610,6 +578,114 @@ Global
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x64.Build.0 = DevRelease|x64
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x86.ActiveCfg = DevRelease|x86
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x86.Build.0 = DevRelease|x86
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Debug|x64.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Debug|x86.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX|x64.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.MSIX|x86.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Release|x64.ActiveCfg = Release|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Release|x64.Build.0 = Release|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Release|x86.ActiveCfg = Release|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Release|x86.Build.0 = Release|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Steam|x64.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.Steam|x86.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{EA01879E-15E9-46C5-99E8-C38771E07A0C}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Debug|x64.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Debug|x86.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX|x64.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.MSIX|x86.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Release|x64.ActiveCfg = Release|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Release|x64.Build.0 = Release|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Release|x86.ActiveCfg = Release|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Release|x86.Build.0 = Release|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Steam|x64.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.Steam|x86.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{0B9B88D2-B1F4-44C3-9527-349BC60F21D9}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Debug|x64.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Debug|x86.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX|x64.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.MSIX|x86.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Release|Any CPU.Build.0 = Release|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Release|x64.ActiveCfg = Release|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Release|x64.Build.0 = Release|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Release|x86.ActiveCfg = Release|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Release|x86.Build.0 = Release|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Steam|x64.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.Steam|x86.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{712C73D1-D882-41AE-9F75-B43434F2B434}.DevRelease|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE