Browse Source

Merge pull request #550 from PixiEditor/supporter-pack

minor changes
Krzysztof Krysiński 2 years ago
parent
commit
6e1d28b525
100 changed files with 1715 additions and 154 deletions
  1. 7 5
      src/PixiEditor.Extensions/Common/Localization/ILocalizationProvider.cs
  2. 4 2
      src/PixiEditor.Extensions/Common/Localization/Language.cs
  3. 11 2
      src/PixiEditor.Extensions/Common/Localization/LanguageData.cs
  4. 28 0
      src/PixiEditor.Extensions/Common/Localization/LocalizationData.cs
  5. 2 2
      src/PixiEditor.Extensions/Common/Localization/LocalizationKeyShowMode.cs
  6. 3 5
      src/PixiEditor.Extensions/Common/Localization/LocalizedString.cs
  7. 11 5
      src/PixiEditor.Extensions/Common/UserPreferences/IPreferences.cs
  8. 13 0
      src/PixiEditor.Extensions/Common/UserPreferences/PreferencesConstants.cs
  9. 51 0
      src/PixiEditor.Extensions/Extension.cs
  10. 17 0
      src/PixiEditor.Extensions/ExtensionServices.cs
  11. 2 2
      src/PixiEditor.Extensions/Helpers/EnumHelpers.cs
  12. 8 0
      src/PixiEditor.Extensions/Metadata/Author.cs
  13. 17 0
      src/PixiEditor.Extensions/Metadata/ExtensionMetadata.cs
  14. 0 0
      src/PixiEditor.Extensions/Palettes/ColorsNumberMode.cs
  15. 17 0
      src/PixiEditor.Extensions/Palettes/ExtensionPalette.cs
  16. 7 6
      src/PixiEditor.Extensions/Palettes/FilteringSettings.cs
  17. 10 0
      src/PixiEditor.Extensions/Palettes/IPalette.cs
  18. 27 0
      src/PixiEditor.Extensions/Palettes/IPaletteProvider.cs
  19. 56 0
      src/PixiEditor.Extensions/Palettes/PaletteColor.cs
  20. 27 0
      src/PixiEditor.Extensions/Palettes/PaletteListDataSource.cs
  21. 46 0
      src/PixiEditor.Extensions/Palettes/Parsers/PaletteFileData.cs
  22. 3 2
      src/PixiEditor.Extensions/Palettes/Parsers/PaletteFileParser.cs
  23. 21 0
      src/PixiEditor.Extensions/PixiEditor.Extensions.csproj
  24. 38 0
      src/PixiEditor.Extensions/UI/ExternalProperty.cs
  25. 10 0
      src/PixiEditor.Extensions/UI/ICustomTranslatorElement.cs
  26. 25 16
      src/PixiEditor.Extensions/UI/Translator.cs
  27. 12 0
      src/PixiEditor.Extensions/Windowing/IPopupWindow.cs
  28. 15 0
      src/PixiEditor.Extensions/Windowing/IWindowProvider.cs
  29. 34 0
      src/PixiEditor.Extensions/Windowing/PopupWindow.cs
  30. 24 0
      src/PixiEditor.Platform.MSStore/MSAdditionalContentProvider.cs
  31. 14 0
      src/PixiEditor.Platform.MSStore/MicrosoftStorePlatform.cs
  32. 13 0
      src/PixiEditor.Platform.MSStore/PixiEditor.Platform.MSStore.csproj
  33. 13 0
      src/PixiEditor.Platform.Standalone/PixiEditor.Platform.Standalone.csproj
  34. 15 0
      src/PixiEditor.Platform.Standalone/StandaloneAdditionalContentProvider.cs
  35. 14 0
      src/PixiEditor.Platform.Standalone/StandalonePlatform.cs
  36. 38 0
      src/PixiEditor.Platform.Steam/PixiEditor.Platform.Steam.csproj
  37. 24 0
      src/PixiEditor.Platform.Steam/SteamAdditionalContentProvider.cs
  38. 24 0
      src/PixiEditor.Platform.Steam/SteamPlatform.cs
  39. BIN
      src/PixiEditor.Platform.Steam/steam_api.dll
  40. BIN
      src/PixiEditor.Platform.Steam/steam_api64.dll
  41. 12 0
      src/PixiEditor.Platform/IAdditionalContentProvider.cs
  42. 20 0
      src/PixiEditor.Platform/IPlatform.cs
  43. 13 0
      src/PixiEditor.Platform/PixiEditor.Platform.csproj
  44. 19 0
      src/PixiEditor.Platform/PlatformServiceCollection.cs
  45. 261 0
      src/PixiEditor.sln
  46. 28 3
      src/PixiEditor/App.xaml.cs
  47. 18 1
      src/PixiEditor/Data/Localization/Languages/en.json
  48. 1 1
      src/PixiEditor/Data/Localization/LocalizationDataSchema.json
  49. 2 1
      src/PixiEditor/Exceptions/CorruptedFileException.cs
  50. 2 1
      src/PixiEditor/Exceptions/InvalidFileTypeException.cs
  51. 2 1
      src/PixiEditor/Exceptions/MissingFileException.cs
  52. 2 1
      src/PixiEditor/Exceptions/RecoverableException.cs
  53. 2 1
      src/PixiEditor/Helpers/Collections/ActionDisplayList.cs
  54. 2 1
      src/PixiEditor/Helpers/Converters/BlendModeToStringConverter.cs
  55. 2 1
      src/PixiEditor/Helpers/Converters/BoolToValueConverter.cs
  56. 64 0
      src/PixiEditor/Helpers/Converters/DefaultUnknownEnumConverter.cs
  57. 0 1
      src/PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  58. 11 3
      src/PixiEditor/Helpers/Converters/GenericColorToMediaColorConverter.cs
  59. 2 1
      src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  60. 2 1
      src/PixiEditor/Helpers/Converters/LangConverter.cs
  61. 7 6
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  62. 0 1
      src/PixiEditor/Helpers/Extensions/BlendModeEx.cs
  63. 6 0
      src/PixiEditor/Helpers/Extensions/ColorHelpers.cs
  64. 3 2
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  65. 3 2
      src/PixiEditor/Helpers/Extensions/SerializableDocumentEx.cs
  66. 19 4
      src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  67. 12 0
      src/PixiEditor/Helpers/Extensions/WindowExtensions.cs
  68. 2 1
      src/PixiEditor/Helpers/PaletteHelpers.cs
  69. 2 1
      src/PixiEditor/Helpers/RegistryHelpers.cs
  70. BIN
      src/PixiEditor/Images/Chevron-right.png
  71. BIN
      src/PixiEditor/Images/News/Article.png
  72. BIN
      src/PixiEditor/Images/News/Misc.png
  73. BIN
      src/PixiEditor/Images/News/NewVersion.png
  74. BIN
      src/PixiEditor/Images/News/OfficialAnnouncement.png
  75. BIN
      src/PixiEditor/Images/News/YouTube.png
  76. 0 9
      src/PixiEditor/Localization/LocalizationData.cs
  77. 48 0
      src/PixiEditor/Models/AppExtensions/ExtensionException.cs
  78. 232 0
      src/PixiEditor/Models/AppExtensions/ExtensionLoader.cs
  79. 72 0
      src/PixiEditor/Models/AppExtensions/Services/PaletteProvider.cs
  80. 46 0
      src/PixiEditor/Models/AppExtensions/Services/WindowProvider.cs
  81. 2 1
      src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  82. 2 1
      src/PixiEditor/Models/Commands/Attributes/Commands/FilterAttribute.cs
  83. 2 1
      src/PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs
  84. 2 1
      src/PixiEditor/Models/Commands/CommandController.cs
  85. 2 1
      src/PixiEditor/Models/Commands/CommandGroup.cs
  86. 2 1
      src/PixiEditor/Models/Commands/Commands/Command.cs
  87. 3 1
      src/PixiEditor/Models/Commands/Search/ColorSearchResult.cs
  88. 3 1
      src/PixiEditor/Models/DataHolders/KeyCombination.cs
  89. 7 3
      src/PixiEditor/Models/DataHolders/Palettes/Palette.cs
  90. 2 1
      src/PixiEditor/Models/DataHolders/Palettes/PaletteList.cs
  91. 25 0
      src/PixiEditor/Models/DataHolders/Palettes/PaletteObject.cs
  92. 30 30
      src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs
  93. 0 12
      src/PixiEditor/Models/DataProviders/PaletteListDataSource.cs
  94. 2 1
      src/PixiEditor/Models/Dialogs/ConfirmationDialog.cs
  95. 1 1
      src/PixiEditor/Models/Dialogs/NewFileDialog.cs
  96. 2 1
      src/PixiEditor/Models/Dialogs/NoticeDialog.cs
  97. 2 1
      src/PixiEditor/Models/Dialogs/OptionDialog.cs
  98. 2 1
      src/PixiEditor/Models/Dialogs/OptionsDialog.cs
  99. 2 1
      src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs
  100. 4 3
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

+ 7 - 5
src/PixiEditor/Localization/ILocalizationProvider.cs → src/PixiEditor.Extensions/Common/Localization/ILocalizationProvider.cs

@@ -1,11 +1,8 @@
-using System.IO;
-
-namespace PixiEditor.Localization;
+namespace PixiEditor.Extensions.Common.Localization;
 
 
 public interface ILocalizationProvider
 public interface ILocalizationProvider
 {
 {
-    public static ILocalizationProvider Current => ViewModelMain.Current?.LocalizationProvider;
-    
+    public static ILocalizationProvider Current { get; private set; }
     public string LocalizationDataPath { get; }
     public string LocalizationDataPath { get; }
     public LocalizationData LocalizationData { get; }
     public LocalizationData LocalizationData { get; }
     public Language CurrentLanguage { get; set; }
     public Language CurrentLanguage { get; set; }
@@ -21,4 +18,9 @@ public interface ILocalizationProvider
     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; }
+
+    protected static void SetAsCurrent(ILocalizationProvider provider)
+    {
+        Current = provider;
+    }
 }
 }

+ 4 - 2
src/PixiEditor/Localization/Language.cs → src/PixiEditor.Extensions/Common/Localization/Language.cs

@@ -1,11 +1,13 @@
 using System.Diagnostics;
 using System.Diagnostics;
 using System.Windows;
 using System.Windows;
 
 
-namespace PixiEditor.Localization;
+namespace PixiEditor.Extensions.Common.Localization;
 
 
 [DebuggerDisplay("{LanguageData.Name}, strings: {Locale.Count}")]
 [DebuggerDisplay("{LanguageData.Name}, strings: {Locale.Count}")]
 public class Language
 public class Language
 {
 {
+    public static bool FlipFlowDirection { get; set; } = false;
+
     private FlowDirection flowDirection;
     private FlowDirection flowDirection;
     
     
     public LanguageData LanguageData { get; }
     public LanguageData LanguageData { get; }
@@ -15,7 +17,7 @@ public class Language
     {
     {
         get
         get
         {
         {
-            if (ViewModelMain.Current.DebugSubViewModel.ForceOtherFlowDirection)
+            if (FlipFlowDirection)
             {
             {
                 return flowDirection switch
                 return flowDirection switch
                 {
                 {

+ 11 - 2
src/PixiEditor/Localization/LanguageData.cs → src/PixiEditor.Extensions/Common/Localization/LanguageData.cs

@@ -1,7 +1,7 @@
 using System.Globalization;
 using System.Globalization;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
 
 
-namespace PixiEditor.Localization;
+namespace PixiEditor.Extensions.Common.Localization;
 
 
 public class LanguageData
 public class LanguageData
 {
 {
@@ -11,7 +11,13 @@ public class LanguageData
     
     
     // https://icons8.com/icon/set/flags/color
     // https://icons8.com/icon/set/flags/color
     public string IconFileName { get; set; }
     public string IconFileName { get; set; }
-    public string IconPath => $"pack://application:,,,/PixiEditor;component/Images/LanguageFlags/{IconFileName}";
+    public string IconPath = $"pack://application:,,,/PixiEditor;component/Images/LanguageFlags/";
+
+    [JsonIgnore]
+    public List<string> AdditionalLocalePaths { get; set; }
+
+    [JsonIgnore]
+    public string IconFullPath => $"{IconPath}{IconFileName}";
     public bool RightToLeft { get; set; }
     public bool RightToLeft { get; set; }
     
     
     [JsonIgnore]
     [JsonIgnore]
@@ -19,6 +25,9 @@ public class LanguageData
     
     
     [JsonProperty(nameof(LastUpdated))]
     [JsonProperty(nameof(LastUpdated))]
     private string LastUpdatedString { get; set; }
     private string LastUpdatedString { get; set; }
+
+    [JsonIgnore]
+    public string? CustomLocaleAssemblyPath { get; set; }
     
     
     public override string ToString()
     public override string ToString()
     {
     {

+ 28 - 0
src/PixiEditor.Extensions/Common/Localization/LocalizationData.cs

@@ -0,0 +1,28 @@
+using System.Diagnostics;
+using System.IO;
+
+namespace PixiEditor.Extensions.Common.Localization;
+
+[DebuggerDisplay("{Languages.Count} Language(s)")]
+public class LocalizationData
+{
+    public List<LanguageData> Languages { get; set; } = new();
+
+    public void MergeWith(List<LanguageData> toMerge, string assemblyLocation)
+    {
+        foreach (LanguageData language in toMerge)
+        {
+            LanguageData existing = Languages.Find(x => x.Code == language.Code);
+            if (existing is null)
+            {
+                language.CustomLocaleAssemblyPath = assemblyLocation;
+                Languages.Add(language);
+            }
+            else
+            {
+                existing.AdditionalLocalePaths ??= new List<string>();
+                existing.AdditionalLocalePaths.Add(Path.Combine(assemblyLocation, language.LocaleFileName));
+            }
+        }
+    }
+}

+ 2 - 2
src/PixiEditor/Models/Enums/LocalizationKeyShowMode.cs → src/PixiEditor.Extensions/Common/Localization/LocalizationKeyShowMode.cs

@@ -1,6 +1,6 @@
-namespace PixiEditor.Models.Enums;
+namespace PixiEditor.Extensions.Common.Localization;
 
 
-internal enum LocalizationKeyShowMode
+public enum LocalizationKeyShowMode
 {
 {
     /// <summary>
     /// <summary>
     /// Shows just the value e.g. Open
     /// Shows just the value e.g. Open

+ 3 - 5
src/PixiEditor/Localization/LocalizedString.cs → src/PixiEditor.Extensions/Common/Localization/LocalizedString.cs

@@ -1,10 +1,8 @@
-using System.Text;
-using PixiEditor.Models.Enums;
-
-namespace PixiEditor.Localization;
+namespace PixiEditor.Extensions.Common.Localization;
 
 
 public struct LocalizedString
 public struct LocalizedString
 {
 {
+    public static LocalizationKeyShowMode? OverridenKeyFlowMode { get; set; } = null;
     private string key;
     private string key;
 
 
     public string Key
     public string Key
@@ -16,7 +14,7 @@ public struct LocalizedString
             #if DEBUG_LOCALIZATION
             #if DEBUG_LOCALIZATION
             Value = key;
             Value = key;
             #else
             #else
-            Value = ViewModelMain.Current?.DebugSubViewModel?.LocalizationKeyShowMode switch
+            Value = OverridenKeyFlowMode switch
             {
             {
                 LocalizationKeyShowMode.Key => Key,
                 LocalizationKeyShowMode.Key => Key,
                 LocalizationKeyShowMode.ValueKey => $"{GetValue(value)} ({Key})",
                 LocalizationKeyShowMode.ValueKey => $"{GetValue(value)} ({Key})",

+ 11 - 5
src/PixiEditor/Models/UserPreferences/IPreferences.cs → src/PixiEditor.Extensions/Common/UserPreferences/IPreferences.cs

@@ -1,10 +1,8 @@
-using PixiEditor.ViewModels;
+namespace PixiEditor.Extensions.Common.UserPreferences;
 
 
-namespace PixiEditor.Models.UserPreferences;
-
-internal interface IPreferences
+public interface IPreferences
 {
 {
-    public static IPreferences Current => ViewModelMain.Current.Preferences;
+    public static IPreferences Current { get; private set; }
 
 
     /// <summary>
     /// <summary>
     /// Saves the preferences to be stored permanently.
     /// Saves the preferences to be stored permanently.
@@ -26,6 +24,9 @@ internal interface IPreferences
     /// <param name="action">The action that will be executed when the setting changes</param>
     /// <param name="action">The action that will be executed when the setting changes</param>
     public void AddCallback<T>(string name, Action<T> action);
     public void AddCallback<T>(string name, Action<T> action);
 
 
+    public void RemoveCallback(string name, Action<object> action);
+    public void RemoveCallback<T>(string name, Action<T> action);
+
     /// <summary>
     /// <summary>
     /// Initializes the preferences.
     /// Initializes the preferences.
     /// </summary>
     /// </summary>
@@ -85,4 +86,9 @@ internal interface IPreferences
     /// <param name="name">The name of the setting</param>
     /// <param name="name">The name of the setting</param>
     /// <returns>The editor setting or the <paramref name="fallbackValue"/> if it has not been set yet</returns>
     /// <returns>The editor setting or the <paramref name="fallbackValue"/> if it has not been set yet</returns>
     public T? GetLocalPreference<T>(string name, T? fallbackValue);
     public T? GetLocalPreference<T>(string name, T? fallbackValue);
+
+    protected static void SetAsCurrent(IPreferences provider)
+    {
+        Current = provider;
+    }
 }
 }

+ 13 - 0
src/PixiEditor.Extensions/Common/UserPreferences/PreferencesConstants.cs

@@ -0,0 +1,13 @@
+namespace PixiEditor.Extensions.Common.UserPreferences;
+
+public static class PreferencesConstants
+{
+    public const string FavouritePalettes = "FavouritePalettes";
+    public const string RecentlyOpened = "RecentlyOpened";
+
+    public const string MaxOpenedRecently = "MaxOpenedRecently";
+    public const int MaxOpenedRecentlyDefault = 8;
+    public const string DisableNewsPanel = "DisableNewsPanel";
+    public const string LastCheckedNewsIds = "LastCheckedNewsIds";
+    public const string NewsPanelCollapsed = "NewsPanelCollapsed";
+}

+ 51 - 0
src/PixiEditor.Extensions/Extension.cs

@@ -0,0 +1,51 @@
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.Metadata;
+
+namespace PixiEditor.Extensions;
+
+/// <summary>
+///     This class is used to extend the functionality of the PixiEditor.
+/// </summary>
+public abstract class Extension
+{
+    public ExtensionServices Api { get; private set; }
+    public ExtensionMetadata Metadata { get; private set; }
+    public Assembly Assembly => GetType().Assembly;
+
+    public void ProvideMetadata(ExtensionMetadata metadata)
+    {
+        if (Metadata != null)
+        {
+            return;
+        }
+
+        Metadata = metadata;
+    }
+
+    public void Load()
+    {
+        OnLoaded();
+    }
+
+    public void Initialize(ExtensionServices api)
+    {
+        Api = api;
+        OnInitialized();
+    }
+
+    /// <summary>
+    ///     Called right after the extension is loaded. Not all extensions are initialized at this point. PixiEditor API at this point is not available.
+    ///     Use this method to load resources, patch language files, etc.
+    /// </summary>
+    protected virtual void OnLoaded()
+    {
+    }
+
+    /// <summary>
+    ///     Called after all extensions and PixiEditor is loaded. All extensions are initialized at this point.
+    /// </summary>
+    protected virtual void OnInitialized()
+    {
+    }
+}

+ 17 - 0
src/PixiEditor.Extensions/ExtensionServices.cs

@@ -0,0 +1,17 @@
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Extensions.Windowing;
+
+namespace PixiEditor.Extensions;
+
+public class ExtensionServices
+{
+    public IServiceProvider Services { get; private set; }
+    public IWindowProvider WindowProvider => Services.GetRequiredService<IWindowProvider>();
+    public IPaletteProvider PaletteProvider => Services.GetRequiredService<IPaletteProvider>();
+
+    public ExtensionServices(IServiceProvider services)
+    {
+        Services = services;
+    }
+}

+ 2 - 2
src/PixiEditor/Helpers/Extensions/EnumHelpers.cs → src/PixiEditor.Extensions/Helpers/EnumHelpers.cs

@@ -1,8 +1,8 @@
 using System.ComponentModel;
 using System.ComponentModel;
 
 
-namespace PixiEditor.Helpers.Extensions;
+namespace PixiEditor.Extensions.Helpers;
 
 
-internal static class EnumHelpers
+public static class EnumHelpers
 {
 {
     public static IEnumerable<T> GetFlags<T>(this T e)
     public static IEnumerable<T> GetFlags<T>(this T e)
         where T : Enum
         where T : Enum

+ 8 - 0
src/PixiEditor.Extensions/Metadata/Author.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Extensions.Metadata;
+
+public class Author
+{
+    public string Name { get; init; }
+    public string Email { get; init; }
+    public string Website { get; init; }
+}

+ 17 - 0
src/PixiEditor.Extensions/Metadata/ExtensionMetadata.cs

@@ -0,0 +1,17 @@
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.Extensions.Metadata;
+
+public class ExtensionMetadata
+{
+    public string UniqueName { get; init; }
+    public string DisplayName { get; init; }
+    public string Description { get; init; }
+    public Author? Author { get; init; }
+    public Author? Publisher { get; init; }
+    public Author[]? Contributors { get; init; }
+    public string Version { get; init; }
+    public string? License { get; init; }
+    public string[]? Categories { get; init; }
+    public LocalizationData? Localization { get; init; }
+}

+ 0 - 0
src/PixiEditor/Models/Enums/ColorsNumberMode.cs → src/PixiEditor.Extensions/Palettes/ColorsNumberMode.cs


+ 17 - 0
src/PixiEditor.Extensions/Palettes/ExtensionPalette.cs

@@ -0,0 +1,17 @@
+namespace PixiEditor.Extensions.Palettes;
+
+public class ExtensionPalette : IPalette
+{
+    public string Name { get; }
+    public List<PaletteColor> Colors { get; }
+    public bool IsFavourite { get; set; }
+    public string FileName { get; set; }
+    public PaletteListDataSource Source { get; }
+
+    public ExtensionPalette(string name, List<PaletteColor> colors, PaletteListDataSource source)
+    {
+        Name = name;
+        Colors = colors;
+        Source = source;
+    }
+}

+ 7 - 6
src/PixiEditor/Models/DataHolders/Palettes/FilteringSettings.cs → src/PixiEditor.Extensions/Palettes/FilteringSettings.cs

@@ -1,24 +1,25 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 
 
-namespace PixiEditor.Models.DataHolders.Palettes;
+namespace PixiEditor.Extensions.Palettes;
 
 
-internal class FilteringSettings
+public sealed class FilteringSettings
 {
 {
     public ColorsNumberMode ColorsNumberMode { get; set; }
     public ColorsNumberMode ColorsNumberMode { get; set; }
     public int ColorsCount { get; set; }
     public int ColorsCount { get; set; }
     public string Name { get; set; }
     public string Name { get; set; }
-
     public bool ShowOnlyFavourites { get; set; }
     public bool ShowOnlyFavourites { get; set; }
+    public List<string> Favourites { get; set; }
 
 
-    public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites)
+    public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites, List<string> favourites)
     {
     {
         ColorsNumberMode = colorsNumberMode;
         ColorsNumberMode = colorsNumberMode;
         ColorsCount = colorsCount;
         ColorsCount = colorsCount;
         Name = name;
         Name = name;
         ShowOnlyFavourites = showOnlyFavourites;
         ShowOnlyFavourites = showOnlyFavourites;
+        Favourites = favourites;
     }
     }
 
 
-    public bool Filter(Palette 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);
@@ -28,7 +29,7 @@ internal class FilteringSettings
             return false;
             return false;
         }
         }
 
 
-        result = (ShowOnlyFavourites && palette.IsFavourite) || !ShowOnlyFavourites;
+        result = (ShowOnlyFavourites && Favourites != null && Favourites.Contains(palette.Name)) || !ShowOnlyFavourites;
 
 
         if (!result)
         if (!result)
         {
         {

+ 10 - 0
src/PixiEditor.Extensions/Palettes/IPalette.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.Extensions.Palettes;
+
+public interface IPalette
+{
+    public string Name { get; }
+    public List<PaletteColor> Colors { get; }
+    public bool IsFavourite { get; }
+    public string FileName { get; set; }
+    public PaletteListDataSource Source { get; }
+}

+ 27 - 0
src/PixiEditor.Extensions/Palettes/IPaletteProvider.cs

@@ -0,0 +1,27 @@
+namespace PixiEditor.Extensions.Palettes;
+
+public interface IPaletteProvider
+{
+    /// <summary>
+    ///     Fetches palettes from the provider.
+    /// </summary>
+    /// <param name="startIndex">Starting fetch index. Palettes before said index won't be fetched.</param>
+    /// <param name="items">Max amount of palettes to fetch.</param>
+    /// <param name="filtering">Filtering settings for fetching.</param>
+    /// <returns>List of palettes.</returns>
+    public Task<List<IPalette>> FetchPalettes(int startIndex, int items, FilteringSettings filtering);
+
+    /// <summary>
+    ///     Adds a palette to the provider. This means that the palette will be saved in local storage.
+    /// </summary>
+    /// <param name="palette">Palette to save.</param>
+    /// <param name="overwrite">If true and palette with the same name exists, it will be overwritten. If false and palette with the same name exists, it will not be added.</param>
+    /// <returns>True if adding palette was successful.</returns>
+    public Task<bool> AddPalette(IPalette palette, bool overwrite = false);
+
+    /// <summary>
+    ///     Registers a palette list data source. This means that the provider will use the data source to fetch palettes.
+    /// </summary>
+    /// <param name="dataSource">Data source to register.</param>
+    public void RegisterDataSource(PaletteListDataSource dataSource);
+}

+ 56 - 0
src/PixiEditor.Extensions/Palettes/PaletteColor.cs

@@ -0,0 +1,56 @@
+namespace PixiEditor.Extensions.Palettes;
+
+public struct PaletteColor
+{
+    public static PaletteColor Empty => new(0, 0, 0);
+    public static PaletteColor Black => new(0, 0, 0);
+    public static PaletteColor White => new(255, 255, 255);
+    public byte R { get; set; }
+    public byte G { get; set; }
+    public byte B { get; set; }
+
+    public string Hex => $"#{R:X2}{G:X2}{B:X2}";
+
+    public PaletteColor(byte r, byte g, byte b)
+    {
+        R = r;
+        G = g;
+        B = b;
+    }
+
+    public override string ToString()
+    {
+        return Hex;
+    }
+
+    public static bool operator ==(PaletteColor left, PaletteColor right)
+    {
+        return left.R == right.R && left.G == right.G && left.B == right.B;
+    }
+
+    public static bool operator !=(PaletteColor left, PaletteColor right)
+    {
+        return !(left == right);
+    }
+
+    public static PaletteColor Parse(string hexString)
+    {
+        string hex = hexString.Replace("#", string.Empty);
+
+        if (hex.Length == 3)
+        {
+            hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}";
+        }
+
+        if (hex.Length != 6)
+        {
+            throw new ArgumentException("Invalid hex string. Expected format: RRGGBB");
+        }
+
+        byte r = Convert.ToByte(hex.Substring(0, 2), 16);
+        byte g = Convert.ToByte(hex.Substring(2, 2), 16);
+        byte b = Convert.ToByte(hex.Substring(4, 2), 16);
+
+        return new PaletteColor(r, g, b);
+    }
+}

+ 27 - 0
src/PixiEditor.Extensions/Palettes/PaletteListDataSource.cs

@@ -0,0 +1,27 @@
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Palettes.Parsers;
+
+namespace PixiEditor.Extensions.Palettes;
+
+public abstract class PaletteListDataSource
+{
+    public LocalizedString Name { get; set; }
+
+    public PaletteListDataSource(LocalizedString name)
+    {
+        Name = name;
+        AvailableParsers = new List<PaletteFileParser>();
+    }
+
+    public virtual void Initialize() { }
+
+    /// <summary>
+    ///     Fetches palettes from the provider.
+    /// </summary>
+    /// <param name="startIndex">Starting fetch index. Palettes before said index won't be fetched.</param>
+    /// <param name="items">Max amount of palettes to fetch.</param>
+    /// <param name="filtering">Filtering settings for fetching.</param>
+    /// <returns>A List of palettes. Null if fetch wasn't successful.</returns>
+    public abstract Task<List<IPalette>> FetchPaletteList(int startIndex, int items, FilteringSettings filtering);
+    public List<PaletteFileParser> AvailableParsers { get; set; }
+}

+ 46 - 0
src/PixiEditor.Extensions/Palettes/Parsers/PaletteFileData.cs

@@ -0,0 +1,46 @@
+using PixiEditor.Extensions.Palettes;
+
+namespace PixiEditor.Models.IO;
+
+public class PaletteFileData
+{
+    public string Title { get; set; }
+    public PaletteColor[] Colors { get; set; }
+    public bool IsCorrupted { get; set; } = false;
+    public static PaletteFileData Corrupted => new ("Corrupted", Array.Empty<PaletteColor>()) { IsCorrupted = true };
+
+    public PaletteFileData(PaletteColor[] colors)
+    {
+        Colors = colors;
+        Title = "";
+    }
+
+    public PaletteFileData(List<string> colors)
+    {
+        Colors = new PaletteColor[colors.Count];
+        for (int i = 0; i < colors.Count; i++)
+        {
+            Colors[i] = PaletteColor.Parse(colors[i]);
+        }
+
+        Title = "";
+    }
+
+    public PaletteFileData(string title, PaletteColor[] colors)
+    {
+        Title = title;
+        Colors = colors;
+    }
+
+    public PaletteColor[] GetPaletteColors()
+    {
+        PaletteColor[] colors = new PaletteColor[Colors.Length];
+        for (int i = 0; i < Colors.Length; i++)
+        {
+            PaletteColor color = Colors[i];
+            colors[i] = new PaletteColor(color.R, color.G, color.B);
+        }
+
+        return colors;
+    }
+}

+ 3 - 2
src/PixiEditor/Models/IO/PaletteFileParser.cs → src/PixiEditor.Extensions/Palettes/Parsers/PaletteFileParser.cs

@@ -1,8 +1,9 @@
 using System.IO;
 using System.IO;
+using PixiEditor.Models.IO;
 
 
-namespace PixiEditor.Models.IO;
+namespace PixiEditor.Extensions.Palettes.Parsers;
 
 
-internal abstract class PaletteFileParser
+public abstract class PaletteFileParser
 {
 {
     public abstract Task<PaletteFileData> Parse(string path);
     public abstract Task<PaletteFileData> Parse(string path);
     public abstract Task<bool> Save(string path, PaletteFileData data);
     public abstract Task<bool> Save(string path, PaletteFileData data);

+ 21 - 0
src/PixiEditor.Extensions/PixiEditor.Extensions.csproj

@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0-windows</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+      <UseWPF>true</UseWPF>
+      <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+      <Version>0.0.3</Version>
+      <Title>PixiEditor Extensions</Title>
+      <Authors>PixiEditor Organization</Authors>
+      <Copyright>PixiEditor Organization</Copyright>
+      <Description>Package for creating custom extensions for pixel art editor PixiEditor</Description>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
+      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+      <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+    </ItemGroup>
+</Project>

+ 38 - 0
src/PixiEditor.Extensions/UI/ExternalProperty.cs

@@ -0,0 +1,38 @@
+using System.Windows;
+using System.Windows.Data;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.Views;
+
+public abstract class ExternalProperty
+{
+    public Type PropertyType { get; set; }
+
+    public Action<DependencyObject, Binding>? SetTranslationBinding { get; set; }
+    public Action<DependencyObject, LocalizedString>? SetTranslation { get; set; }
+
+    public ExternalProperty(Type propertyType, Action<DependencyObject, Binding> setTranslationBinding)
+    {
+        PropertyType = propertyType;
+        SetTranslationBinding = setTranslationBinding;
+    }
+
+    public ExternalProperty(Type propertyType, Action<DependencyObject, LocalizedString> translationAction)
+    {
+        PropertyType = propertyType;
+        SetTranslation = translationAction;
+    }
+}
+
+public class ExternalProperty<T> : ExternalProperty
+{
+    public ExternalProperty(Action<DependencyObject, Binding> setTranslationBinding) : base(typeof(T), setTranslationBinding)
+    {
+    }
+
+    public ExternalProperty(Action<DependencyObject, LocalizedString> translationAction) : base(typeof(T), translationAction)
+    {
+    }
+}
+
+

+ 10 - 0
src/PixiEditor.Extensions/UI/ICustomTranslatorElement.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+using System.Windows.Data;
+
+namespace PixiEditor.Views;
+
+public interface ICustomTranslatorElement
+{
+    public void SetTranslationBinding(DependencyProperty dependencyProperty, Binding binding);
+    public DependencyProperty GetDependencyProperty();
+}

+ 25 - 16
src/PixiEditor/Views/Translator.cs → src/PixiEditor.Extensions/UI/Translator.cs

@@ -2,14 +2,16 @@
 using System.Windows.Controls;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Documents;
-using AvalonDock.Layout;
-using PixiEditor.Localization;
-using PixiEditor.Views.Dialogs;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Helpers;
+using PixiEditor.Views;
 
 
-namespace PixiEditor.Views;
+namespace PixiEditor.Extensions.UI;
 
 
 public class Translator : UIElement
 public class Translator : UIElement
 {
 {
+    public static List<ExternalProperty> ExternalProperties { get; } = new();
+
     public static readonly DependencyProperty KeyProperty = DependencyProperty.RegisterAttached(
     public static readonly DependencyProperty KeyProperty = DependencyProperty.RegisterAttached(
         "Key",
         "Key",
         typeof(string),
         typeof(string),
@@ -139,7 +141,24 @@ public class Translator : UIElement
             RelativeSource = new RelativeSource(RelativeSourceMode.Self)
             RelativeSource = new RelativeSource(RelativeSourceMode.Self)
         };
         };
 
 
-        if (d is TextBox textBox)
+        ExternalProperty externalProperty = ExternalProperties.FirstOrDefault(x => x.PropertyType.IsAssignableFrom(d.GetType()));
+
+        if (d is ICustomTranslatorElement customTranslatorElement)
+        {
+            customTranslatorElement.SetTranslationBinding(customTranslatorElement.GetDependencyProperty(), binding);
+        }
+        else if (externalProperty != null)
+        {
+            if (externalProperty.SetTranslationBinding != null)
+            {
+                externalProperty.SetTranslationBinding(d, binding);
+            }
+            else
+            {
+                externalProperty.SetTranslation(d, localizedString);
+            }
+        }
+        else if (d is TextBox textBox)
         {
         {
             textBox.SetBinding(TextBox.TextProperty, binding);
             textBox.SetBinding(TextBox.TextProperty, binding);
         }
         }
@@ -155,12 +174,6 @@ public class Translator : UIElement
         {
         {
             window.SetBinding(Window.TitleProperty, binding);
             window.SetBinding(Window.TitleProperty, binding);
         }
         }
-        #if DEBUG
-        else if (d is DialogTitleBar)
-        {
-            throw new ArgumentException($"Use {nameof(DialogTitleBar)}.{nameof(DialogTitleBar.TitleKey)} to set the localization key for the title");
-        }
-        #endif
         else if (d is ContentControl contentControl)
         else if (d is ContentControl contentControl)
         {
         {
             contentControl.SetBinding(ContentControl.ContentProperty, binding);
             contentControl.SetBinding(ContentControl.ContentProperty, binding);
@@ -169,11 +182,7 @@ public class Translator : UIElement
         {
         {
             menuItem.SetBinding(HeaderedItemsControl.HeaderProperty, binding);
             menuItem.SetBinding(HeaderedItemsControl.HeaderProperty, binding);
         }
         }
-        else if (d is LayoutContent layoutContent)
-        {
-            layoutContent.SetValue(LayoutContent.TitleProperty, localizedString.Value);
-        }
-        #if DEBUG
+#if DEBUG
         else
         else
         {
         {
             throw new ArgumentException($"'{d.GetType().Name}' does not support {nameof(Translator)}.Key");
             throw new ArgumentException($"'{d.GetType().Name}' does not support {nameof(Translator)}.Key");

+ 12 - 0
src/PixiEditor.Extensions/Windowing/IPopupWindow.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.Extensions;
+
+public interface IPopupWindow
+{
+    public string UniqueId { get; }
+    public string Title { get; set; }
+    public void Show();
+    public void Close();
+    public bool? ShowDialog();
+    public double Width { get; set; }
+    public double Height { get; set; }
+}

+ 15 - 0
src/PixiEditor.Extensions/Windowing/IWindowProvider.cs

@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace PixiEditor.Extensions.Windowing;
+
+public interface IWindowProvider
+{
+    public PopupWindow CreatePopupWindow(string title, object body);
+    public PopupWindow OpenWindow(WindowType type);
+    public PopupWindow OpenWindow(string windowId);
+}
+
+public enum WindowType
+{
+    BrowserPalette
+}

+ 34 - 0
src/PixiEditor.Extensions/Windowing/PopupWindow.cs

@@ -0,0 +1,34 @@
+namespace PixiEditor.Extensions.Windowing;
+
+public class PopupWindow : IPopupWindow
+{
+    public string UniqueId => _underlyingWindow.UniqueId;
+
+    private IPopupWindow _underlyingWindow;
+
+    public PopupWindow(IPopupWindow basicPopup)
+    {
+        _underlyingWindow = basicPopup;
+    }
+
+    public string Title
+    {
+        get => _underlyingWindow.Title;
+        set => _underlyingWindow.Title = value;
+    }
+
+    public void Show() => _underlyingWindow.Show();
+    public void Close() => _underlyingWindow.Close();
+
+    public bool? ShowDialog() => _underlyingWindow.ShowDialog();
+    public double Width
+    {
+        get => _underlyingWindow.Width;
+        set => _underlyingWindow.Width = value;
+    }
+    public double Height
+    {
+        get => _underlyingWindow.Height;
+        set => _underlyingWindow.Height = value;
+    }
+}

+ 24 - 0
src/PixiEditor.Platform.MSStore/MSAdditionalContentProvider.cs

@@ -0,0 +1,24 @@
+namespace PixiEditor.Platform.MSStore;
+
+public sealed class MSAdditionalContentProvider : IAdditionalContentProvider
+{
+    public bool IsContentInstalled(AdditionalContentProduct product)
+    {
+        if(!PlatformHasContent(product)) return false;
+
+        return product switch
+        {
+            AdditionalContentProduct.SupporterPack => false,
+            _ => false
+        };
+    }
+
+    public bool PlatformHasContent(AdditionalContentProduct product)
+    {
+        return product switch
+        {
+            AdditionalContentProduct.SupporterPack => false,
+            _ => false
+        };
+    }
+}

+ 14 - 0
src/PixiEditor.Platform.MSStore/MicrosoftStorePlatform.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Platform.MSStore;
+
+public sealed class MicrosoftStorePlatform : IPlatform
+{
+    public string Id { get; } = "ms-store";
+    public string Name => "Microsoft Store";
+
+    public bool PerformHandshake()
+    {
+        return true;
+    }
+
+    public IAdditionalContentProvider? AdditionalContentProvider { get; } = new MSAdditionalContentProvider();
+}

+ 13 - 0
src/PixiEditor.Platform.MSStore/PixiEditor.Platform.MSStore.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Platform\PixiEditor.Platform.csproj" />
+    </ItemGroup>
+
+</Project>

+ 13 - 0
src/PixiEditor.Platform.Standalone/PixiEditor.Platform.Standalone.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Platform\PixiEditor.Platform.csproj" />
+    </ItemGroup>
+
+</Project>

+ 15 - 0
src/PixiEditor.Platform.Standalone/StandaloneAdditionalContentProvider.cs

@@ -0,0 +1,15 @@
+namespace PixiEditor.Platform.Standalone;
+
+public sealed class StandaloneAdditionalContentProvider : IAdditionalContentProvider
+{
+    public bool IsContentInstalled(AdditionalContentProduct product)
+    {
+        //if(!PlatformHasContent(product)) return false;
+        return false;
+    }
+
+    public bool PlatformHasContent(AdditionalContentProduct product)
+    {
+        return false;
+    }
+}

+ 14 - 0
src/PixiEditor.Platform.Standalone/StandalonePlatform.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Platform.Standalone;
+
+public sealed class StandalonePlatform : IPlatform
+{
+    public string Id { get; } = "standalone";
+    public string Name => "Standalone";
+
+    public bool PerformHandshake()
+    {
+        return true;
+    }
+
+    public IAdditionalContentProvider? AdditionalContentProvider { get; } = new StandaloneAdditionalContentProvider();
+}

+ 38 - 0
src/PixiEditor.Platform.Steam/PixiEditor.Platform.Steam.csproj

@@ -0,0 +1,38 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Platform\PixiEditor.Platform.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Steamworks.NET" Version="20.1.0" />
+    </ItemGroup>
+
+  <ItemGroup Condition="'$(Platform)' == 'x86'">
+    <None Update="steam_api.dll">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(Platform)' == 'AnyCPU'">
+    <None Update="steam_api.dll">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+    <None Update="steam_api64.dll">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(Platform)' == 'x64'">
+    <None Update="steam_api64.dll">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 24 - 0
src/PixiEditor.Platform.Steam/SteamAdditionalContentProvider.cs

@@ -0,0 +1,24 @@
+using Steamworks;
+
+namespace PixiEditor.Platform.Steam;
+
+public sealed class SteamAdditionalContentProvider : IAdditionalContentProvider
+{
+    private Dictionary<AdditionalContentProduct, AppId_t> productIds = new()
+    {
+        { AdditionalContentProduct.SupporterPack, new AppId_t(2435860) }
+    };
+
+    public bool IsContentInstalled(AdditionalContentProduct product)
+    {
+        if(!PlatformHasContent(product)) return false;
+
+        AppId_t appId = productIds[product];
+        return SteamApps.BIsDlcInstalled(appId);
+    }
+
+    public bool PlatformHasContent(AdditionalContentProduct product)
+    {
+        return productIds.ContainsKey(product);
+    }
+}

+ 24 - 0
src/PixiEditor.Platform.Steam/SteamPlatform.cs

@@ -0,0 +1,24 @@
+using Steamworks;
+
+namespace PixiEditor.Platform.Steam;
+
+public class SteamPlatform : IPlatform
+{
+    public string Id { get; } = "steam";
+    public string Name => "Steam";
+
+    public bool PerformHandshake()
+    {
+        try
+        {
+            SteamAPI.Init();
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+
+    public IAdditionalContentProvider? AdditionalContentProvider { get; } = new SteamAdditionalContentProvider();
+}

BIN
src/PixiEditor.Platform.Steam/steam_api.dll


BIN
src/PixiEditor.Platform.Steam/steam_api64.dll


+ 12 - 0
src/PixiEditor.Platform/IAdditionalContentProvider.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.Platform;
+
+public enum AdditionalContentProduct
+{
+    SupporterPack
+}
+
+public interface IAdditionalContentProvider
+{
+    public bool IsContentInstalled(AdditionalContentProduct product);
+    public bool PlatformHasContent(AdditionalContentProduct product);
+}

+ 20 - 0
src/PixiEditor.Platform/IPlatform.cs

@@ -0,0 +1,20 @@
+namespace PixiEditor.Platform;
+
+public interface IPlatform
+{
+    public static IPlatform Current { get; private set; }
+    public abstract string Id { get; }
+    public abstract string Name { get; }
+    public bool PerformHandshake();
+    public IAdditionalContentProvider? AdditionalContentProvider { get; }
+
+    public static void RegisterPlatform(IPlatform platform)
+    {
+        if (Current != null)
+        {
+            throw new InvalidOperationException("Platform already initialized.");
+        }
+
+        Current = platform;
+    }
+}

+ 13 - 0
src/PixiEditor.Platform/PixiEditor.Platform.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+    </ItemGroup>
+
+</Project>

+ 19 - 0
src/PixiEditor.Platform/PlatformServiceCollection.cs

@@ -0,0 +1,19 @@
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace PixiEditor.Platform;
+
+public static class PlatformServiceCollection
+{
+    public static IServiceCollection AddPlatform(this IServiceCollection services)
+    {
+        if(IPlatform.Current == null)
+            throw new InvalidOperationException("No platform was found");
+
+        services.AddSingleton(IPlatform.Current);
+
+        if (IPlatform.Current.AdditionalContentProvider != null)
+            services.AddSingleton(IPlatform.Current.AdditionalContentProvider);
+        return services;
+    }
+}

+ 261 - 0
src/PixiEditor.sln

@@ -40,6 +40,28 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorGen", "PixiEditor
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Builder", "PixiEditor.Builder\build\PixiEditor.Builder.csproj", "{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Builder", "PixiEditor.Builder\build\PixiEditor.Builder.csproj", "{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Platform.Steam", "PixiEditor.Platform.Steam\PixiEditor.Platform.Steam.csproj", "{9BCD0764-9C16-4A2A-B153-C676FEF38887}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Platform", "PixiEditor.Platform\PixiEditor.Platform.csproj", "{2BDEB8C6-F22D-43EA-A309-B3387A803689}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Platform.MSStore", "PixiEditor.Platform.MSStore\PixiEditor.Platform.MSStore.csproj", "{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platforms", "Platforms", "{9A81B795-66AB-4743-9284-90565941343D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{1E816135-76C1-4255-BE3C-BF17895A65AA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Deployment", "Deployment", "{68C3DA2D-D2EA-426E-A866-0019E425C816}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{5AFBF881-C054-4CE4-8159-8D4017FFD27A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Platform.Standalone", "PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj", "{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions", "PixiEditor.Extensions\PixiEditor.Extensions.csproj", "{1249EE2B-EB0D-411C-B311-53A7A22B7743}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{E4FF4CE6-5831-450D-8006-0539353C030B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleExtension", "SampleExtension\SampleExtension.csproj", "{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -620,6 +642,222 @@ Global
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x64.Build.0 = Debug|x64
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x64.Build.0 = Debug|x64
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x86.ActiveCfg = Debug|x86
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x86.ActiveCfg = Debug|x86
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x86.Build.0 = Debug|x86
 		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x86.Build.0 = Debug|x86
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x64.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x86.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX|x64.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.MSIX|x86.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|x64.ActiveCfg = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|x64.Build.0 = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|x86.ActiveCfg = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|x86.Build.0 = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|x64.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|x86.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x64.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x86.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX|x64.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.MSIX|x86.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|x64.ActiveCfg = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|x64.Build.0 = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|x86.ActiveCfg = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|x86.Build.0 = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|x64.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|x86.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x64.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x86.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX|x64.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.MSIX|x86.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Release|x64.ActiveCfg = Release|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Release|x64.Build.0 = Release|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Release|x86.ActiveCfg = Release|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Release|x86.Build.0 = Release|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|x64.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|x86.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|x64.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|x86.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX|x64.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.MSIX|x86.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|x64.ActiveCfg = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|x64.Build.0 = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|x86.ActiveCfg = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|x86.Build.0 = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|x64.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|x86.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|x64.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|x86.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX|x64.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.MSIX|x86.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|x64.ActiveCfg = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|x64.Build.0 = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|x86.ActiveCfg = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|x86.Build.0 = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|x64.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|x86.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Debug|x64.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Debug|x86.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX|x64.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.MSIX|x86.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Release|x64.ActiveCfg = Release|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Release|x64.Build.0 = Release|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Release|x86.ActiveCfg = Release|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Release|x86.Build.0 = Release|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Steam|x64.Build.0 = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814}.Steam|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -627,4 +865,27 @@ Global
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}
 	EndGlobalSection
 	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689} = {9A81B795-66AB-4743-9284-90565941343D}
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6} = {9A81B795-66AB-4743-9284-90565941343D}
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887} = {9A81B795-66AB-4743-9284-90565941343D}
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{294FD171-9536-474C-A679-83F0266275FB} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{758DF7DF-A8B1-4409-B79A-018E542B7251} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{98040E8A-F08E-45F8-956F-6480C8272049} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6} = {5AFBF881-C054-4CE4-8159-8D4017FFD27A}
+		{510ED47C-2455-4DCE-A561-1074725E1236} = {5AFBF881-C054-4CE4-8159-8D4017FFD27A}
+		{5193C1C1-8362-40FD-802B-E097E8C88082} = {5AFBF881-C054-4CE4-8159-8D4017FFD27A}
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE} = {9A81B795-66AB-4743-9284-90565941343D}
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{CE1C8DC9-E26B-4BBB-AB87-34054DE34814} = {E4FF4CE6-5831-450D-8006-0539353C030B}
+	EndGlobalSection
 EndGlobal
 EndGlobal

+ 28 - 3
src/PixiEditor/App.xaml.cs

@@ -2,12 +2,14 @@
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.AppExtensions;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Helpers.UI;
-using PixiEditor.Localization;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.Platform;
 using PixiEditor.Views;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
@@ -50,11 +52,34 @@ internal partial class App : Application
         #endif
         #endif
 
 
         AddNativeAssets();
         AddNativeAssets();
-        
-        MainWindow = new MainWindow();
+
+        InitPlatform();
+
+        ExtensionLoader extensionLoader = new ExtensionLoader();
+        extensionLoader.LoadExtensions();
+
+        MainWindow = new MainWindow(extensionLoader);
         MainWindow.Show();
         MainWindow.Show();
     }
     }
 
 
+    private void InitPlatform()
+    {
+        var platform = GetActivePlatform();
+        IPlatform.RegisterPlatform(platform);
+        platform.PerformHandshake();
+    }
+
+    private IPlatform GetActivePlatform()
+    {
+#if STEAM
+        return new PixiEditor.Platform.Steam.SteamPlatform();
+#elif MSIX || MSIX_DEBUG
+        return new PixiEditor.Platform.MSStore.MicrosoftStorePlatform();
+#else
+        return new PixiEditor.Platform.Standalone.StandalonePlatform();
+#endif
+    }
+
     private void AddNativeAssets()
     private void AddNativeAssets()
     {
     {
         var iconFont = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)
         var iconFont = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)

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

@@ -562,6 +562,23 @@
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally. Drag outside handles to rotate.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally. Drag outside handles to rotate.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_NOROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_NOROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally.",
+
+  "LOCAL_PALETTE_SOURCE_NAME": "Local",
+
+  "ERROR_FORBIDDEN_UNIQUE_NAME": "Extension unique name cannot start with 'pixieditor'.",
+  "ERROR_MISSING_METADATA": "Extension metadata key '{0}' is missing.",
+  "ERROR_NO_CLASS_ENTRY": "Extension class entry is missing on path '{0}'.",
+  "ERROR_NO_ENTRY_ASSEMBLY": "Extension entry assembly is missing on path '{0}'.",
+  "ERROR_MISSING_ADDITIONAL_CONTENT": "Your current setup doesn't allow loading this extension. Perhaps you don't own it or don't have it installed. You can purchase it here '{0}'.",
+
+  "BUY_NOW": "Buy Now",
+  "BUY_SUPPORTER_PACK": "Buy Supporter Pack",
+  "PALETTE_BROWSER_BUY_TEXT": "Buy Supporter Pack and get 21 beautiful palettes!",
+
+  "NEWS": "News",
+  "DISABLE_NEWS_PANEL": "Disable News panel in startup window",
+  "FAILED_FETCH_NEWS": "Failed to fetch news",
+  
   "CROP_TO_SELECTION": "Crop to selection",
   "CROP_TO_SELECTION": "Crop to selection",
   "CROP_TO_SELECTION_DESCRIPTIVE": "Crop image to selection",
   "CROP_TO_SELECTION_DESCRIPTIVE": "Crop image to selection",
   "SHOW_CONTEXT_MENU": "Show context menu",
   "SHOW_CONTEXT_MENU": "Show context menu",
@@ -570,4 +587,4 @@
   "RIGHT_CLICK_MODE": "Right click mode",
   "RIGHT_CLICK_MODE": "Right click mode",
   "ADD_PRIMARY_COLOR_TO_PALETTE": "Add primary color to palette",
   "ADD_PRIMARY_COLOR_TO_PALETTE": "Add primary color to palette",
   "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette"
   "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette"
-}
+}

+ 1 - 1
src/PixiEditor/Data/Localization/LocalizationDataSchema.json

@@ -18,7 +18,7 @@
           },
           },
           "localeFileName": {
           "localeFileName": {
             "type": "string",
             "type": "string",
-            "description": "The name of the key-value json file found in Data/Localization/Languages",
+            "description": "The name of the key-value json file found in Data/Localization/Languages. Must be prepended with extension unique name and : (e.g. pixieditor.sampleExtension:en.json)",
             "pattern": ".*\\.json",
             "pattern": ".*\\.json",
             "format": "uri",
             "format": "uri",
             "default": ".json"
             "default": ".json"

+ 2 - 1
src/PixiEditor/Exceptions/CorruptedFileException.cs

@@ -1,6 +1,7 @@
 using System.IO;
 using System.IO;
 using System.Runtime.Serialization;
 using System.Runtime.Serialization;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Exceptions;
 namespace PixiEditor.Exceptions;
 
 

+ 2 - 1
src/PixiEditor/Exceptions/InvalidFileTypeException.cs

@@ -1,5 +1,6 @@
 using System.Runtime.Serialization;
 using System.Runtime.Serialization;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Exceptions;
 namespace PixiEditor.Exceptions;
 
 

+ 2 - 1
src/PixiEditor/Exceptions/MissingFileException.cs

@@ -1,5 +1,6 @@
 using System.Runtime.Serialization;
 using System.Runtime.Serialization;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Exceptions;
 namespace PixiEditor.Exceptions;
 
 

+ 2 - 1
src/PixiEditor/Exceptions/RecoverableException.cs

@@ -1,5 +1,6 @@
 using System.Runtime.Serialization;
 using System.Runtime.Serialization;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Exceptions;
 namespace PixiEditor.Exceptions;
 
 

+ 2 - 1
src/PixiEditor/Helpers/Collections/ActionDisplayList.cs

@@ -1,5 +1,6 @@
 using System.Collections;
 using System.Collections;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Helpers.Collections;
 namespace PixiEditor.Helpers.Collections;
 
 

+ 2 - 1
src/PixiEditor/Helpers/Converters/BlendModeToStringConverter.cs

@@ -1,7 +1,8 @@
 using System.Globalization;
 using System.Globalization;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
-using PixiEditor.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 internal class BlendModeToStringConverter : SingleInstanceConverter<BlendModeToStringConverter>
 internal class BlendModeToStringConverter : SingleInstanceConverter<BlendModeToStringConverter>

+ 2 - 1
src/PixiEditor/Helpers/Converters/BoolToValueConverter.cs

@@ -1,5 +1,6 @@
 using System.Globalization;
 using System.Globalization;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 
 

+ 64 - 0
src/PixiEditor/Helpers/Converters/DefaultUnknownEnumConverter.cs

@@ -0,0 +1,64 @@
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace PixiEditor.Helpers.Converters;
+
+public class DefaultUnknownEnumConverter : StringEnumConverter
+{
+    /// <summary>
+    /// The default value used to fallback on when a enum is not convertable.
+    /// </summary>
+    private readonly int defaultValue;
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Default constructor. Defaults the default value to 0.
+    /// </summary>
+    public DefaultUnknownEnumConverter()
+    {
+
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Sets the default value for the enum value.
+    /// </summary>
+    /// <param name="defaultValue">The default value to use.</param>
+    public DefaultUnknownEnumConverter(int defaultValue)
+    {
+        this.defaultValue = defaultValue;
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Reads the provided JSON and attempts to convert using StringEnumConverter. If that fails set the value to the default value.
+    /// </summary>
+    /// <param name="reader">Reads the JSON value.</param>
+    /// <param name="objectType">Current type that is being converted.</param>
+    /// <param name="existingValue">The existing value being read.</param>
+    /// <param name="serializer">Instance of the JSON Serializer.</param>
+    /// <returns>The deserialized value of the enum if it exists or the default value if it does not.</returns>
+    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+    {
+        try
+        {
+            return base.ReadJson(reader, objectType, existingValue, serializer);
+        }
+        catch
+        {
+            return Enum.Parse(objectType, $"{defaultValue}");
+        }
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Validates that this converter can handle the type that is being provided.
+    /// </summary>
+    /// <param name="objectType">The type of the object being converted.</param>
+    /// <returns>True if the base class says so, and if the value is an enum and has a default value to fall on.</returns>
+    public override bool CanConvert(Type objectType)
+    {
+        return base.CanConvert(objectType) && objectType.GetTypeInfo().IsEnum && Enum.IsDefined(objectType, defaultValue);
+    }
+}

+ 0 - 1
src/PixiEditor/Helpers/Converters/EnumToStringConverter.cs

@@ -1,6 +1,5 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using System;
 using System;
-using PixiEditor.Localization;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 
 

+ 11 - 3
src/PixiEditor/Helpers/Converters/BackendColorToMediaColorConverter.cs → src/PixiEditor/Helpers/Converters/GenericColorToMediaColorConverter.cs

@@ -1,15 +1,23 @@
 using System.Globalization;
 using System.Globalization;
 using System.Windows.Media;
 using System.Windows.Media;
+using PixiEditor.Extensions.Palettes;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 
 
-internal class BackendColorToMediaColorConverter : SingleInstanceConverter<BackendColorToMediaColorConverter>
+internal class GenericColorToMediaColorConverter : SingleInstanceConverter<GenericColorToMediaColorConverter>
 {
 {
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
     {
-        var backendColor = (BackendColor)value;
-        var color = Color.FromArgb(backendColor.A, backendColor.R, backendColor.G, backendColor.B);
+        Color color = default;
+        if (value is BackendColor backendColor)
+        {
+            color = backendColor.ToColor();
+        }
+        else if (value is PaletteColor paletteColor)
+        {
+            color = paletteColor.ToMediaColor();
+        }
 
 
         if (targetType == typeof(Brush))
         if (targetType == typeof(Brush))
         {
         {

+ 2 - 1
src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -1,6 +1,7 @@
 using System.Globalization;
 using System.Globalization;
 using System.Windows.Input;
 using System.Windows.Input;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 
 

+ 2 - 1
src/PixiEditor/Helpers/Converters/LangConverter.cs

@@ -1,5 +1,6 @@
 using System.Globalization;
 using System.Globalization;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 
 

+ 7 - 6
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -5,6 +5,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Extensions.Palettes;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 
@@ -15,8 +16,8 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
     public int Width { get; set; }
     public int Width { get; set; }
     public int Height { get; set; }
     public int Height { get; set; }
     
     
-    public List<Color> Swatches { get; set; } = new List<Color>();
-    public List<Color> Palette { get; set; } = new List<Color>();
+    public List<PaletteColor> Swatches { get; set; } = new List<PaletteColor>();
+    public List<PaletteColor> Palette { get; set; } = new List<PaletteColor>();
     
     
     public ReferenceLayerBuilder ReferenceLayer { get; set; }
     public ReferenceLayerBuilder ReferenceLayer { get; set; }
 
 
@@ -30,22 +31,22 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
 
 
     public DocumentViewModelBuilder WithSize(VecI size) => WithSize(size.X, size.Y);
     public DocumentViewModelBuilder WithSize(VecI size) => WithSize(size.X, size.Y);
     
     
-    public DocumentViewModelBuilder WithSwatches(IEnumerable<Color> swatches)
+    public DocumentViewModelBuilder WithSwatches(IEnumerable<PaletteColor> swatches)
     {
     {
         Swatches = new (swatches);
         Swatches = new (swatches);
         return this;
         return this;
     }
     }
 
 
-    public DocumentViewModelBuilder WithSwatches<T>(IEnumerable<T> swatches, Func<T, Color> toColor) =>
+    public DocumentViewModelBuilder WithSwatches<T>(IEnumerable<T> swatches, Func<T, PaletteColor> toColor) =>
         WithSwatches(swatches.Select(toColor));
         WithSwatches(swatches.Select(toColor));
     
     
-    public DocumentViewModelBuilder WithPalette(IEnumerable<Color> palette)
+    public DocumentViewModelBuilder WithPalette(IEnumerable<PaletteColor> palette)
     {
     {
         Palette = new(palette);
         Palette = new(palette);
         return this;
         return this;
     }
     }
 
 
-    public DocumentViewModelBuilder WithPalette<T>(IEnumerable<T> pallet, Func<T, Color> toColor) =>
+    public DocumentViewModelBuilder WithPalette<T>(IEnumerable<T> pallet, Func<T, PaletteColor> toColor) =>
         WithPalette(pallet.Select(toColor));
         WithPalette(pallet.Select(toColor));
 
 
     public DocumentViewModelBuilder WithReferenceLayer<T>(T reference, Action<T, ReferenceLayerBuilder> builder)
     public DocumentViewModelBuilder WithReferenceLayer<T>(T reference, Action<T, ReferenceLayerBuilder> builder)

+ 0 - 1
src/PixiEditor/Helpers/Extensions/BlendModeEx.cs

@@ -1,5 +1,4 @@
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
-using PixiEditor.Localization;
 
 
 namespace PixiEditor.Helpers.Extensions;
 namespace PixiEditor.Helpers.Extensions;
 internal static class BlendModeEx
 internal static class BlendModeEx

+ 6 - 0
src/PixiEditor/Helpers/Extensions/ColorHelpers.cs

@@ -1,4 +1,5 @@
 using System.Windows.Media;
 using System.Windows.Media;
+using PixiEditor.Extensions.Palettes;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 
 
 namespace PixiEditor.Helpers.Extensions;
 namespace PixiEditor.Helpers.Extensions;
@@ -7,9 +8,14 @@ internal static class ColorHelpers
 {
 {
     public static BackendColor ToOpaqueColor(this Color color) => new(color.R, color.G, color.B);
     public static BackendColor ToOpaqueColor(this Color color) => new(color.R, color.G, color.B);
     public static BackendColor ToColor(this Color color) => new(color.R, color.G, color.B, color.A);
     public static BackendColor ToColor(this Color color) => new(color.R, color.G, color.B, color.A);
+    public static BackendColor ToColor(this PaletteColor color) => new(color.R, color.G, color.B, 255);
+
+    public static PaletteColor ToPaletteColor(this Color color) => new(color.R, color.G, color.B);
+    public static PaletteColor ToPaletteColor(this BackendColor color) => new(color.R, color.G, color.B);
 
 
     public static Color ToOpaqueMediaColor(this BackendColor color) => Color.FromRgb(color.R, color.G, color.B);
     public static Color ToOpaqueMediaColor(this BackendColor color) => Color.FromRgb(color.R, color.G, color.B);
     public static Color ToColor(this BackendColor color) => Color.FromArgb(color.A, color.R, color.G, color.B);
     public static Color ToColor(this BackendColor color) => Color.FromArgb(color.A, color.R, color.G, color.B);
+    public static Color ToMediaColor(this PaletteColor color) => Color.FromRgb(color.R, color.G, color.B);
     
     
     public static BackendColor BlendColors(BackendColor bottomColor, BackendColor topColor)
     public static BackendColor BlendColors(BackendColor bottomColor, BackendColor topColor)
     {
     {

+ 3 - 2
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -1,6 +1,7 @@
 using ChunkyImageLib;
 using ChunkyImageLib;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.Palettes;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -19,8 +20,8 @@ internal static class PixiParserDocumentEx
         return DocumentViewModel.Build(b =>
         return DocumentViewModel.Build(b =>
         {
         {
             b.WithSize(document.Width, document.Height)
             b.WithSize(document.Width, document.Height)
-                .WithPalette(document.Palette, x => new Color(x.R, x.G, x.B, x.A))
-                .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B, x.A))
+                .WithPalette(document.Palette, x => new PaletteColor(x.R, x.G, x.B))
+                .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B))
                 .WithReferenceLayer(document.ReferenceLayer, (r, builder) => builder
                 .WithReferenceLayer(document.ReferenceLayer, (r, builder) => builder
                     .WithIsVisible(r.Enabled)
                     .WithIsVisible(r.Enabled)
                     .WithShape(r.Corners)
                     .WithShape(r.Corners)

+ 3 - 2
src/PixiEditor/Helpers/Extensions/SerializableDocumentEx.cs

@@ -2,6 +2,7 @@
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Extensions.Palettes;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Collections.Deprecated;
 using PixiEditor.Parser.Collections.Deprecated;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.Parser.Deprecated;
@@ -32,8 +33,8 @@ internal static class SerializableDocumentEx
         {
         {
             builder
             builder
                 .WithSize(serializableDocument.Width, serializableDocument.Height)
                 .WithSize(serializableDocument.Width, serializableDocument.Height)
-                .WithPalette(serializableDocument.Palette.Select(x => new Color(x.R, x.G, x.B, x.A)).ToList())
-                .WithSwatches(serializableDocument.Swatches.Select(x => new Color(x.R, x.G, x.B, x.A)).ToList());
+                .WithPalette(serializableDocument.Palette.Select(x => new PaletteColor(x.R, x.G, x.B)).ToList())
+                .WithSwatches(serializableDocument.Swatches.Select(x => new PaletteColor(x.R, x.G, x.B)).ToList());
 
 
             if (serializableDocument.Groups != null)
             if (serializableDocument.Groups != null)
             {
             {

+ 19 - 4
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -1,13 +1,22 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
-using PixiEditor.Localization;
+using PixiEditor.Extensions;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Extensions.Palettes.Parsers;
+using PixiEditor.Extensions.Windowing;
+using PixiEditor.Models.AppExtensions;
+using PixiEditor.Models.AppExtensions.Services;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataProviders;
 using PixiEditor.Models.DataProviders;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO.PaletteParsers;
 using PixiEditor.Models.IO.PaletteParsers;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
-using PixiEditor.Models.UserPreferences;
+using PixiEditor.Models.Localization;
+using PixiEditor.Models.Preferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
+using PixiEditor.ViewModels.SubViewModels.AdditionalContent;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Main;
 using PixiEditor.ViewModels.SubViewModels.Main;
 using PixiEditor.ViewModels.SubViewModels.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools;
@@ -20,10 +29,10 @@ internal static class ServiceCollectionHelpers
     /// <summary>
     /// <summary>
     /// Adds all the services required to fully run PixiEditor's MainWindow
     /// Adds all the services required to fully run PixiEditor's MainWindow
     /// </summary>
     /// </summary>
-    public static IServiceCollection AddPixiEditor(this IServiceCollection collection) => collection
+    public static IServiceCollection AddPixiEditor(this IServiceCollection collection, ExtensionLoader extensionLoader) => collection
         .AddSingleton<ViewModelMain>()
         .AddSingleton<ViewModelMain>()
         .AddSingleton<IPreferences, PreferencesSettings>()
         .AddSingleton<IPreferences, PreferencesSettings>()
-        .AddSingleton<ILocalizationProvider, LocalizationProvider>()
+        .AddSingleton<ILocalizationProvider, LocalizationProvider>(x => new LocalizationProvider(extensionLoader))
         // View Models
         // View Models
         .AddSingleton<StylusViewModel>()
         .AddSingleton<StylusViewModel>()
         .AddSingleton<WindowViewModel>()
         .AddSingleton<WindowViewModel>()
@@ -41,6 +50,8 @@ internal static class ServiceCollectionHelpers
         .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
         .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
         .AddSingleton<DebugViewModel>()
         .AddSingleton<DebugViewModel>()
         .AddSingleton<SearchViewModel>()
         .AddSingleton<SearchViewModel>()
+        .AddSingleton<AdditionalContentViewModel>()
+        .AddSingleton(x => new ExtensionsViewModel(x.GetService<ViewModelMain>(), extensionLoader))
         // Controllers
         // Controllers
         .AddSingleton<ShortcutController>()
         .AddSingleton<ShortcutController>()
         .AddSingleton<CommandController>()
         .AddSingleton<CommandController>()
@@ -71,4 +82,8 @@ internal static class ServiceCollectionHelpers
         .AddSingleton<PaletteFileParser, PixiPaletteParser>()
         .AddSingleton<PaletteFileParser, PixiPaletteParser>()
         // Palette data sources
         // Palette data sources
         .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>();
         .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>();
+
+    public static IServiceCollection AddExtensionServices(this IServiceCollection collection) =>
+        collection.AddSingleton<IWindowProvider, WindowProvider>()
+            .AddSingleton<IPaletteProvider, PaletteProvider>();
 }
 }

+ 12 - 0
src/PixiEditor/Helpers/Extensions/WindowExtensions.cs

@@ -0,0 +1,12 @@
+using System.Reflection;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Extensions;
+
+public static class WindowExtensions
+{
+    public static bool IsModal(this Window window)
+    {
+        return (bool)typeof(Window).GetField("_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window);
+    }
+}

+ 2 - 1
src/PixiEditor/Helpers/PaletteHelpers.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.IO;
+using PixiEditor.Extensions.Palettes.Parsers;
+using PixiEditor.Models.IO;
 
 
 namespace PixiEditor.Helpers;
 namespace PixiEditor.Helpers;
 
 

+ 2 - 1
src/PixiEditor/Helpers/RegistryHelpers.cs

@@ -2,8 +2,9 @@
 using System.Security.AccessControl;
 using System.Security.AccessControl;
 using System.Windows;
 using System.Windows;
 using Microsoft.Win32;
 using Microsoft.Win32;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Helpers;
 namespace PixiEditor.Helpers;
 
 

BIN
src/PixiEditor/Images/Chevron-right.png


BIN
src/PixiEditor/Images/News/Article.png


BIN
src/PixiEditor/Images/News/Misc.png


BIN
src/PixiEditor/Images/News/NewVersion.png


BIN
src/PixiEditor/Images/News/OfficialAnnouncement.png


BIN
src/PixiEditor/Images/News/YouTube.png


+ 0 - 9
src/PixiEditor/Localization/LocalizationData.cs

@@ -1,9 +0,0 @@
-using System.Diagnostics;
-
-namespace PixiEditor.Localization;
-
-[DebuggerDisplay("{Languages.Count} Language(s)")]
-public class LocalizationData
-{
-    public List<LanguageData> Languages { get; set; }
-}

+ 48 - 0
src/PixiEditor/Models/AppExtensions/ExtensionException.cs

@@ -0,0 +1,48 @@
+using PixiEditor.Exceptions;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
+
+namespace PixiEditor.Models.AppExtensions;
+
+public class ExtensionException : RecoverableException
+{
+    public ExtensionException(LocalizedString messageKey) : base(messageKey)
+    {
+    }
+}
+
+public class NoEntryAssemblyException : ExtensionException
+{
+    public NoEntryAssemblyException(string containingFolder) : base(new LocalizedString("ERROR_NO_ENTRY_ASSEMBLY", containingFolder))
+    {
+    }
+}
+
+public class NoClassEntryException : ExtensionException
+{
+    public NoClassEntryException(string assemblyPath) : base(new LocalizedString("ERROR_NO_CLASS_ENTRY", assemblyPath))
+    {
+    }
+}
+
+public class MissingMetadataException : ExtensionException
+{
+    public MissingMetadataException(string missingMetadataKey) : base(new LocalizedString("ERROR_MISSING_METADATA", missingMetadataKey))
+    {
+    }
+}
+
+public class ForbiddenUniqueNameExtension : ExtensionException
+{
+    public ForbiddenUniqueNameExtension() : base(new LocalizedString("ERROR_FORBIDDEN_UNIQUE_NAME"))
+    {
+    }
+}
+
+public class MissingAdditionalContentException : ExtensionException
+{
+    public MissingAdditionalContentException(string productLink) : base(new LocalizedString("ERROR_MISSING_ADDITIONAL_CONTENT", productLink))
+    {
+    }
+}
+

+ 232 - 0
src/PixiEditor/Models/AppExtensions/ExtensionLoader.cs

@@ -0,0 +1,232 @@
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+using Newtonsoft.Json;
+using PixiEditor.Extensions;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Metadata;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO;
+using PixiEditor.Platform;
+
+namespace PixiEditor.Models.AppExtensions;
+
+internal class ExtensionLoader
+{
+    private readonly Dictionary<string, OfficialExtensionData> _officialExtensionsKeys = new Dictionary<string, OfficialExtensionData>();
+    public List<Extension> LoadedExtensions { get; } = new();
+
+    public ExtensionLoader()
+    {
+        ValidateExtensionFolder();
+        _officialExtensionsKeys.Add("pixieditor.supporterpack", new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
+    }
+
+    public void LoadExtensions()
+    {
+        var directories = Directory.GetDirectories(Paths.ExtensionsFullPath);
+        foreach (var directory in directories)
+        {
+            string packageJsonPath = Path.Combine(directory, "extension.json");
+            bool isExtension = File.Exists(packageJsonPath);
+            if (isExtension)
+            {
+                LoadExtension(packageJsonPath);
+            }
+        }
+    }
+
+    public void InitializeExtensions(ExtensionServices pixiEditorApi)
+    {
+        try
+        {
+            foreach (var extension in LoadedExtensions)
+            {
+                extension.Initialize(pixiEditorApi);
+            }
+        }
+        catch (Exception ex)
+        {
+            Task.Run(async () => await CrashHelper.SendExceptionInfoToWebhook(ex));
+        }
+    }
+
+    private void LoadExtension(string packageJsonPath)
+    {
+        string json = File.ReadAllText(packageJsonPath);
+        try
+        {
+            var metadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
+            string directory = Path.GetDirectoryName(packageJsonPath);
+            Assembly entry = GetEntryAssembly(directory, out Type extensionType);
+            if (entry is null)
+            {
+                throw new NoEntryAssemblyException(directory);
+            }
+
+            if (!ValidateMetadata(metadata, entry))
+            {
+                return;
+            }
+
+            var extension = LoadExtensionEntry(entry, extensionType, metadata);
+            extension.Load();
+            LoadedExtensions.Add(extension);
+        }
+        catch (JsonException)
+        {
+            //MessageBox.Show(new LocalizedString("ERROR_INVALID_PACKAGE", packageJsonPath), "ERROR");
+        }
+        catch (ExtensionException ex)
+        {
+            //MessageBox.Show(ex.DisplayMessage, "ERROR");
+        }
+        catch (Exception ex)
+        {
+            //MessageBox.Show(new LocalizedString("ERROR_LOADING_PACKAGE", packageJsonPath), "ERROR");
+            Task.Run(async () => await CrashHelper.SendExceptionInfoToWebhook(ex));
+        }
+    }
+
+    private Assembly? GetEntryAssembly(string assemblyFolder, out Type extensionType)
+    {
+        string[] dlls = Directory.GetFiles(assemblyFolder, "*.dll");
+        Assembly? entryAssembly = GetEntryAssembly(dlls, out extensionType);
+
+        return entryAssembly;
+    }
+
+    private bool ValidateMetadata(ExtensionMetadata metadata, Assembly assembly)
+    {
+        if (string.IsNullOrEmpty(metadata.UniqueName))
+        {
+            throw new MissingMetadataException("Description");
+        }
+
+        string fixedUniqueName = metadata.UniqueName.ToLower().Trim();
+
+        if (fixedUniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
+        {
+            if(!IsOfficialAssemblyLegit(fixedUniqueName, assembly))
+            {
+                throw new ForbiddenUniqueNameExtension();
+            }
+
+            if (!IsAdditionalContentInstalled(fixedUniqueName))
+            {
+                return false;
+            }
+        }
+        // TODO: Validate if unique name is unique
+
+        if (string.IsNullOrEmpty(metadata.DisplayName))
+        {
+            throw new MissingMetadataException("DisplayName");
+        }
+
+        if (string.IsNullOrEmpty(metadata.Version))
+        {
+            throw new MissingMetadataException("Version");
+        }
+
+        return true;
+    }
+
+    private bool IsAdditionalContentInstalled(string fixedUniqueName)
+    {
+        if (!_officialExtensionsKeys.ContainsKey(fixedUniqueName)) return false;
+        AdditionalContentProduct? product = _officialExtensionsKeys[fixedUniqueName].Product;
+
+        if (product == null) return true;
+
+        return IPlatform.Current.AdditionalContentProvider?.IsContentInstalled(product.Value) ?? false;
+    }
+
+    private bool IsOfficialAssemblyLegit(string metadataUniqueName, Assembly assembly)
+    {
+        if (assembly == null) return false; // All official extensions must have a valid assembly
+        if (!_officialExtensionsKeys.ContainsKey(metadataUniqueName)) return false;
+        bool wasVerified = false;
+        bool verified = StrongNameSignatureVerificationEx(assembly.Location, true, ref wasVerified);
+        if (!verified || !wasVerified) return false;
+
+        byte[]? assemblyPublicKey = assembly.GetName().GetPublicKey();
+        if (assemblyPublicKey == null) return false;
+
+        return PublicKeysMatch(assemblyPublicKey, _officialExtensionsKeys[metadataUniqueName].PublicKeyName);
+    }
+
+    private bool PublicKeysMatch(byte[] assemblyPublicKey, string pathToPublicKey)
+    {
+        Assembly currentAssembly = Assembly.GetExecutingAssembly();
+        using Stream? stream = currentAssembly.GetManifestResourceStream($"{currentAssembly.GetName().Name}.OfficialExtensions.{pathToPublicKey}");
+        if (stream == null) return false;
+
+        using MemoryStream memoryStream = new MemoryStream();
+        stream.CopyTo(memoryStream);
+        byte[] publicKey = memoryStream.ToArray();
+
+        return assemblyPublicKey.SequenceEqual(publicKey);
+    }
+
+    [DllImport("mscoree.dll", CharSet=CharSet.Unicode)]
+    static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool pfWasVerified);
+
+    private Extension LoadExtensionEntry(Assembly entryAssembly, Type extensionType, ExtensionMetadata metadata)
+    {
+        var extension = (Extension)Activator.CreateInstance(extensionType);
+        if (extension is null)
+        {
+            throw new NoClassEntryException(entryAssembly.Location);
+        }
+
+        extension.ProvideMetadata(metadata);
+        return extension;
+    }
+
+    private Assembly? GetEntryAssembly(string[] dlls, out Type extensionType)
+    {
+        foreach (var dll in dlls)
+        {
+            try
+            {
+                var assembly = Assembly.LoadFrom(dll);
+                extensionType = assembly.GetTypes().FirstOrDefault(x => x.IsSubclassOf(typeof(Extension)));
+                if (extensionType is not null)
+                {
+                    return assembly;
+                }
+            }
+            catch
+            {
+                // ignored
+            }
+        }
+
+        extensionType = null;
+        return null;
+    }
+
+    private void ValidateExtensionFolder()
+    {
+        if (!Directory.Exists(Paths.ExtensionsFullPath))
+        {
+            Directory.CreateDirectory(Paths.ExtensionsFullPath);
+        }
+    }
+}
+
+internal struct OfficialExtensionData
+{
+    public string PublicKeyName { get; }
+    public AdditionalContentProduct? Product { get; }
+    public string? PurchaseLink { get; }
+
+    public OfficialExtensionData(string publicKeyName, AdditionalContentProduct product, string? purchaseLink = null)
+    {
+        PublicKeyName = publicKeyName;
+        Product = product;
+    }
+}

+ 72 - 0
src/PixiEditor/Models/AppExtensions/Services/PaletteProvider.cs

@@ -0,0 +1,72 @@
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Extensions.Palettes.Parsers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.DataProviders;
+
+namespace PixiEditor.Models.AppExtensions.Services;
+
+internal sealed class PaletteProvider : IPaletteProvider
+{
+    public WpfObservableRangeCollection<PaletteFileParser> AvailableParsers { get; set; }
+    public WpfObservableRangeCollection<PaletteListDataSource> DataSources => dataSources;
+    private readonly WpfObservableRangeCollection<PaletteListDataSource> dataSources;
+
+    public PaletteProvider()
+    {
+        dataSources = new WpfObservableRangeCollection<PaletteListDataSource>();
+    }
+
+    public async Task<List<IPalette>> FetchPalettes(int startIndex, int items, FilteringSettings filtering)
+    {
+        List<IPalette> allPalettes = new();
+        foreach (PaletteListDataSource dataSource in dataSources)
+        {
+            var palettes = await dataSource.FetchPaletteList(startIndex, items, filtering);
+            allPalettes.AddRange(palettes);
+        }
+
+        return allPalettes;
+    }
+
+    public async Task<bool> AddPalette(IPalette palette, bool overwrite = false)
+    {
+        LocalPalettesFetcher localPalettesFetcher = dataSources.OfType<LocalPalettesFetcher>().FirstOrDefault();
+        if(localPalettesFetcher == null)
+        {
+            return false;
+        }
+
+        if (LocalPalettesFetcher.PaletteExists(palette.Name))
+        {
+            if (overwrite)
+            {
+                await localPalettesFetcher.DeletePalette(palette.Name);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+
+        string finalName = LocalPalettesFetcher.GetNonExistingName(palette.Name, true);
+
+        await localPalettesFetcher.SavePalette(
+            finalName,
+            palette.Colors.ToArray());
+
+        return true;
+    }
+
+    public void RegisterDataSource(PaletteListDataSource dataSource)
+    {
+        if(dataSources.Contains(dataSource)) return;
+
+        dataSources.Add(dataSource);
+
+        dataSource.AvailableParsers = AvailableParsers.ToList();
+        dataSource.Initialize();
+    }
+}

+ 46 - 0
src/PixiEditor/Models/AppExtensions/Services/WindowProvider.cs

@@ -0,0 +1,46 @@
+using System.Windows.Markup;
+using PixiEditor.Extensions;
+using PixiEditor.Extensions.Windowing;
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Models.AppExtensions.Services;
+
+public class WindowProvider : IWindowProvider
+{
+    private Dictionary<string, Func<IPopupWindow>> _openHandlers = new();
+
+    public WindowProvider RegisterHandler(string id, Func<IPopupWindow> handler)
+    {
+        if (_openHandlers.ContainsKey(id))
+        {
+            _openHandlers[id] = handler;
+            throw new ArgumentException($"Window with id {id} already has a handler");
+        }
+
+        _openHandlers.Add(id, handler);
+        return this;
+    }
+
+    public PopupWindow CreatePopupWindow(string title, object body)
+    {
+        return new PopupWindow(new BasicPopup { Title = title, Body = body });
+    }
+
+    public PopupWindow OpenWindow(WindowType type)
+    {
+        return OpenWindow($"PixiEditor.{type}");
+    }
+
+    public PopupWindow OpenWindow(string windowId)
+    {
+        var handler = _openHandlers.FirstOrDefault(x => x.Key == windowId);
+        if (handler.Key != null)
+        {
+            return new PopupWindow(handler.Value());
+        }
+        else
+        {
+            throw new ArgumentException($"Window with id {windowId} does not exist");
+        }
+    }
+}

+ 2 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using System.Windows.Input;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Models.Commands.Attributes.Commands;
 namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 

+ 2 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/FilterAttribute.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Models.Commands.Attributes.Commands;
 namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 

+ 2 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Models.Commands.Attributes.Commands;
 namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 

+ 2 - 1
src/PixiEditor/Models/Commands/CommandController.cs

@@ -3,11 +3,12 @@ using System.Reflection;
 using System.Windows.Media;
 using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Localization;
 using PixiEditor.ViewModels.SubViewModels.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools;
 using CommandAttribute = PixiEditor.Models.Commands.Attributes.Commands.Command;
 using CommandAttribute = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 

+ 2 - 1
src/PixiEditor/Models/Commands/CommandGroup.cs

@@ -1,9 +1,10 @@
 using System.Collections;
 using System.Collections;
 using System.ComponentModel;
 using System.ComponentModel;
 using System.Windows.Input;
 using System.Windows.Input;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Models.Commands;
 namespace PixiEditor.Models.Commands;
 
 

+ 2 - 1
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -1,9 +1,10 @@
 using System.Diagnostics;
 using System.Diagnostics;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Models.Commands.Commands;
 namespace PixiEditor.Models.Commands.Commands;
 
 

+ 3 - 1
src/PixiEditor/Models/Commands/Search/ColorSearchResult.cs

@@ -2,6 +2,7 @@
 using System.Windows.Controls;
 using System.Windows.Controls;
 using System.Windows.Documents;
 using System.Windows.Documents;
 using System.Windows.Media;
 using System.Windows.Media;
+using PixiEditor.Extensions.Palettes;
 
 
 namespace PixiEditor.Models.Commands.Search;
 namespace PixiEditor.Models.Commands.Search;
 
 
@@ -44,7 +45,8 @@ internal class ColorSearchResult : SearchResult
     public static ColorSearchResult PastePalette(DrawingApi.Core.ColorsImpl.Color color, string searchTerm = null)
     public static ColorSearchResult PastePalette(DrawingApi.Core.ColorsImpl.Color color, string searchTerm = null)
     {
     {
         //var result = new ColorSearchResult(color, x => ViewModelMain.Current.BitmapManager.ActiveDocument.Palette.Add(x))
         //var result = new ColorSearchResult(color, x => ViewModelMain.Current.BitmapManager.ActiveDocument.Palette.Add(x))
-        var result = new ColorSearchResult(color, x => ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument!.Palette.Add(x))
+        var result = new ColorSearchResult(color, x =>
+            ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument!.Palette.Add(new PaletteColor(x.R, x.G, x.B)))
         {
         {
             SearchTerm = searchTerm,
             SearchTerm = searchTerm,
             isPalettePaste = true
             isPalettePaste = true

+ 3 - 1
src/PixiEditor/Models/DataHolders/KeyCombination.cs

@@ -4,7 +4,9 @@ using System.Diagnostics;
 using System.Globalization;
 using System.Globalization;
 using System.Text;
 using System.Text;
 using System.Windows.Input;
 using System.Windows.Input;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Helpers;
+using PixiEditor.Models.Localization;
 
 
 namespace PixiEditor.Models.DataHolders;
 namespace PixiEditor.Models.DataHolders;
 
 

+ 7 - 3
src/PixiEditor/Models/DataHolders/Palettes/Palette.cs

@@ -1,9 +1,10 @@
 #nullable enable
 #nullable enable
 using System.IO;
 using System.IO;
+using PixiEditor.Extensions.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 namespace PixiEditor.Models.DataHolders.Palettes;
 namespace PixiEditor.Models.DataHolders.Palettes;
 
 
-internal class Palette : NotifyableObject
+internal class Palette : NotifyableObject, IPalette
 {
 {
     private string _name = "";
     private string _name = "";
 
 
@@ -12,7 +13,7 @@ internal class Palette : NotifyableObject
         get => _name;
         get => _name;
         set => SetProperty(ref _name, value);
         set => SetProperty(ref _name, value);
     }
     }
-    public List<string> Colors { get; set; }
+    public List<PaletteColor> Colors { get; set; }
 
 
     private string? fileName;
     private string? fileName;
 
 
@@ -28,11 +29,14 @@ internal class Palette : NotifyableObject
 
 
     public bool IsFavourite { get; set; }
     public bool IsFavourite { get; set; }
 
 
-    public Palette(string name, List<string> colors, string fileName)
+    public PaletteListDataSource Source { get; }
+
+    public Palette(string name, List<PaletteColor> colors, string? fileName, PaletteListDataSource source)
     {
     {
         Name = name;
         Name = name;
         Colors = colors;
         Colors = colors;
         FileName = fileName;
         FileName = fileName;
+        Source = source;
     }
     }
 
 
     public static string? ReplaceInvalidChars(string? filename)
     public static string? ReplaceInvalidChars(string? filename)

+ 2 - 1
src/PixiEditor/Models/DataHolders/Palettes/PaletteList.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Helpers;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Helpers;
 
 
 namespace PixiEditor.Models.DataHolders.Palettes;
 namespace PixiEditor.Models.DataHolders.Palettes;
 
 

+ 25 - 0
src/PixiEditor/Models/DataHolders/Palettes/PaletteObject.cs

@@ -0,0 +1,25 @@
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Extensions.Palettes;
+
+namespace PixiEditor.Models.DataHolders.Palettes;
+
+/// <summary>
+///     Class used to deserialize palette file from Lospec.
+/// </summary>
+internal class PaletteObject
+{
+    public string Name { get; set; }
+    public List<string> Colors { get; set; }
+
+    public Palette ToPalette()
+    {
+        List<PaletteColor> colors = new();
+        foreach (string color in Colors)
+        {
+            Color parsedColor = Color.Parse(color);
+            colors.Add(new PaletteColor(parsedColor.R, parsedColor.G, parsedColor.B));
+        }
+
+        return new(Name, colors, null, null);
+    }
+}

+ 30 - 30
src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs

@@ -1,10 +1,13 @@
 using System.IO;
 using System.IO;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Extensions.Palettes.Parsers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders.Palettes;
 using PixiEditor.Models.DataHolders.Palettes;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
-using PixiEditor.Models.UserPreferences;
 
 
 namespace PixiEditor.Models.DataProviders;
 namespace PixiEditor.Models.DataProviders;
 
 
@@ -12,10 +15,6 @@ internal delegate void CacheUpdate(RefreshType refreshType, Palette itemAffected
 
 
 internal class LocalPalettesFetcher : PaletteListDataSource
 internal class LocalPalettesFetcher : PaletteListDataSource
 {
 {
-    public static string PathToPalettesFolder { get; } = Path.Join(
-        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-        "PixiEditor", "Palettes");
-
     private List<Palette> cachedPalettes;
     private List<Palette> cachedPalettes;
 
 
     public event CacheUpdate CacheUpdated;
     public event CacheUpdate CacheUpdated;
@@ -24,10 +23,14 @@ internal class LocalPalettesFetcher : PaletteListDataSource
 
 
     private FileSystemWatcher watcher;
     private FileSystemWatcher watcher;
 
 
+    public LocalPalettesFetcher() : base("LOCAL_PALETTE_SOURCE_NAME")
+    {
+    }
+
     public override void Initialize()
     public override void Initialize()
     {
     {
         InitDir();
         InitDir();
-        watcher = new FileSystemWatcher(PathToPalettesFolder);
+        watcher = new FileSystemWatcher(Paths.PathToPalettesFolder);
         watcher.Filter = "*.pal";
         watcher.Filter = "*.pal";
         watcher.Changed += FileSystemChanged;
         watcher.Changed += FileSystemChanged;
         watcher.Deleted += FileSystemChanged;
         watcher.Deleted += FileSystemChanged;
@@ -40,22 +43,20 @@ internal class LocalPalettesFetcher : PaletteListDataSource
         IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, updated =>
         IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, updated =>
         {
         {
             cachedFavoritePalettes = (List<string>)updated;
             cachedFavoritePalettes = (List<string>)updated;
+            cachedPalettes.ForEach(x => x.IsFavourite = cachedFavoritePalettes.Contains(x.Name));
         });
         });
     }
     }
 
 
-    public override async Task<PaletteList> FetchPaletteList(int startIndex, int count, FilteringSettings filtering)
+    public override async Task<List<IPalette>> FetchPaletteList(int startIndex, int count, FilteringSettings filtering)
     {
     {
         if (cachedPalettes == null)
         if (cachedPalettes == null)
         {
         {
             await RefreshCacheAll();
             await RefreshCacheAll();
         }
         }
 
 
-        PaletteList result = new PaletteList
-        {
-            Palettes = new WpfObservableRangeCollection<Palette>()
-        };
+        var filteredPalettes = cachedPalettes.Where(filtering.Filter).OrderByDescending(x => x.IsFavourite).ToArray();
 
 
-        var filteredPalettes = cachedPalettes.Where(filtering.Filter).ToArray();
+        List<IPalette> result = new List<IPalette>();
 
 
         if (startIndex >= filteredPalettes.Length) return result;
         if (startIndex >= filteredPalettes.Length) return result;
 
 
@@ -63,10 +64,9 @@ internal class LocalPalettesFetcher : PaletteListDataSource
         {
         {
             if (startIndex + i >= filteredPalettes.Length) break;
             if (startIndex + i >= filteredPalettes.Length) break;
             Palette palette = filteredPalettes[startIndex + i];
             Palette palette = filteredPalettes[startIndex + i];
-            result.Palettes.Add(palette);
+            result.Add(palette);
         }
         }
 
 
-        result.FetchedCorrectly = true;
         return result;
         return result;
     }
     }
 
 
@@ -78,21 +78,21 @@ internal class LocalPalettesFetcher : PaletteListDataSource
             finalFileName += ".pal";
             finalFileName += ".pal";
         }
         }
 
 
-        return File.Exists(Path.Join(PathToPalettesFolder, finalFileName));
+        return File.Exists(Path.Join(Paths.PathToPalettesFolder, finalFileName));
     }
     }
 
 
     public static string GetNonExistingName(string currentName, bool appendExtension = false)
     public static string GetNonExistingName(string currentName, bool appendExtension = false)
     {
     {
         string newName = Path.GetFileNameWithoutExtension(currentName);
         string newName = Path.GetFileNameWithoutExtension(currentName);
 
 
-        if (File.Exists(Path.Join(PathToPalettesFolder, newName + ".pal")))
+        if (File.Exists(Path.Join(Paths.PathToPalettesFolder, newName + ".pal")))
         {
         {
             int number = 1;
             int number = 1;
             while (true)
             while (true)
             {
             {
                 string potentialName = $"{newName} ({number})";
                 string potentialName = $"{newName} ({number})";
                 number++;
                 number++;
-                if (File.Exists(Path.Join(PathToPalettesFolder, potentialName + ".pal")))
+                if (File.Exists(Path.Join(Paths.PathToPalettesFolder, potentialName + ".pal")))
                     continue;
                     continue;
                 newName = potentialName;
                 newName = potentialName;
                 break;
                 break;
@@ -105,10 +105,10 @@ internal class LocalPalettesFetcher : PaletteListDataSource
         return newName;
         return newName;
     }
     }
 
 
-    public async Task SavePalette(string fileName, Color[] colors)
+    public async Task SavePalette(string fileName, PaletteColor[] colors)
     {
     {
         watcher.EnableRaisingEvents = false;
         watcher.EnableRaisingEvents = false;
-        string path = Path.Join(PathToPalettesFolder, fileName);
+        string path = Path.Join(Paths.PathToPalettesFolder, fileName);
         InitDir();
         InitDir();
         await JascFileParser.SaveFile(path, new PaletteFileData(colors));
         await JascFileParser.SaveFile(path, new PaletteFileData(colors));
         watcher.EnableRaisingEvents = true;
         watcher.EnableRaisingEvents = true;
@@ -118,8 +118,8 @@ internal class LocalPalettesFetcher : PaletteListDataSource
 
 
     public async Task DeletePalette(string name)
     public async Task DeletePalette(string name)
     {
     {
-        if (!Directory.Exists(PathToPalettesFolder)) return;
-        string path = Path.Join(PathToPalettesFolder, name);
+        if (!Directory.Exists(Paths.PathToPalettesFolder)) return;
+        string path = Path.Join(Paths.PathToPalettesFolder, name);
         if (!File.Exists(path)) return;
         if (!File.Exists(path)) return;
 
 
         watcher.EnableRaisingEvents = false;
         watcher.EnableRaisingEvents = false;
@@ -131,11 +131,11 @@ internal class LocalPalettesFetcher : PaletteListDataSource
 
 
     public void RenamePalette(string oldFileName, string newFileName)
     public void RenamePalette(string oldFileName, string newFileName)
     {
     {
-        if (!Directory.Exists(PathToPalettesFolder))
+        if (!Directory.Exists(Paths.PathToPalettesFolder))
             return;
             return;
 
 
-        string oldPath = Path.Join(PathToPalettesFolder, oldFileName);
-        string newPath = Path.Join(PathToPalettesFolder, newFileName);
+        string oldPath = Path.Join(Paths.PathToPalettesFolder, oldFileName);
+        string newPath = Path.Join(Paths.PathToPalettesFolder, newFileName);
         if (!File.Exists(oldPath) || File.Exists(newPath))
         if (!File.Exists(oldPath) || File.Exists(newPath))
             return;
             return;
 
 
@@ -149,7 +149,7 @@ internal class LocalPalettesFetcher : PaletteListDataSource
     public async Task RefreshCacheAll()
     public async Task RefreshCacheAll()
     {
     {
         string[] files = DirectoryExtensions.GetFiles(
         string[] files = DirectoryExtensions.GetFiles(
-            PathToPalettesFolder,
+            Paths.PathToPalettesFolder,
             string.Join("|", AvailableParsers.SelectMany(x => x.SupportedFileExtensions)),
             string.Join("|", AvailableParsers.SelectMany(x => x.SupportedFileExtensions)),
             SearchOption.TopDirectoryOnly);
             SearchOption.TopDirectoryOnly);
         cachedPalettes = await ParseAll(files);
         cachedPalettes = await ParseAll(files);
@@ -317,12 +317,12 @@ internal class LocalPalettesFetcher : PaletteListDataSource
         return result;
         return result;
     }
     }
 
 
-    private static Palette CreatePalette(PaletteFileData fileData, string file, bool isFavourite)
+    private Palette CreatePalette(PaletteFileData fileData, string file, bool isFavourite)
     {
     {
         var palette = new Palette(
         var palette = new Palette(
             fileData.Title,
             fileData.Title,
-            new List<string>(fileData.GetHexColors()),
-            Path.GetFileName(file))
+            new List<PaletteColor>(fileData.GetPaletteColors()),
+            Path.GetFileName(file), this)
         {
         {
             IsFavourite = isFavourite
             IsFavourite = isFavourite
         };
         };
@@ -337,9 +337,9 @@ internal class LocalPalettesFetcher : PaletteListDataSource
 
 
     private static void InitDir()
     private static void InitDir()
     {
     {
-        if (!Directory.Exists(PathToPalettesFolder))
+        if (!Directory.Exists(Paths.PathToPalettesFolder))
         {
         {
-            Directory.CreateDirectory(PathToPalettesFolder);
+            Directory.CreateDirectory(Paths.PathToPalettesFolder);
         }
         }
     }
     }
 }
 }

+ 0 - 12
src/PixiEditor/Models/DataProviders/PaletteListDataSource.cs

@@ -1,12 +0,0 @@
-using PixiEditor.Models.DataHolders.Palettes;
-using PixiEditor.Models.IO;
-
-namespace PixiEditor.Models.DataProviders;
-
-internal abstract class PaletteListDataSource
-{
-    public virtual void Initialize() { }
-    public abstract Task<PaletteList> FetchPaletteList(int startIndex, int items, FilteringSettings filtering);
-    public List<PaletteFileParser> AvailableParsers { get; set; }
-
-}

+ 2 - 1
src/PixiEditor/Models/Dialogs/ConfirmationDialog.cs

@@ -1,5 +1,6 @@
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.Models.Localization;
 using PixiEditor.Views;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 

+ 1 - 1
src/PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -1,4 +1,4 @@
-using PixiEditor.Models.UserPreferences;
+using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Views;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 

+ 2 - 1
src/PixiEditor/Models/Dialogs/NoticeDialog.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
 namespace PixiEditor.Models.Dialogs;
 namespace PixiEditor.Models.Dialogs;

+ 2 - 1
src/PixiEditor/Models/Dialogs/OptionDialog.cs

@@ -1,5 +1,6 @@
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.Models.Localization;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
 namespace PixiEditor.Models.Dialogs;
 namespace PixiEditor.Models.Dialogs;

+ 2 - 1
src/PixiEditor/Models/Dialogs/OptionsDialog.cs

@@ -1,7 +1,8 @@
 using System.Collections;
 using System.Collections;
 using System.Windows.Controls;
 using System.Windows.Controls;
 using System.Windows.Media;
 using System.Windows.Media;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Localization;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
 namespace PixiEditor.Models.Dialogs;
 namespace PixiEditor.Models.Dialogs;

+ 2 - 1
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -1,6 +1,7 @@
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
-using PixiEditor.Localization;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.Models.Localization;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 
 namespace PixiEditor.Models.DocumentModels;
 namespace PixiEditor.Models.DocumentModels;

+ 4 - 3
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -7,6 +7,7 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.Vector;
 using PixiEditor.DrawingApi.Core.Surface.Vector;
+using PixiEditor.Extensions.Palettes;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.DocumentPassthroughActions;
@@ -233,16 +234,16 @@ internal class DocumentOperationsModule
     /// </summary>
     /// </summary>
     /// <param name="oldColor">The color to replace</param>
     /// <param name="oldColor">The color to replace</param>
     /// <param name="newColor">The new color</param>
     /// <param name="newColor">The new color</param>
-    public void ReplaceColor(Color oldColor, Color newColor)
+    public void ReplaceColor(PaletteColor oldColor, PaletteColor newColor)
     {
     {
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
             return;
             return;
         
         
-        Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor, newColor));
+        Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor.ToColor(), newColor.ToColor()));
         ReplaceInPalette(oldColor, newColor);
         ReplaceInPalette(oldColor, newColor);
     }
     }
 
 
-    private void ReplaceInPalette(Color oldColor, Color newColor)
+    private void ReplaceInPalette(PaletteColor oldColor, PaletteColor newColor)
     {
     {
         int indexOfOldColor = Document.Palette.IndexOf(oldColor);
         int indexOfOldColor = Document.Palette.IndexOf(oldColor);
         if(indexOfOldColor == -1)
         if(indexOfOldColor == -1)

Some files were not shown because too many files changed in this diff