Explorar el Código

Added extensions updating

Krzysztof Krysiński hace 4 meses
padre
commit
cf6fd060da

+ 22 - 3
src/PixiEditor.Extensions.Runtime/ExtensionLoader.cs

@@ -29,12 +29,31 @@ public class ExtensionLoader
 
     public void LoadExtensions()
     {
-        foreach (var file in Directory.GetFiles(PackagesPath))
+        foreach (var updateFile in Directory.GetFiles(PackagesPath, "*.update"))
         {
-            if (file.EndsWith(".pixiext"))
+            try
+            {
+                string newExtension = Path.ChangeExtension(updateFile, ".pixiext");
+                if (File.Exists(newExtension))
+                {
+                    File.Delete(newExtension);
+                }
+
+                File.Move(updateFile, newExtension);
+            }
+            catch (IOException)
             {
-                LoadExtension(file);
+                // File is in use, ignore
             }
+            catch (UnauthorizedAccessException)
+            {
+                // File is in use, ignore
+            }
+        }
+
+        foreach (var file in Directory.GetFiles(PackagesPath, "*.pixiext"))
+        {
+            LoadExtension(file);
         }
     }
 

+ 3 - 3
src/PixiEditor.IdentityProvider.PixiAuth/PixiAuthIdentityProvider.cs

@@ -213,7 +213,7 @@ public class PixiAuthIdentityProvider : IIdentityProvider
             if (products != null)
             {
                 User.OwnedProducts = products.Where(x => x is { IsDlc: true, Target: "PixiEditor" })
-                    .Select(x => new ProductData(x.ProductId, x.ProductName)).ToList();
+                    .Select(x => new ProductData(x.ProductId, x.ProductName) { LatestVersion = x.LatestVersion }).ToList();
                 OwnedProductsUpdated?.Invoke(new List<ProductData>(User.OwnedProducts));
             }
         }
@@ -252,8 +252,8 @@ public class PixiAuthIdentityProvider : IIdentityProvider
                 var products = await PixiAuthClient.GetOwnedProducts(User.SessionToken);
                 if (products != null)
                 {
-                    User.OwnedProducts = products.Where(x => x.IsDlc && x.Target == "PixiEditor")
-                        .Select(x => new ProductData(x.ProductId, x.ProductName)).ToList();
+                    User.OwnedProducts = products.Where(x => x is { IsDlc: true, Target: "PixiEditor" })
+                        .Select(x => new ProductData(x.ProductId, x.ProductName) { LatestVersion = x.LatestVersion }).ToList();
                     OwnedProductsUpdated?.Invoke(new List<ProductData>(User.OwnedProducts));
                 }
 

+ 1 - 0
src/PixiEditor.IdentityProvider/ProductData.cs

@@ -4,6 +4,7 @@ public record ProductData
 {
     public string Id { get; set; }
     public string DisplayName { get; set; }
+    public string? LatestVersion { get; set; }
 
     public ProductData(string id, string displayName)
     {

+ 3 - 0
src/PixiEditor.PixiAuth/Models/Product.cs

@@ -16,4 +16,7 @@ public class Product
 
     [JsonPropertyName("target")]
     public string Target { get; set; }
+
+    [JsonPropertyName("latestVersion")]
+    public string? LatestVersion { get; set; }
 }

+ 20 - 5
src/PixiEditor.Platform.Standalone/StandaloneAdditionalContentProvider.cs

@@ -30,16 +30,31 @@ public sealed class StandaloneAdditionalContentProvider : IAdditionalContentProv
 
         try
         {
-            var stream = await IdentityProvider.PixiAuthClient.DownloadProduct(IdentityProvider.User.SessionToken, productId);
+            var stream =
+                await IdentityProvider.PixiAuthClient.DownloadProduct(IdentityProvider.User.SessionToken, productId);
             if (stream != null)
             {
                 var filePath = Path.Combine(ExtensionsPath, $"{productId}.pixiext");
-                await using (var fileStream = File.Create(filePath))
+                try
                 {
-                    await stream.CopyToAsync(fileStream);
-                }
+                    await using (var fileStream = File.Create(filePath))
+                    {
+                        await stream.CopyToAsync(fileStream);
+                    }
 
-                await stream.DisposeAsync();
+                    await stream.DisposeAsync();
+                }
+                catch (IOException e)
+                {
+                    filePath = Path.Combine(ExtensionsPath, $"{productId}.update");
+                    await using (var fileStream = File.Create(filePath))
+                    {
+                        await stream.CopyToAsync(fileStream);
+                    }
+
+                    await stream.DisposeAsync();
+                    return null;
+                }
 
                 return filePath;
             }

+ 2 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1057,5 +1057,6 @@
   "OWNED_PRODUCTS": "Owned Content",
   "INSTALLING": "Installing",
   "INSTALLED": "Installed",
-  "ACCOUNT_PROVIDER_INFO": "Account handled by"
+  "ACCOUNT_PROVIDER_INFO": "Account handled by",
+  "UPDATE": "Update"
 }

+ 44 - 7
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
@@ -27,6 +28,8 @@ namespace PixiEditor.Initialization;
 
 internal class ClassicDesktopEntry
 {
+    public static ClassicDesktopEntry? Active { get; private set; }
+    private bool restartQueued;
     private IClassicDesktopStyleApplicationLifetime desktop;
 
     public ClassicDesktopEntry(IClassicDesktopStyleApplicationLifetime desktop)
@@ -34,6 +37,8 @@ internal class ClassicDesktopEntry
         this.desktop = desktop;
         IActivatableLifetime? activable =
             (IActivatableLifetime?)App.Current.TryGetFeature(typeof(IActivatableLifetime));
+
+        Active = this;
         if (activable != null)
         {
             activable.Activated += ActivableOnActivated;
@@ -118,6 +123,12 @@ internal class ClassicDesktopEntry
         return extensionLoader;
     }
 
+    public void Restart()
+    {
+        restartQueued = true;
+        desktop.TryShutdown();
+    }
+
     private IPlatform GetActivePlatform()
     {
 #if STEAM || DEV_STEAM
@@ -196,11 +207,11 @@ internal class ClassicDesktopEntry
         var vm = ViewModels_ViewModelMain.Current;
         if (vm is null)
             return;
-
-        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
+        e.Cancel = true;
+        Dispatcher.UIThread.InvokeAsync(async () =>
         {
-            e.Cancel = true;
-            Task.Run(async () =>
+            await vm.CloseWindow();
+            if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
             {
                 await Dispatcher.UIThread.InvokeAsync(async () =>
                 {
@@ -208,13 +219,39 @@ internal class ClassicDesktopEntry
                         new LocalizedString("SESSION_UNSAVED_DATA", "Shutdown"),
                         $"Shutdown");
 
-                    if (confirmation != ConfirmationType.Yes)
+                    if (confirmation == ConfirmationType.Yes)
                     {
+                        if (restartQueued)
+                        {
+                            var process = Process.GetCurrentProcess().MainModule.FileName;
+                            desktop.Exit += (_, _) =>
+                            {
+                                Process.Start(process);
+                            };
+                        }
+
                         desktop.Shutdown();
                     }
+                    else
+                    {
+                        restartQueued = false;
+                    }
                 });
-            });
-        }
+            }
+            else
+            {
+                if (restartQueued)
+                {
+                    var process = Process.GetCurrentProcess().MainModule.FileName;
+                    desktop.Exit += (_, _) =>
+                    {
+                        Process.Start(process);
+                    };
+                }
+
+                desktop.Shutdown();
+            }
+        });
     }
 
     private string GetApiUrl()

+ 23 - 7
src/PixiEditor/ViewModels/SubViewModels/MiscViewModel.cs

@@ -1,4 +1,9 @@
-using PixiEditor.Helpers;
+using System.Diagnostics;
+using System.Reflection;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using PixiEditor.Helpers;
+using PixiEditor.Initialization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.OperatingSystem;
@@ -15,15 +20,20 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.Links.OpenHyperlink")]
-    [Command.Basic("PixiEditor.Links.OpenDocumentation", "https://pixieditor.net/docs/introduction", "DOCUMENTATION", "OPEN_DOCUMENTATION", Icon = PixiPerfectIcons.Globe,
+    [Command.Basic("PixiEditor.Links.OpenDocumentation", "https://pixieditor.net/docs/introduction", "DOCUMENTATION",
+        "OPEN_DOCUMENTATION", Icon = PixiPerfectIcons.Globe,
         MenuItemPath = "HELP/DOCUMENTATION", MenuItemOrder = 0, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Links.OpenWebsite", "https://pixieditor.net", "WEBSITE", "OPEN_WEBSITE", Icon = PixiPerfectIcons.Globe,
+    [Command.Basic("PixiEditor.Links.OpenWebsite", "https://pixieditor.net", "WEBSITE", "OPEN_WEBSITE",
+        Icon = PixiPerfectIcons.Globe,
         MenuItemPath = "HELP/WEBSITE", MenuItemOrder = 1, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "REPOSITORY", "OPEN_REPOSITORY", Icon = PixiPerfectIcons.Globe,
+    [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "REPOSITORY",
+        "OPEN_REPOSITORY", Icon = PixiPerfectIcons.Globe,
         MenuItemPath = "HELP/REPOSITORY", MenuItemOrder = 2, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Links.OpenLicense", "{BaseDir}LICENSE", "LICENSE", "OPEN_LICENSE", Icon = PixiPerfectIcons.Folder,
+    [Command.Basic("PixiEditor.Links.OpenLicense", "{BaseDir}LICENSE", "LICENSE", "OPEN_LICENSE",
+        Icon = PixiPerfectIcons.Folder,
         MenuItemPath = "HELP/LICENSE", MenuItemOrder = 3, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "{BaseDir}/Third Party Licenses", "THIRD_PARTY_LICENSES", "OPEN_THIRD_PARTY_LICENSES", Icon = PixiPerfectIcons.Folder,
+    [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "{BaseDir}/Third Party Licenses", "THIRD_PARTY_LICENSES",
+        "OPEN_THIRD_PARTY_LICENSES", Icon = PixiPerfectIcons.Folder,
         MenuItemPath = "HELP/THIRD_PARTY_LICENSES", MenuItemOrder = 4, AnalyticsTrack = true)]
     public static void OpenUri(string uri)
     {
@@ -33,7 +43,7 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
             {
                 uri = uri.Replace("{BaseDir}", AppDomain.CurrentDomain.BaseDirectory);
             }
-            
+
             IOperatingSystem.Current.OpenUri(uri);
         }
         catch (Exception e)
@@ -42,4 +52,10 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
             NoticeDialog.Show(title: "Error", message: $"Couldn't open the address {uri} in your default browser");
         }
     }
+
+    [Command.Internal("PixiEditor.Restart")]
+    public static void Restart()
+    {
+        ClassicDesktopEntry.Active?.Restart();
+    }
 }

+ 23 - 3
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -135,7 +135,14 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         {
             bool isInstalled = IsInstalled(product.Id);
 
-            OwnedProducts.Add(new OwnedProductViewModel(product, isInstalled, InstallContentCommand, IsInstalled));
+            string? installedVersion = null;
+            if (isInstalled)
+            {
+                installedVersion = Owner.ExtensionsSubViewModel.ExtensionLoader.LoadedExtensions
+                    .FirstOrDefault(x => x.Metadata.UniqueName == product.Id)?.Metadata.Version;
+            }
+
+            OwnedProducts.Add(new OwnedProductViewModel(product, isInstalled, installedVersion, InstallContentCommand, IsInstalled));
         }
 
         NotifyProperties();
@@ -282,7 +289,21 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
 
     public bool CanInstallContent(string productId)
     {
-        return !IsInstalled(productId);
+        return !IsInstalled(productId) || UpdateAvailable(productId);
+    }
+
+    private bool UpdateAvailable(string productId)
+    {
+        ProductData product = IdentityProvider.User.OwnedProducts
+            .FirstOrDefault(x => x.Id == productId);
+
+        if (product == null)
+        {
+            return false;
+        }
+
+        return Owner.ExtensionsSubViewModel.ExtensionLoader.LoadedExtensions
+                   .FirstOrDefault(x => x.Metadata.UniqueName == productId)?.Metadata.Version != product.LatestVersion;
     }
 
     private bool IsInstalled(string productId)
@@ -312,7 +333,6 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             {
                 Owner.ExtensionsSubViewModel.LoadExtensionAdHoc(extensionPath);
             }
-
         }
         catch (Exception ex)
         {

+ 32 - 7
src/PixiEditor/ViewModels/User/OwnedProductViewModel.cs

@@ -25,20 +25,45 @@ public class OwnedProductViewModel : ObservableObject
         set => SetProperty(ref isInstalling, value);
     }
 
+    private bool updateAvailable;
+
+    public bool UpdateAvailable
+    {
+        get => updateAvailable;
+        set => SetProperty(ref updateAvailable, value);
+    }
+
+    private bool restartRequired;
+    public bool RestartRequired
+    {
+        get => restartRequired;
+        set => SetProperty(ref restartRequired, value);
+    }
+
     public IAsyncRelayCommand InstallCommand { get; }
 
-    public OwnedProductViewModel(ProductData productData, bool isInstalled,
+    public OwnedProductViewModel(ProductData productData, bool isInstalled, string? installedVersion,
         IAsyncRelayCommand<string> installContentCommand, Func<string, bool> isInstalledFunc)
     {
         ProductData = productData;
         IsInstalled = isInstalled;
+        if (productData.LatestVersion != null && installedVersion != null)
+        {
+            UpdateAvailable = productData.LatestVersion != installedVersion;
+        }
+        else
+        {
+            UpdateAvailable = false;
+        }
+
         InstallCommand = new AsyncRelayCommand(
             async () =>
-        {
-            IsInstalling = true;
-            await installContentCommand.ExecuteAsync(ProductData.Id);
-            IsInstalling = false;
-            IsInstalled = isInstalledFunc(ProductData.Id);
-        }, () => !IsInstalled && !IsInstalling);
+            {
+                IsInstalling = true;
+                UpdateAvailable = false;
+                await installContentCommand.ExecuteAsync(ProductData.Id);
+                IsInstalling = false;
+                RestartRequired = true;
+            }, () => !IsInstalled && !IsInstalling || UpdateAvailable);
     }
 }

+ 31 - 4
src/PixiEditor/Views/Auth/UserAvatarToggle.axaml

@@ -42,7 +42,7 @@
                             </Border>
                             <TextBlock Margin="0, 12, 0, 24" HorizontalAlignment="Center"
                                        FontSize="{DynamicResource FontSizeLarge}">
-                                <Run ui:Translator.Key="LOGGED_IN_AS"/>
+                                <Run ui:Translator.Key="LOGGED_IN_AS" />
                                 <Run Text="{Binding Username}" />
                             </TextBlock>
 
@@ -59,23 +59,44 @@
                                             <TextBlock DockPanel.Dock="Left" HorizontalAlignment="Left"
                                                        Text="{Binding ProductData.DisplayName}" />
                                             <Button DockPanel.Dock="Right" HorizontalAlignment="Right"
+                                                    IsVisible="{Binding !RestartRequired}"
+                                                    FontSize="{DynamicResource FontSizeNormal}"
                                                     Command="{Binding InstallCommand}">
                                                 <Panel Margin="4 0">
-                                                    <TextBlock IsVisible="{Binding IsInstalled}">
+                                                    <TextBlock>
+                                                        <TextBlock.IsVisible>
+                                                            <MultiBinding Converter="{converters:AllTrueConverter}">
+                                                                <Binding Path="IsInstalled" />
+                                                                <Binding Path="!UpdateAvailable" />
+                                                            </MultiBinding>
+                                                        </TextBlock.IsVisible>
                                                         <Run Classes="pixi-icon"
-                                                              Text="{DynamicResource icon-checkTick}"/>
+                                                             Text="{DynamicResource icon-checkTick}" />
                                                         <Run ui:Translator.Key="INSTALLED" />
                                                     </TextBlock>
 
+                                                    <TextBlock Foreground="{DynamicResource ThemeAccent3Brush}">
+                                                        <TextBlock.IsVisible>
+                                                            <MultiBinding Converter="{converters:AllTrueConverter}">
+                                                                <Binding Path="UpdateAvailable" />
+                                                                <Binding Path="!IsInstalling" />
+                                                            </MultiBinding>
+                                                        </TextBlock.IsVisible>
+                                                        <Run Classes="pixi-icon"
+                                                             Text="{DynamicResource icon-download}" />
+                                                        <Run ui:Translator.Key="UPDATE" />
+                                                    </TextBlock>
+
                                                     <TextBlock>
                                                         <TextBlock.IsVisible>
                                                             <MultiBinding Converter="{converters:AllTrueConverter}">
                                                                 <Binding Path="!IsInstalled" />
                                                                 <Binding Path="!IsInstalling" />
+                                                                <Binding Path="!UpdateAvailable" />
                                                             </MultiBinding>
                                                         </TextBlock.IsVisible>
 
-                                                        <Run Classes="pixi-icon" Text="{DynamicResource icon-download}"/>
+                                                        <Run Classes="pixi-icon" Text="{DynamicResource icon-download}" />
                                                         <Run ui:Translator.Key="INSTALL" />
                                                     </TextBlock>
 
@@ -83,6 +104,12 @@
                                                                ui:Translator.Key="INSTALLING" />
                                                 </Panel>
                                             </Button>
+                                            <Button DockPanel.Dock="Right" HorizontalAlignment="Right"
+                                                    FontSize="{DynamicResource FontSizeNormal}"
+                                                    IsVisible="{Binding RestartRequired}"
+                                                    ui:Translator.Key="RESTART"
+                                                    Background="{DynamicResource ThemeAccentBrush}"
+                                                    Command="{xaml:Command Name=PixiEditor.Restart}"/>
                                         </DockPanel>
                                     </DataTemplate>
                                 </ItemsControl.ItemTemplate>