Browse Source

Merge pull request #59 from PixiEditor/auto-updater

Created Auto updater
Krzysztof Krysiński 4 years ago
parent
commit
5fccdfef85

+ 9 - 0
PixiEditor.UpdateInstaller/App.xaml

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

+ 17 - 0
PixiEditor.UpdateInstaller/App.xaml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.UpdateInstaller
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 10 - 0
PixiEditor.UpdateInstaller/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    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)
+)]

+ 24 - 0
PixiEditor.UpdateInstaller/Extensions.cs

@@ -0,0 +1,24 @@
+using System;
+
+namespace PixiEditor.UpdateInstaller
+{
+    public static class Extensions
+	{
+		[System.Runtime.InteropServices.DllImport("kernel32.dll")]
+		static extern uint GetModuleFileName(IntPtr hModule, System.Text.StringBuilder lpFilename, int nSize);
+		static readonly int MAX_PATH = 255;
+		public static string GetExecutablePath()
+		{
+			if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
+			{
+				var sb = new System.Text.StringBuilder(MAX_PATH);
+				GetModuleFileName(IntPtr.Zero, sb, MAX_PATH);
+				return sb.ToString();
+			}
+			else
+			{
+				return System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
+			}
+		}
+	}
+}

BIN
PixiEditor.UpdateInstaller/Images/PixiEditorLogo.png


+ 25 - 0
PixiEditor.UpdateInstaller/MainWindow.xaml

@@ -0,0 +1,25 @@
+<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"
+        xmlns:local="clr-namespace:PixiEditor.UpdateInstaller"
+        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>

+ 44 - 0
PixiEditor.UpdateInstaller/MainWindow.xaml.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.UpdateInstaller
+{
+    /// <summary>
+    /// Interaction logic for MainWindow.xaml
+    /// </summary>
+    public partial class MainWindow : Window
+    {
+        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();
+        }
+    }
+}

+ 20 - 0
PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="Images\PixiEditorLogo.png" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Resource Include="Images\PixiEditorLogo.png" />
+  </ItemGroup>
+</Project>

+ 17 - 0
PixiEditor.UpdateInstaller/ViewModelBase.cs

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

+ 65 - 0
PixiEditor.UpdateInstaller/ViewModelMain.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.UpdateInstaller
+{
+    public class ViewModelMain : ViewModelBase
+    {
+        public ViewModelMain Current { get; private set; }
+        public UpdateModule.UpdateInstaller Installer { get; set; }
+
+        public string UpdateDirectory { get; private set; }
+
+        private float _progressValue;
+
+        public float ProgressValue
+        {
+            get => _progressValue;
+            set 
+            { 
+                _progressValue = value;
+                RaisePropertyChanged(nameof(ProgressValue));
+            }
+        }
+
+        public ViewModelMain()
+        {
+            Current = this;
+
+            string updateDirectory = Path.GetDirectoryName(Extensions.GetExecutablePath());
+
+#if DEBUG
+            updateDirectory = Environment.GetCommandLineArgs()[1];
+#endif
+            UpdateDirectory = updateDirectory;
+        }
+
+        public void InstallUpdate()
+        {
+            string[] files = Directory.GetFiles(UpdateDirectory, "update-*.zip");
+
+            if (files.Length > 0)
+            {
+                Installer = new UpdateModule.UpdateInstaller(files[0]);
+                Installer.ProgressChanged += Installer_ProgressChanged;
+                Installer.Install();
+            }
+            else
+            {
+                ProgressValue = 100;
+            }
+        }
+
+        private void Installer_ProgressChanged(object sender, UpdateModule.UpdateProgressChangedEventArgs e)
+        {
+            ProgressValue = e.Progress;
+        }
+    }
+}

+ 17 - 0
PixiEditor.UpdateModule/Asset.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json.Serialization;
+
+namespace PixiEditor.UpdateModule
+{
+    public class Asset
+    {
+        [JsonPropertyName("url")]
+        public string Url { get; set; }
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+        [JsonPropertyName("content_type")]
+        public string ContentType { get; set; }
+    }
+}

+ 7 - 0
PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj

@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+</Project>

+ 26 - 0
PixiEditor.UpdateModule/ReleaseInfo.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json.Serialization;
+
+namespace PixiEditor.UpdateModule
+{
+    public class ReleaseInfo
+    {
+        [JsonPropertyName("tag_name")]
+        public string TagName { get; set; }
+        [JsonPropertyName("draft")]
+        public bool IsDraft { get; set; }
+        [JsonPropertyName("prerelease")]
+        public bool IsPrerelease { get; set; }
+        [JsonPropertyName("assets")]
+        public Asset[] Assets { get; set; }
+        public bool WasDataFetchSuccessfull { get; set; } = true;
+
+        public ReleaseInfo() { }
+        public ReleaseInfo(bool dataFetchSuccessfull)
+        {
+            WasDataFetchSuccessfull = dataFetchSuccessfull;
+        }
+    }
+}

+ 70 - 0
PixiEditor.UpdateModule/UpdateChecker.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Globalization;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace PixiEditor.UpdateModule
+{
+    public class UpdateChecker
+    {
+        public const string ReleaseApiUrl = "https://api.github.com/repos/PixiEditor/PixiEditor/releases/latest";
+        public string CurrentVersionTag { get; set; }
+
+        public ReleaseInfo LatestReleaseInfo { get; set; }
+
+        public UpdateChecker(string currentVersionTag)
+        {
+            CurrentVersionTag = currentVersionTag;
+        }
+
+        public async Task<bool> CheckUpdateAvailable()
+        {
+            LatestReleaseInfo = await GetLatestReleaseInfo();
+            return CheckUpdateAvailable(LatestReleaseInfo);
+        }
+
+        public bool CheckUpdateAvailable(ReleaseInfo latestRelease)
+        {          
+            return latestRelease.WasDataFetchSuccessfull && VersionBigger(CurrentVersionTag, latestRelease.TagName);
+        }
+
+        /// <summary>
+        /// Compares version strings and returns true if newVer > originalVer
+        /// </summary>
+        /// <param name="originalVer"></param>
+        /// <param name="newVer"></param>
+        /// <returns></returns>
+        public static bool VersionBigger(string originalVer, string newVer)
+        {
+            if(ParseVersionString(originalVer, out float ver1))
+            {
+                if (ParseVersionString(newVer, out float ver2))
+                {
+                    return ver2 > ver1;
+                }
+            }
+            return false;
+        }
+
+        private static bool ParseVersionString(string versionString, out float version)
+        {
+            return float.TryParse(versionString.Replace(".", "").Insert(1, "."), NumberStyles.Any, CultureInfo.InvariantCulture, out version);
+        }
+
+        public async Task<ReleaseInfo> GetLatestReleaseInfo()
+        {
+            using(HttpClient client = new HttpClient())
+            {
+                client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
+                var response = await client.GetAsync(ReleaseApiUrl);
+                if(response.StatusCode == System.Net.HttpStatusCode.OK)
+                {
+                    string content = await response.Content.ReadAsStringAsync();
+                    return JsonSerializer.Deserialize<ReleaseInfo>(content);
+                }
+            }
+            return new ReleaseInfo(false);
+        }
+    }
+}

+ 38 - 0
PixiEditor.UpdateModule/UpdateDownloader.cs

@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.UpdateModule
+{
+    public static class UpdateDownloader
+    {
+        public static string DownloadLocation = AppDomain.CurrentDomain.BaseDirectory;
+        public static async Task DownloadReleaseZip(ReleaseInfo release)
+        {
+            Asset matchingAsset = GetMatchingAsset(release);
+
+            using (HttpClient client = new HttpClient())
+            {
+                client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
+                client.DefaultRequestHeaders.Add("Accept", "application/octet-stream");
+                var response = await client.GetAsync(matchingAsset.Url);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    byte[] bytes = await response.Content.ReadAsByteArrayAsync();
+                    File.WriteAllBytes(Path.Join(DownloadLocation, $"update-{release.TagName}.zip"), bytes);
+                }
+            }
+        }
+
+        private static Asset GetMatchingAsset(ReleaseInfo release)
+        {
+            string arch = IntPtr.Size == 8 ? "x64" : "x32";
+            return release.Assets.First(x => x.ContentType == "application/x-zip-compressed"
+            && x.Name.Contains(arch));
+        }
+    }
+}

+ 63 - 0
PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+
+namespace PixiEditor.UpdateModule
+{
+    public class UpdateInstaller
+    {
+        public const string TargetDirectoryName = "UpdateFiles";
+
+        public event EventHandler<UpdateProgressChangedEventArgs> ProgressChanged;
+        private float _progress = 0;
+        public float Progress 
+        {
+            get => _progress;
+            set
+            {
+                _progress = value;
+                ProgressChanged?.Invoke(this, new UpdateProgressChangedEventArgs(value));
+            }
+        }
+        public string ArchiveFileName { get; set; }
+
+        public UpdateInstaller(string archiveFileName)
+        {
+            ArchiveFileName = archiveFileName;
+        }
+
+        public void Install()
+        {
+            var processes = Process.GetProcessesByName("PixiEditor");
+            if(processes.Length > 0)
+            {
+                processes[0].WaitForExit();
+            }
+            ZipFile.ExtractToDirectory(ArchiveFileName, TargetDirectoryName, true);
+            Progress = 25; //25% for unzip
+            string dirWithFiles = Directory.GetDirectories(TargetDirectoryName)[0];
+            string[] files = Directory.GetFiles(dirWithFiles);
+            CopyFilesToDestination(files);
+            DeleteArchive();
+            Progress = 100;
+        }
+
+        private void DeleteArchive()
+        {
+            File.Delete(ArchiveFileName);
+        }
+
+        private void CopyFilesToDestination(string[] files)
+        {
+            float fileCopiedVal = 74f / files.Length; //74% is reserved for copying
+            string destinationDir = Path.GetDirectoryName(ArchiveFileName);
+            foreach (string file in files)
+            {
+                string targetFileName = Path.GetFileName(file);
+                File.Copy(file, Path.Join(destinationDir, targetFileName), true);
+                Progress += fileCopiedVal;
+            }
+        }
+    }
+}

+ 14 - 0
PixiEditor.UpdateModule/UpdateProgressChangedEventArgs.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace PixiEditor.UpdateModule
+{
+    public class UpdateProgressChangedEventArgs : EventArgs
+    {
+        public float Progress { get; set; }
+
+        public UpdateProgressChangedEventArgs(float progress)
+        {
+            Progress = progress;
+        }
+    }
+}

+ 0 - 0
PixiEditor/Helpers/NotifyableObject.cs → PixiEditor/NotifyableObject.cs


+ 4 - 3
PixiEditor/PixiEditor.csproj

@@ -64,9 +64,7 @@
     <Resource Include="Images\MoveImage.png" />
     <Resource Include="Images\PenImage.png" />
     <Resource Include="Images\ColorPickerImage.png" />
-    <Resource Include="Images\PixiEditorLogo.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Resource>
+    <Resource Include="Images\PixiEditorLogo.png" />
     <Resource Include="Images\RectangleImage.png" />
     <Resource Include="Images\SelectImage.png" />
     <Resource Include="Images\transparentbg.png" />
@@ -78,6 +76,9 @@
       <PackagePath></PackagePath>
     </None>
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
+  </ItemGroup>
   <ItemGroup>
     <Compile Update="Properties\Settings.Designer.cs">
       <DesignTimeSharedInput>True</DesignTimeSharedInput>

+ 12 - 0
PixiEditor/PixiEditor.sln

@@ -7,6 +7,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor", "PixiEditor.cs
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorTests", "..\PixiEditorTests\PixiEditorTests.csproj", "{D61922EA-3BF3-4AFA-8930-3A8B30A9A195}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateModule", "..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj", "{9A0AB1E7-435C-4567-9EAC-FF102A9F1B01}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateInstaller", "..\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj", "{BE4F478D-E903-4011-B24B-F6AB96D66514}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -21,6 +25,14 @@ Global
 		{D61922EA-3BF3-4AFA-8930-3A8B30A9A195}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{D61922EA-3BF3-4AFA-8930-3A8B30A9A195}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D61922EA-3BF3-4AFA-8930-3A8B30A9A195}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9A0AB1E7-435C-4567-9EAC-FF102A9F1B01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9A0AB1E7-435C-4567-9EAC-FF102A9F1B01}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9A0AB1E7-435C-4567-9EAC-FF102A9F1B01}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9A0AB1E7-435C-4567-9EAC-FF102A9F1B01}.Release|Any CPU.Build.0 = Release|Any CPU
+		{BE4F478D-E903-4011-B24B-F6AB96D66514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{BE4F478D-E903-4011-B24B-F6AB96D66514}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{BE4F478D-E903-4011-B24B-F6AB96D66514}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{BE4F478D-E903-4011-B24B-F6AB96D66514}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 3 - 3
PixiEditor/Properties/AssemblyInfo.cs

@@ -8,7 +8,7 @@ using System.Windows;
 [assembly: AssemblyTitle("PixiEditor")]
 [assembly: AssemblyDescription("A lighweighted Pixel Art editor.")]
 [assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
+[assembly: AssemblyCompany("PixiEditor")]
 [assembly: AssemblyProduct("PixiEditor")]
 [assembly: AssemblyCopyright("Copyright Krzysztof Krysiński © 2018 - 2020")]
 [assembly: AssemblyTrademark("")]
@@ -50,5 +50,5 @@ using System.Windows;
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 
-[assembly: AssemblyVersion("0.1.2.0")]
-[assembly: AssemblyFileVersion("0.1.2.0")]
+[assembly: AssemblyVersion("0.1.3.0")]
+[assembly: AssemblyFileVersion("0.1.3.0")]

+ 71 - 1
PixiEditor/ViewModels/ViewModelMain.cs

@@ -5,6 +5,8 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Media;
@@ -21,6 +23,7 @@ using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
+using PixiEditor.UpdateModule;
 
 namespace PixiEditor.ViewModels
 {
@@ -80,6 +83,7 @@ namespace PixiEditor.ViewModels
         public RelayCommand OpenHyperlinkCommand { get; set; }
         public RelayCommand ZoomCommand { get; set; }
         public RelayCommand ChangeToolSizeCommand { get; set; }
+        public RelayCommand RestartApplicationCommand { get; set; }
 
 
         private double _mouseXonCanvas;
@@ -106,6 +110,19 @@ namespace PixiEditor.ViewModels
             }
         }
 
+        private string _versionText;
+
+        public string VersionText
+        {
+            get => _versionText;
+            set
+            {
+                _versionText = value;
+                RaisePropertyChanged(nameof(VersionText));
+            }
+        }
+
+
         public bool RecenterZoombox
         {
             get => _recenterZoombox;
@@ -178,6 +195,18 @@ namespace PixiEditor.ViewModels
             }
         }
 
+        private bool _updateReadyToInstall = false;
+
+        public bool UpdateReadyToInstall
+        {
+            get => _updateReadyToInstall;
+            set
+            {
+                _updateReadyToInstall = value;
+                RaisePropertyChanged(nameof(UpdateReadyToInstall));
+            }
+        }
+
         public BitmapManager BitmapManager { get; set; }
         public PixelChangesController ChangesController { get; set; }
 
@@ -196,6 +225,8 @@ namespace PixiEditor.ViewModels
         private bool _restoreToolOnKeyUp = false;
         private Tool _lastActionTool;
 
+        public UpdateChecker UpdateChecker { get; set; }
+
         public ViewModelMain()
         {
             BitmapManager = new BitmapManager();
@@ -238,6 +269,7 @@ namespace PixiEditor.ViewModels
             OpenHyperlinkCommand = new RelayCommand(OpenHyperlink);
             ZoomCommand = new RelayCommand(ZoomViewport);
             ChangeToolSizeCommand = new RelayCommand(ChangeToolSize);
+            RestartApplicationCommand = new RelayCommand(RestartApplication);
             ToolSet = new ObservableCollection<Tool>
             {
                 new MoveTool(), new PenTool(), new SelectTool(), new FloodFill(), new LineTool(),
@@ -292,6 +324,39 @@ namespace PixiEditor.ViewModels
             BitmapManager.PrimaryColor = PrimaryColor;
             ActiveSelection = new Selection(Array.Empty<Coordinates>());
             Current = this;
+            InitUpdateChecker();
+        }
+
+        private void RestartApplication(object parameter)
+        {
+            Process.Start(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "PixiEditor.UpdateInstaller.exe"));
+            Application.Current.Shutdown();
+        }
+
+        public async Task<bool> CheckForUpdate()
+        {
+            return await Task.Run(async () =>
+            {
+                bool updateAvailable = await UpdateChecker.CheckUpdateAvailable();
+                bool updateFileDoesNotExists = !File.Exists($"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip");
+                if (updateAvailable && updateFileDoesNotExists)
+                {
+                    VersionText = "Downloading update...";
+                    await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo);
+                    VersionText = "to install update"; //Button shows "Restart" before this text
+                    UpdateReadyToInstall = true;
+                    return true;
+                }
+                return false;
+            });
+        }
+
+        private void InitUpdateChecker()
+        {
+            var assembly = Assembly.GetExecutingAssembly();
+            FileVersionInfo info = FileVersionInfo.GetVersionInfo(assembly.Location);
+            UpdateChecker = new UpdateChecker(info.FileVersion);
+            VersionText = $"Version {info.FileVersion}";
         }
 
         private void ZoomViewport(object parameter)
@@ -344,13 +409,18 @@ namespace PixiEditor.ViewModels
             if (result != ConfirmationType.Canceled) ((CancelEventArgs) property).Cancel = false;
         }
 
-        private void OnStartup(object parameter)
+        private async void OnStartup(object parameter)
         {
             var lastArg = Environment.GetCommandLineArgs().Last();
             if (Importer.IsSupportedFile(lastArg) && File.Exists(lastArg))
+            {
                 Open(lastArg);
+            }
             else
+            {
                 OpenNewFilePopup(null);
+            }
+            await CheckForUpdate();
         }
 
         private void BitmapManager_DocumentChanged(object sender, DocumentChangedEventArgs e)

+ 8 - 1
PixiEditor/Views/MainWindow.xaml

@@ -13,7 +13,7 @@
         xmlns:cmd="http://www.galasoft.ch/mvvmlight" 
         xmlns:avalondock="https://github.com/Dirkster99/AvalonDock"
         xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
-        mc:Ignorable="d" WindowStyle="None"
+        mc:Ignorable="d" WindowStyle="None" Initialized="mainWindow_Initialized"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
         WindowStartupLocation="CenterScreen" WindowState="Maximized" DataContext="{DynamicResource ViewModelMain}">
     <WindowChrome.WindowChrome>
@@ -391,5 +391,12 @@
                 <TextBlock Margin="4,0,10,0" Text="{Binding MouseYOnCanvas, Converter={StaticResource DoubleToIntConverter}}" Foreground="White" FontSize="16"/>
             </StackPanel>
         </DockPanel>
+        <StackPanel Margin="10,0,0,0" VerticalAlignment="Center" Grid.Row="3"
+                       Grid.Column="3" Orientation="Horizontal">
+            <Button Style="{StaticResource BaseDarkButton}" 
+                    Visibility="{Binding UpdateReadyToInstall, Converter={StaticResource BoolToVisibilityConverter}}" FontSize="14" Height="20" Command="{Binding RestartApplicationCommand}">Restart</Button>
+            <TextBlock VerticalAlignment="Center" Padding="10" HorizontalAlignment="Right"
+                       Foreground="White" FontSize="14"  Text="{Binding VersionText}" />
+        </StackPanel>
     </Grid>
 </Window>

+ 18 - 1
PixiEditor/Views/MainWindow.xaml.cs

@@ -1,4 +1,7 @@
 using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
 using System.Windows;
 using System.Windows.Input;
 using PixiEditor.ViewModels;
@@ -10,12 +13,14 @@ namespace PixiEditor
     /// </summary>
     public partial class MainWindow : Window
     {
+        ViewModelMain viewModel;
         public MainWindow()
         {
             InitializeComponent();
             StateChanged += MainWindowStateChangeRaised;
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
-            ((ViewModelMain) DataContext).CloseAction = Close;
+            viewModel = ((ViewModelMain)DataContext);
+            viewModel.CloseAction = Close;
         }
 
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
@@ -60,5 +65,17 @@ namespace PixiEditor
                 MaximizeButton.Visibility = Visibility.Visible;
             }
         }
+
+        private void mainWindow_Initialized(object sender, EventArgs e)
+        {
+            string dir = AppDomain.CurrentDomain.BaseDirectory;
+            bool updateFileExists = Directory.GetFiles(dir, "update-*.zip").Length > 0;
+            string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
+            if (updateFileExists && File.Exists(updaterPath))
+            {                
+                Process.Start(updaterPath);
+                Close();
+            }
+        }
     }
 }

+ 1 - 0
PixiEditorTests/PixiEditorTests.csproj

@@ -28,6 +28,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
     <ProjectReference Include="..\PixiEditor\PixiEditor.csproj" />
   </ItemGroup>
 

+ 37 - 0
PixiEditorTests/UpdateModuleTests/UpdateCheckerTests.cs

@@ -0,0 +1,37 @@
+using PixiEditor.UpdateModule;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace PixiEditorTests.UpdateModuleTests
+{
+    public class UpdateCheckerTests
+    {
+        [Theory]
+        [InlineData("0.1.2", "0.1.2", false)]
+        [InlineData("0.5", "0.1.2", false)]
+        [InlineData("0.1.3", "0.1.2", false)]
+        [InlineData("0.1.2", "0.1.3", true)]
+        [InlineData("0.2.1", "0.1.3", false)]
+        public void TestThatCheckUpdateAvailableChecksCorrectly(string currentVersion, string newVersion, bool expectedValue)
+        {
+            UpdateChecker checker = new UpdateChecker(currentVersion);
+            bool result = checker.CheckUpdateAvailable(new ReleaseInfo(true) { TagName = newVersion });
+            Assert.True(result == expectedValue);
+        }
+
+        [Theory]
+        [InlineData("0.1.2", "0.1.2", false)]
+        [InlineData("0.5", "0.1.2", false)]
+        [InlineData("0.1.3", "0.1.2", false)]
+        [InlineData("0.1.2", "0.1.3", true)]
+        [InlineData("0.2.1", "0.1.3", false)]
+        public void CheckThatVersionBiggerComparesCorrectly(string currentVersion, string newVersion, bool expectedValue)
+        {
+            Assert.True(UpdateChecker.VersionBigger(currentVersion, newVersion) == expectedValue);
+        }
+    }
+}