Browse Source

Installing extension

Krzysztof Krysiński 4 months ago
parent
commit
d40d5ed638

+ 1 - 1
src/PixiEditor.Extensions.CommonApi/.config/dotnet-tools.json

@@ -3,7 +3,7 @@
   "isRoot": true,
   "isRoot": true,
   "tools": {
   "tools": {
     "protobuf-net.protogen": {
     "protobuf-net.protogen": {
-      "version": "3.2.42",
+      "version": "3.2.52",
       "commands": [
       "commands": [
         "protogen"
         "protogen"
       ],
       ],

+ 8 - 2
src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.Impl.cs

@@ -2,7 +2,12 @@
 
 
 public partial class FilteringSettings
 public partial class FilteringSettings
 {
 {
-    public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites, List<string> favourites)
+    public FilteringSettings()
+    {
+    }
+
+    public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites,
+        List<string> favourites)
     {
     {
         ColorsNumberMode = colorsNumberMode;
         ColorsNumberMode = colorsNumberMode;
         ColorsCount = colorsCount;
         ColorsCount = colorsCount;
@@ -14,7 +19,8 @@ public partial class FilteringSettings
     public bool Filter(IPalette palette)
     public bool Filter(IPalette palette)
     {
     {
         // Lexical comparison
         // Lexical comparison
-        bool result = string.IsNullOrWhiteSpace(Name) || palette.Name.Contains(Name, StringComparison.OrdinalIgnoreCase);
+        bool result = string.IsNullOrWhiteSpace(Name) ||
+                      palette.Name.Contains(Name, StringComparison.OrdinalIgnoreCase);
 
 
         if (!result)
         if (!result)
         {
         {

+ 2 - 2
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -11,11 +11,11 @@
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>
-      <PackageReference Include="protobuf-net" Version="3.2.45" />
+      <PackageReference Include="protobuf-net" Version="3.2.52" />
     </ItemGroup>
     </ItemGroup>
   
   
   <PropertyGroup>
   <PropertyGroup>
-    <ProtogenVersion>3.2.42</ProtogenVersion>
+    <ProtogenVersion>3.2.52</ProtogenVersion>
   </PropertyGroup>
   </PropertyGroup>
   
   
   <Target Name="ProtogenCheck" BeforeTargets="GenerateProtoContracts">
   <Target Name="ProtogenCheck" BeforeTargets="GenerateProtoContracts">

+ 29 - 18
src/PixiEditor.Extensions.Runtime/ExtensionLoader.cs

@@ -10,9 +10,11 @@ namespace PixiEditor.Extensions.Runtime;
 
 
 public class ExtensionLoader
 public class ExtensionLoader
 {
 {
-    private readonly Dictionary<string, OfficialExtensionData> _officialExtensionsKeys = new Dictionary<string, OfficialExtensionData>();
+    private readonly Dictionary<string, OfficialExtensionData> _officialExtensionsKeys =
+        new Dictionary<string, OfficialExtensionData>();
+
     public List<Extension> LoadedExtensions { get; } = new();
     public List<Extension> LoadedExtensions { get; } = new();
-    
+
     public string PackagesPath { get; }
     public string PackagesPath { get; }
     public string UnpackedExtensionsPath { get; }
     public string UnpackedExtensionsPath { get; }
 
 
@@ -24,7 +26,7 @@ public class ExtensionLoader
         UnpackedExtensionsPath = unpackedExtensionsPath;
         UnpackedExtensionsPath = unpackedExtensionsPath;
         ValidateExtensionFolder();
         ValidateExtensionFolder();
     }
     }
-    
+
     public void AddOfficialExtension(string uniqueName, OfficialExtensionData data)
     public void AddOfficialExtension(string uniqueName, OfficialExtensionData data)
     {
     {
         _officialExtensionsKeys.Add(uniqueName, data);
         _officialExtensionsKeys.Add(uniqueName, data);
@@ -85,17 +87,23 @@ public class ExtensionLoader
     {
     {
         var extZip = ZipFile.OpenRead(extension);
         var extZip = ZipFile.OpenRead(extension);
         ExtensionMetadata metadata = ExtractMetadata(extZip);
         ExtensionMetadata metadata = ExtractMetadata(extZip);
-        if(IsDifferentThanCached(metadata, extension))
+        bool isLoaded = LoadedExtensions.Any(x => x.Metadata.UniqueName == metadata.UniqueName);
+        if (isLoaded)
+        {
+            return null;
+        }
+
+        if (IsDifferentThanCached(metadata, extension))
         {
         {
             UnpackExtension(extZip, metadata);
             UnpackExtension(extZip, metadata);
         }
         }
-        
+
         string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
         string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
         if (!File.Exists(extensionJson))
         if (!File.Exists(extensionJson))
         {
         {
             return null;
             return null;
         }
         }
-            
+
         return LoadExtensionFromCache(extensionJson);
         return LoadExtensionFromCache(extensionJson);
     }
     }
 
 
@@ -124,7 +132,7 @@ public class ExtensionLoader
         var serializer = new JsonSerializer();
         var serializer = new JsonSerializer();
         return serializer.Deserialize<ExtensionMetadata>(jsonTextReader);
         return serializer.Deserialize<ExtensionMetadata>(jsonTextReader);
     }
     }
-    
+
     private bool IsDifferentThanCached(ExtensionMetadata metadata, string extension)
     private bool IsDifferentThanCached(ExtensionMetadata metadata, string extension)
     {
     {
         string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
         string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
@@ -135,24 +143,24 @@ public class ExtensionLoader
 
 
         string json = File.ReadAllText(extensionJson);
         string json = File.ReadAllText(extensionJson);
         ExtensionMetadata? cachedMetadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
         ExtensionMetadata? cachedMetadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
-        
-        if(cachedMetadata is null)
+
+        if (cachedMetadata is null)
         {
         {
             return true;
             return true;
         }
         }
-        
+
         if (metadata.UniqueName != cachedMetadata.UniqueName)
         if (metadata.UniqueName != cachedMetadata.UniqueName)
         {
         {
             return true;
             return true;
         }
         }
-        
+
         bool isDifferent = metadata.Version != cachedMetadata.Version;
         bool isDifferent = metadata.Version != cachedMetadata.Version;
-        
+
         if (isDifferent)
         if (isDifferent)
         {
         {
             return true;
             return true;
         }
         }
-        
+
         return PackageWriteTimeIsBigger(Path.Combine(UnpackedExtensionsPath, metadata.UniqueName), extension);
         return PackageWriteTimeIsBigger(Path.Combine(UnpackedExtensionsPath, metadata.UniqueName), extension);
     }
     }
 
 
@@ -249,7 +257,7 @@ public class ExtensionLoader
 
 
         if (fixedUniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
         if (fixedUniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
         {
         {
-            if(!IsOfficialAssemblyLegit(fixedUniqueName, assembly))
+            if (!IsOfficialAssemblyLegit(fixedUniqueName, assembly))
             {
             {
                 throw new ForbiddenUniqueNameExtension();
                 throw new ForbiddenUniqueNameExtension();
             }
             }
@@ -319,7 +327,9 @@ public class ExtensionLoader
     private bool PublicKeysMatch(byte[] assemblyPublicKey, string pathToPublicKey)
     private bool PublicKeysMatch(byte[] assemblyPublicKey, string pathToPublicKey)
     {
     {
         Assembly currentAssembly = Assembly.GetExecutingAssembly();
         Assembly currentAssembly = Assembly.GetExecutingAssembly();
-        using Stream? stream = currentAssembly.GetManifestResourceStream($"{currentAssembly.GetName().Name}.OfficialExtensions.{pathToPublicKey}");
+        using Stream? stream =
+            currentAssembly.GetManifestResourceStream(
+                $"{currentAssembly.GetName().Name}.OfficialExtensions.{pathToPublicKey}");
         if (stream == null) return false;
         if (stream == null) return false;
 
 
         using MemoryStream memoryStream = new MemoryStream();
         using MemoryStream memoryStream = new MemoryStream();
@@ -330,8 +340,9 @@ public class ExtensionLoader
     }
     }
 
 
     //TODO: uhh, other platforms dumbass?
     //TODO: uhh, other platforms dumbass?
-    [DllImport("mscoree.dll", CharSet=CharSet.Unicode)]
-    static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool pfWasVerified);
+    [DllImport("mscoree.dll", CharSet = CharSet.Unicode)]
+    static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification,
+        ref bool pfWasVerified);
 
 
     private Extension LoadExtensionEntry(ExtensionEntry entry, ExtensionMetadata metadata)
     private Extension LoadExtensionEntry(ExtensionEntry entry, ExtensionMetadata metadata)
     {
     {
@@ -389,7 +400,7 @@ public class ExtensionLoader
         {
         {
             Directory.CreateDirectory(PackagesPath);
             Directory.CreateDirectory(PackagesPath);
         }
         }
-        
+
         if (!Directory.Exists(UnpackedExtensionsPath))
         if (!Directory.Exists(UnpackedExtensionsPath))
         {
         {
             Directory.CreateDirectory(UnpackedExtensionsPath);
             Directory.CreateDirectory(UnpackedExtensionsPath);

+ 2 - 1
src/PixiEditor.Extensions/Common/Localization/ILocalizationProvider.cs

@@ -15,7 +15,8 @@ public interface ILocalizationProvider
     ///     Loads the localization data from the specified file.
     ///     Loads the localization data from the specified file.
     /// </summary>
     /// </summary>
     public void LoadData(string currentLanguageCode = null);
     public void LoadData(string currentLanguageCode = null);
-    public void LoadLanguage(LanguageData languageData);
+    public void LoadLanguage(LanguageData languageData, bool forceReload = false);
+    public void LoadExtensionData(Extension extension);
     public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft);
     public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft);
     public void ReloadLanguage();
     public void ReloadLanguage();
     public Language DefaultLanguage { get; }
     public Language DefaultLanguage { get; }

+ 106 - 0
src/PixiEditor.PixiAuth/PixiAuthClient.cs

@@ -206,4 +206,110 @@ public class PixiAuthClient
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
         }
         }
     }
     }
+
+    public async Task<bool> OwnsProduct(string token, string productId)
+    {
+        HttpRequestMessage request =
+            new HttpRequestMessage(HttpMethod.Get, $"/session/ownsProduct?productId={productId}");
+        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+        var response = await httpClient.SendAsync(request);
+
+        if (response.StatusCode == HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
+
+        if (response.StatusCode == HttpStatusCode.InternalServerError)
+        {
+            throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
+        }
+
+        if (response.StatusCode == HttpStatusCode.OK)
+        {
+            string result = await response.Content.ReadAsStringAsync();
+            Dictionary<string, string>? resultDict =
+                System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(result);
+            if (resultDict != null && resultDict.TryGetValue("ownsProduct", out string? ownsProductString))
+            {
+                if (bool.TryParse(ownsProductString, out bool ownsProduct))
+                {
+                    return ownsProduct;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public async Task<List<string>> GetOwnedProducts(string token)
+    {
+        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "/content/getOwnedProducts");
+        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+        var response = await httpClient.SendAsync(request);
+
+        if (response.StatusCode == HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
+
+        if (response.StatusCode == HttpStatusCode.InternalServerError)
+        {
+            throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
+        }
+
+        if (response.StatusCode == HttpStatusCode.OK)
+        {
+            string result = await response.Content.ReadAsStringAsync();
+            List<Dictionary<string, string>>? ownedProducts =
+                System.Text.Json.JsonSerializer.Deserialize<List<Dictionary<string, string>>>(result);
+
+            if (ownedProducts != null)
+            {
+                List<string> productIds = new List<string>();
+                foreach (var ownedProduct in ownedProducts)
+                {
+                    if (ownedProduct.TryGetValue("productId", out string? productId))
+                    {
+                        productIds.Add(productId);
+                    }
+                }
+
+                return productIds;
+            }
+        }
+
+        return new List<string>();
+    }
+
+    public async Task<Stream> DownloadProduct(string token, string productId)
+    {
+        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"/content/downloadProduct?productId={productId}");
+        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+        request.Content = JsonContent.Create(productId);
+
+        var response = await httpClient.SendAsync(request);
+
+        if (response.StatusCode == HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
+
+        if (response.StatusCode == HttpStatusCode.InternalServerError)
+        {
+            throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
+        }
+
+        if (response.StatusCode == HttpStatusCode.OK)
+        {
+            var result = await response.Content.ReadAsStreamAsync();
+            if (result != null)
+            {
+                return result;
+            }
+        }
+
+        throw new BadRequestException("DOWNLOAD_FAILED");
+    }
 }
 }

+ 2 - 0
src/PixiEditor.PixiAuth/User.cs

@@ -9,6 +9,8 @@ public class User
     public string? SessionToken { get; set; } = string.Empty;
     public string? SessionToken { get; set; } = string.Empty;
     public DateTime? SessionExpirationDate { get; set; }
     public DateTime? SessionExpirationDate { get; set; }
 
 
+    public List<string> OwnedProducts { get; set; } = new();
+
     public User()
     public User()
     {
     {
 
 

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

@@ -1052,5 +1052,6 @@
   "CUSTOM_BACKGROUND_SCALE": "Custom background scale",
   "CUSTOM_BACKGROUND_SCALE": "Custom background scale",
   "PRIMARY_BG_COLOR": "Primary background color",
   "PRIMARY_BG_COLOR": "Primary background color",
   "SECONDARY_BG_COLOR": "Secondary background color",
   "SECONDARY_BG_COLOR": "Secondary background color",
-  "RESET": "Reset"
+  "RESET": "Reset",
+  "INSTALL": "Install"
 }
 }

+ 32 - 20
src/PixiEditor/Models/Localization/LocalizationProvider.cs

@@ -16,7 +16,8 @@ internal class LocalizationProvider : ILocalizationProvider
 {
 {
     private Language debugLanguage;
     private Language debugLanguage;
 
 
-    public string LocalizationDataPath { get; } = Path.Combine(Paths.DataResourceUri, "Localization", "LocalizationData.json");
+    public string LocalizationDataPath { get; } =
+        Path.Combine(Paths.DataResourceUri, "Localization", "LocalizationData.json");
 
 
     public LocalizationData LocalizationData { get; private set; }
     public LocalizationData LocalizationData { get; private set; }
 
 
@@ -43,14 +44,17 @@ internal class LocalizationProvider : ILocalizationProvider
     public void LoadData(string currentLanguageCode = null)
     public void LoadData(string currentLanguageCode = null)
     {
     {
         JsonSerializer serializer = new();
         JsonSerializer serializer = new();
-        
+
         if (!AssetLoader.Exists(new Uri(LocalizationDataPath)))
         if (!AssetLoader.Exists(new Uri(LocalizationDataPath)))
         {
         {
             throw new FileNotFoundException("Localization data file not found.", LocalizationDataPath);
             throw new FileNotFoundException("Localization data file not found.", LocalizationDataPath);
         }
         }
-        
+
         using Stream stream = AssetLoader.Open(new Uri(LocalizationDataPath));
         using Stream stream = AssetLoader.Open(new Uri(LocalizationDataPath));
-        LocalizationData = serializer.Deserialize<LocalizationData>(new JsonTextReader(new StreamReader(stream)) { Culture = CultureInfo.InvariantCulture, DateTimeZoneHandling = DateTimeZoneHandling.Utc });
+        LocalizationData = serializer.Deserialize<LocalizationData>(new JsonTextReader(new StreamReader(stream))
+        {
+            Culture = CultureInfo.InvariantCulture, DateTimeZoneHandling = DateTimeZoneHandling.Utc
+        });
 
 
         if (LocalizationData is null)
         if (LocalizationData is null)
         {
         {
@@ -65,15 +69,26 @@ internal class LocalizationProvider : ILocalizationProvider
         }
         }
 
 
         LocalizationData.Languages.Add(FollowSystem);
         LocalizationData.Languages.Add(FollowSystem);
-        
+
         DefaultLanguage = LoadLanguageInternal(LocalizationData.Languages[0]);
         DefaultLanguage = LoadLanguageInternal(LocalizationData.Languages[0]);
 
 
         LoadLanguage(LocalizationData.Languages.FirstOrDefault(x => x.Code == currentLanguageCode, FollowSystem));
         LoadLanguage(LocalizationData.Languages.FirstOrDefault(x => x.Code == currentLanguageCode, FollowSystem));
     }
     }
 
 
+    public void LoadExtensionData(Extension extension)
+    {
+        LoadExtensionData(extension, LocalizationData);
+        LoadLanguage(CurrentLanguage.LanguageData, true);
+    }
+
+    private void LoadExtensionData(Extension extension, LocalizationData data)
+    {
+        data.MergeWith(extension.Metadata.Localization.Languages, Path.GetDirectoryName(extension.Location));
+    }
+
     private void LoadExtensionLocalizationData(LocalizationData localizationData)
     private void LoadExtensionLocalizationData(LocalizationData localizationData)
     {
     {
-        if(localizationData is null)
+        if (localizationData is null)
         {
         {
             throw new InvalidDataException(nameof(localizationData));
             throw new InvalidDataException(nameof(localizationData));
         }
         }
@@ -90,32 +105,33 @@ internal class LocalizationProvider : ILocalizationProvider
                 continue;
                 continue;
             }
             }
 
 
-            localizationData.MergeWith(extension.Metadata.Localization.Languages, Path.GetDirectoryName(extension.Location));
+            LoadExtensionData(extension, localizationData);
         }
         }
     }
     }
 
 
-    public void LoadLanguage(LanguageData languageData)
+    public void LoadLanguage(LanguageData languageData, bool forceReload = false)
     {
     {
         if (languageData is null)
         if (languageData is null)
         {
         {
             throw new ArgumentNullException(nameof(languageData));
             throw new ArgumentNullException(nameof(languageData));
         }
         }
-        
-        if(languageData.Code == CurrentLanguage?.LanguageData.Code)
+
+        if (languageData.Code == CurrentLanguage?.LanguageData.Code && !forceReload)
         {
         {
             return;
             return;
         }
         }
-        
+
         bool firstLoad = CurrentLanguage is null;
         bool firstLoad = CurrentLanguage is null;
-        
+
         SelectedLanguage = languageData;
         SelectedLanguage = languageData;
 
 
         if (languageData.Code == FollowSystem.Code)
         if (languageData.Code == FollowSystem.Code)
         {
         {
             string osLanguage = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
             string osLanguage = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
-            languageData = LocalizationData.Languages.FirstOrDefault(x => x.Code == osLanguage, LocalizationData.Languages[0]);
+            languageData =
+                LocalizationData.Languages.FirstOrDefault(x => x.Code == osLanguage, LocalizationData.Languages[0]);
         }
         }
-        
+
         CurrentLanguage = LoadLanguageInternal(languageData);
         CurrentLanguage = LoadLanguageInternal(languageData);
 
 
         if (!firstLoad)
         if (!firstLoad)
@@ -127,14 +143,10 @@ internal class LocalizationProvider : ILocalizationProvider
     public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft)
     public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft)
     {
     {
         debugLanguage = new Language(
         debugLanguage = new Language(
-            new LanguageData
-        {
-            Code = "debug",
-            Name = "Debug"
-        }, languageKeys, rightToLeft);
+            new LanguageData { Code = "debug", Name = "Debug" }, languageKeys, rightToLeft);
 
 
         CurrentLanguage = debugLanguage;
         CurrentLanguage = debugLanguage;
-        
+
         OnLanguageChanged?.Invoke(debugLanguage);
         OnLanguageChanged?.Invoke(debugLanguage);
     }
     }
 
 

+ 17 - 0
src/PixiEditor/ViewModels/SubViewModels/ExtensionsViewModel.cs

@@ -1,5 +1,6 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Extensions;
 using PixiEditor.Extensions;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Models.ExtensionServices;
 using PixiEditor.Models.ExtensionServices;
@@ -10,6 +11,7 @@ namespace PixiEditor.ViewModels.SubViewModels;
 internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
 internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
 {
 {
     public ExtensionLoader ExtensionLoader { get; }
     public ExtensionLoader ExtensionLoader { get; }
+
     public ExtensionsViewModel(ViewModelMain owner, ExtensionLoader loader) : base(owner)
     public ExtensionsViewModel(ViewModelMain owner, ExtensionLoader loader) : base(owner)
     {
     {
         ExtensionLoader = loader;
         ExtensionLoader = loader;
@@ -24,6 +26,21 @@ internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
         windowProvider?.RegisterWindow<PalettesBrowser>();
         windowProvider?.RegisterWindow<PalettesBrowser>();
     }
     }
 
 
+    public void LoadExtensionAdHoc(string extension)
+    {
+        if (extension.EndsWith(".pixiext"))
+        {
+            var loadedExtension = ExtensionLoader.LoadExtension(extension);
+            if (loadedExtension is null)
+            {
+                return;
+            }
+
+            ILocalizationProvider.Current.LoadExtensionData(loadedExtension);
+            loadedExtension.Initialize(new ExtensionServices(Owner.Services));
+        }
+    }
+
     private void Owner_OnStartupEvent()
     private void Owner_OnStartupEvent()
     {
     {
         ExtensionLoader.InitializeExtensions(new ExtensionServices(Owner.Services));
         ExtensionLoader.InitializeExtensions(new ExtensionServices(Owner.Services));

+ 66 - 0
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
 using Avalonia.Threading;
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
@@ -25,6 +26,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     public AsyncRelayCommand TryValidateSessionCommand { get; }
     public AsyncRelayCommand TryValidateSessionCommand { get; }
     public AsyncRelayCommand<string> ResendActivationCommand { get; }
     public AsyncRelayCommand<string> ResendActivationCommand { get; }
     public AsyncRelayCommand LogoutCommand { get; }
     public AsyncRelayCommand LogoutCommand { get; }
+    public AsyncRelayCommand<string> InstallContentCommand { get; }
 
 
     private string lastSentHash = string.Empty;
     private string lastSentHash = string.Empty;
 
 
@@ -55,6 +57,8 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     public string? UserGravatarUrl =>
     public string? UserGravatarUrl =>
         User?.EmailHash != null ? $"https://www.gravatar.com/avatar/{User.EmailHash}?s=100&d=initials" : null;
         User?.EmailHash != null ? $"https://www.gravatar.com/avatar/{User.EmailHash}?s=100&d=initials" : null;
 
 
+    public ObservableCollection<string> OwnedProducts { get; private set; } = new ObservableCollection<string>();
+
     private string currentEmail = string.Empty;
     private string currentEmail = string.Empty;
     private string username;
     private string username;
 
 
@@ -84,6 +88,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         RequestLoginCommand = new AsyncRelayCommand<string>(RequestLogin, CanRequestLogin);
         RequestLoginCommand = new AsyncRelayCommand<string>(RequestLogin, CanRequestLogin);
         TryValidateSessionCommand = new AsyncRelayCommand(TryValidateSession);
         TryValidateSessionCommand = new AsyncRelayCommand(TryValidateSession);
         ResendActivationCommand = new AsyncRelayCommand<string>(ResendActivation, CanResendActivation);
         ResendActivationCommand = new AsyncRelayCommand<string>(ResendActivation, CanResendActivation);
+        InstallContentCommand = new AsyncRelayCommand<string>(InstallContent);
         LogoutCommand = new AsyncRelayCommand(Logout);
         LogoutCommand = new AsyncRelayCommand(Logout);
 
 
         string baseUrl = BuildConstants.PixiEditorApiUrl;
         string baseUrl = BuildConstants.PixiEditorApiUrl;
@@ -289,6 +294,14 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
                 LastError = null;
                 LastError = null;
                 User.SessionToken = token;
                 User.SessionToken = token;
                 User.SessionExpirationDate = expirationDate;
                 User.SessionExpirationDate = expirationDate;
+                var products = await PixiAuthClient.GetOwnedProducts(User.SessionToken);
+                if (products != null)
+                {
+                    User.OwnedProducts = products;
+                    OwnedProducts = new ObservableCollection<string>(User.OwnedProducts);
+                    NotifyProperties();
+                }
+
                 Task.Run(async () =>
                 Task.Run(async () =>
                 {
                 {
                     string username = User.Username;
                     string username = User.Username;
@@ -343,6 +356,9 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         string? sessionToken = User?.SessionToken;
         string? sessionToken = User?.SessionToken;
 
 
         User = null;
         User = null;
+        LastError = null;
+        OwnedProducts.Clear();
+        Username = string.Empty;
         NotifyProperties();
         NotifyProperties();
         SaveUserInfo();
         SaveUserInfo();
 
 
@@ -363,6 +379,48 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
+    public async Task InstallContent(string productId)
+    {
+        if (!apiValid) return;
+
+        if (User?.SessionToken == null)
+        {
+            LastError = new LocalizedString("NOT_LOGGED_IN");
+            return;
+        }
+
+        try
+        {
+            var stream = await PixiAuthClient.DownloadProduct(User.SessionToken, productId);
+            if (stream != null)
+            {
+                var packagesPath = Owner.ExtensionsSubViewModel.ExtensionLoader.PackagesPath;
+                var filePath = Path.Combine(packagesPath, $"{productId}.pixiext");
+                await using (var fileStream = File.Create(filePath))
+                {
+                    await stream.CopyToAsync(fileStream);
+                }
+
+                await stream.DisposeAsync();
+
+                Owner.ExtensionsSubViewModel.LoadExtensionAdHoc(filePath);
+                LastError = null;
+            }
+        }
+        catch (PixiAuthException authException)
+        {
+            LastError = new LocalizedString(authException.Message);
+        }
+        catch (HttpRequestException httpRequestException)
+        {
+            LastError = new LocalizedString("CONNECTION_ERROR");
+        }
+        catch (TaskCanceledException timeoutException)
+        {
+            LastError = new LocalizedString("CONNECTION_TIMEOUT");
+        }
+    }
+
     public async Task SaveUserInfo()
     public async Task SaveUserInfo()
     {
     {
         try
         try
@@ -383,6 +441,13 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             try
             try
             {
             {
                 User.Username = await TryFetchUserName(User.EmailHash);
                 User.Username = await TryFetchUserName(User.EmailHash);
+                var products = await PixiAuthClient.GetOwnedProducts(User.SessionToken);
+                if (products != null)
+                {
+                    User.OwnedProducts = products;
+                    OwnedProducts = new ObservableCollection<string>(User.OwnedProducts);
+                }
+
                 Username = User.Username;
                 Username = User.Username;
                 NotifyProperties();
                 NotifyProperties();
             }
             }
@@ -458,6 +523,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         OnPropertyChanged(nameof(TimeToEndTimeoutString));
         OnPropertyChanged(nameof(TimeToEndTimeoutString));
         OnPropertyChanged(nameof(UserGravatarUrl));
         OnPropertyChanged(nameof(UserGravatarUrl));
         OnPropertyChanged(nameof(EmailEqualsLastSentMail));
         OnPropertyChanged(nameof(EmailEqualsLastSentMail));
+        OnPropertyChanged(nameof(OwnedProducts));
         ResendActivationCommand.NotifyCanExecuteChanged();
         ResendActivationCommand.NotifyCanExecuteChanged();
     }
     }
 }
 }

+ 20 - 3
src/PixiEditor/Views/Auth/LoginPopup.axaml

@@ -10,12 +10,29 @@
                          mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                          mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                          x:Class="PixiEditor.Views.Auth.LoginPopup"
                          x:Class="PixiEditor.Views.Auth.LoginPopup"
                          CanMinimize="False"
                          CanMinimize="False"
-                         CanResize="False"
-                         Width="320" Height="190"
+                         CanResize="True"
+                         Width="320" MinHeight="190"
                          ui:Translator.Key="LOGIN_WINDOW_TITLE">
                          ui:Translator.Key="LOGIN_WINDOW_TITLE">
     <Design.DataContext>
     <Design.DataContext>
         <subViewModels:UserViewModel />
         <subViewModels:UserViewModel />
     </Design.DataContext>
     </Design.DataContext>
 
 
-    <auth:LoginForm Margin="15" DataContext="{Binding}" />
+    <StackPanel Margin="15" Spacing="5" Orientation="Vertical">
+        <auth:LoginForm DataContext="{Binding}" />
+        <Separator />
+        <TextBlock Text="{ui:Translate Key=OWNED_PRODUCTS}" Classes="h4" />
+        <ItemsControl IsVisible="{Binding !!User}" ItemsSource="{Binding OwnedProducts}">
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <StackPanel Orientation="Vertical" Spacing="5">
+                        <TextBlock Text="{Binding }" />
+                    <Button
+                        Content="{ui:Translate Key=INSTALL}"
+                        Command="{Binding DataContext.InstallContentCommand, RelativeSource={RelativeSource AncestorType=auth:LoginPopup, Mode=FindAncestor}}"
+                        CommandParameter="{Binding}"/>
+                    </StackPanel>
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+        </ItemsControl>
+    </StackPanel>
 </dialogs:PixiEditorPopup>
 </dialogs:PixiEditorPopup>