diff --git a/src/CurvaLauncher.Common/CurvaLauncher.Common.csproj b/src/CurvaLauncher.Common/CurvaLauncher.Common.csproj index be1f7b8..6e5b9d2 100644 --- a/src/CurvaLauncher.Common/CurvaLauncher.Common.csproj +++ b/src/CurvaLauncher.Common/CurvaLauncher.Common.csproj @@ -1,5 +1,7 @@  + + net8.0-windows enable @@ -11,6 +13,18 @@ true CurvaLauncher true + Common library of CurvaLauncher + git + https://github.com/OrgEleCho/CurvaLauncher + Icon128.png + MIT + + + True + \ + + + diff --git a/src/CurvaLauncher.Plugins/CurvaLauncher.Plugins.csproj b/src/CurvaLauncher.Plugins/CurvaLauncher.Plugins.csproj index fd26206..54821ee 100644 --- a/src/CurvaLauncher.Plugins/CurvaLauncher.Plugins.csproj +++ b/src/CurvaLauncher.Plugins/CurvaLauncher.Plugins.csproj @@ -1,14 +1,30 @@ - + + + net8.0-windows enable enable - true CurvaLauncher + + true true + + Plugin library of CurvaLauncher + git + https://github.com/OrgEleCho/CurvaLauncher + Icon128.png + MIT + + + True + \ + + + diff --git a/src/CurvaLauncher.props b/src/CurvaLauncher.props new file mode 100644 index 0000000..97d1ea8 --- /dev/null +++ b/src/CurvaLauncher.props @@ -0,0 +1,5 @@ + + + 0.7.1-beta + + \ No newline at end of file diff --git a/src/CurvaLauncher.sln b/src/CurvaLauncher.sln index deac774..fcc589e 100644 --- a/src/CurvaLauncher.sln +++ b/src/CurvaLauncher.sln @@ -37,6 +37,15 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CurvaLauncher.Plugins.SHLoa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CurvaLauncher.Plugins.Everything", "Plugins\CurvaLauncher.Plugins.Everything\CurvaLauncher.Plugins.Everything.csproj", "{C7AC6C75-89C6-42E5-8D2A-D06994949F05}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F36EDE5B-9C67-44F4-87A9-792390FECF85}" + ProjectSection(SolutionItems) = preProject + CurvaLauncher.props = CurvaLauncher.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Templates", "Templates", "{45D0660D-436E-4419-AEB9-B6ED5BC3E0ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CurvaLauncher.PluginTemplate", "Templates\CurvaLauncher.PluginTemplate\CurvaLauncher.PluginTemplate.csproj", "{702E5FF3-99D0-4FD3-86E2-04C0A2E82560}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -165,6 +174,14 @@ Global {C7AC6C75-89C6-42E5-8D2A-D06994949F05}.Release|Any CPU.Build.0 = Release|Any CPU {C7AC6C75-89C6-42E5-8D2A-D06994949F05}.Release|x64.ActiveCfg = Release|x64 {C7AC6C75-89C6-42E5-8D2A-D06994949F05}.Release|x64.Build.0 = Release|x64 + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Debug|Any CPU.Build.0 = Debug|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Debug|x64.ActiveCfg = Debug|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Debug|x64.Build.0 = Debug|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Release|Any CPU.ActiveCfg = Release|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Release|Any CPU.Build.0 = Release|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Release|x64.ActiveCfg = Release|Any CPU + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -181,6 +198,7 @@ Global {F3F6F783-4636-457F-80E1-CC489F524B43} = {BAACD50D-2F94-4A65-8B13-49031D617CAC} {8CFC1C29-51AA-45ED-A91F-01F513182002} = {4A86F98E-B276-4F75-9847-8D0E4280D887} {C7AC6C75-89C6-42E5-8D2A-D06994949F05} = {BAACD50D-2F94-4A65-8B13-49031D617CAC} + {702E5FF3-99D0-4FD3-86E2-04C0A2E82560} = {45D0660D-436E-4419-AEB9-B6ED5BC3E0ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3FC4E11A-3D67-43DE-84D8-DCA1841F0D71} diff --git a/src/CurvaLauncher/CurvaLauncher.csproj b/src/CurvaLauncher/CurvaLauncher.csproj index cb1ae56..916982a 100644 --- a/src/CurvaLauncher/CurvaLauncher.csproj +++ b/src/CurvaLauncher/CurvaLauncher.csproj @@ -1,5 +1,7 @@  + + WinExe net8.0-windows @@ -12,8 +14,6 @@ app.manifest Assets\Icon.ico - 0.7.1-beta - true true diff --git a/src/CurvaLauncher/Services/PluginService.cs b/src/CurvaLauncher/Services/PluginService.cs index 408ed99..616ff50 100644 --- a/src/CurvaLauncher/Services/PluginService.cs +++ b/src/CurvaLauncher/Services/PluginService.cs @@ -14,6 +14,8 @@ using CurvaLauncher.Plugins; using CurvaLauncher.PluginInteraction; using System.Diagnostics; +using System.IO.Compression; +using System.Runtime.Loader; namespace CurvaLauncher.Services; @@ -50,65 +52,123 @@ private void CoreLoadPlugins(out List plugins) plugins = new List(); var dir = EnsurePluginDirectory(); - var dllFiles = dir.GetFiles("*.dll"); AppConfig config = _configService.Config; - foreach (FileInfo dllFile in dllFiles) - if (CoreLoadPlugin(config, dllFile.FullName, out PluginInstance? plugin)) + foreach (var dllFile in dir.GetFiles("*.dll")) + { + if (CoreLoadDllPlugin(config, dllFile.FullName, out PluginInstance? plugin)) + { + plugins.Add(plugin); + } + } + + foreach (var zipFile in dir.GetFiles("*.zip")) + { + if (CoreLoadZipPlugin(config, zipFile.FullName, out PluginInstance? plugin)) { plugins.Add(plugin); } + } } - private bool CoreLoadPlugin(AppConfig config, string dllFilePath, [NotNullWhen(true)] out PluginInstance? pluginInstance) + private bool CoreLoadPluginFromAssembly(AppConfig config, Assembly assembly, [NotNullWhen(true)] out PluginInstance? pluginInstance) { pluginInstance = null; - try - { - var assembly = Assembly.LoadFile(dllFilePath); - - Type? pluginType = assembly.ExportedTypes + Type? pluginType = assembly.ExportedTypes .Where(type => type.IsAssignableTo(typeof(ISyncPlugin)) || type.IsAssignableTo(typeof(IAsyncPlugin))) .FirstOrDefault(); - if (pluginType == null) - return false; + if (pluginType == null) + return false; - if (!PluginInstance.TryCreate(pluginType, out pluginInstance)) - return false; + if (!PluginInstance.TryCreate(pluginType, out pluginInstance)) + return false; - var typeName = pluginType.FullName!; + var typeName = pluginType.FullName!; - if (config.Plugins.TryGetValue(typeName, out var pluginConfig)) - { - var props = pluginInstance.Plugin.GetType().GetProperties() + if (config.Plugins.TryGetValue(typeName, out var pluginConfig)) + { + var props = pluginInstance.Plugin.GetType().GetProperties() .Where(p => p.GetCustomAttribute() is not null || p.GetCustomAttribute() is not null); - if (pluginConfig.Options != null) + if (pluginConfig.Options != null) + { + foreach (var property in props) { - foreach (var property in props) + if (pluginConfig.Options.TryGetPropertyValue(property.Name, out var value)) { - if (pluginConfig.Options.TryGetPropertyValue(property.Name, out var value)) - { - var type = property.PropertyType; - var val = JsonSerializer.Deserialize(value, type); - property.SetValue(pluginInstance.Plugin, val); - } + var type = property.PropertyType; + var val = JsonSerializer.Deserialize(value, type); + property.SetValue(pluginInstance.Plugin, val); } } - - pluginInstance.IsEnabled = pluginConfig.IsEnabled; - pluginInstance.Weight = pluginConfig.Weight; } - else + + pluginInstance.IsEnabled = pluginConfig.IsEnabled; + pluginInstance.Weight = pluginConfig.Weight; + } + else + { + pluginInstance.IsEnabled = true; + } + + return true; + } + + private bool CoreLoadDllPlugin(AppConfig config, string dllFilePath, [NotNullWhen(true)] out PluginInstance? pluginInstance) + { + pluginInstance = null; + + try + { + var assembly = Assembly.LoadFile(dllFilePath); + return CoreLoadPluginFromAssembly(config, assembly, out pluginInstance); + } + catch (Exception ex) + { + Debug.WriteLine($"Plugin load failed, {ex}"); + return false; + } + } + + private bool CoreLoadZipPlugin(AppConfig config, string zipFilePath, [NotNullWhen(true)] out PluginInstance? pluginInstance) + { + pluginInstance = null; + + try + { + using var zipFile = File.OpenRead(zipFilePath); + using var zipArchive = new ZipArchive(zipFile, ZipArchiveMode.Read); + var assemblyLoadContext = new AssemblyLoadContext(null, false); + + foreach (var entry in zipArchive.Entries) { - pluginInstance.IsEnabled = true; + if (!entry.FullName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + using var entryStream = entry.Open(); + + try + { + var assembly = assemblyLoadContext.LoadFromStream(entryStream); + + if (pluginInstance is null) + { + CoreLoadPluginFromAssembly(config, assembly, out pluginInstance); + } + } + catch (Exception ex) + { + Debug.WriteLine($"DLL load failed, {ex}"); + } } - return true; + return pluginInstance is not null; } catch (Exception ex) { diff --git a/src/Templates/CurvaLauncher.PluginTemplate/.template.config/template.json b/src/Templates/CurvaLauncher.PluginTemplate/.template.config/template.json new file mode 100644 index 0000000..21072b2 --- /dev/null +++ b/src/Templates/CurvaLauncher.PluginTemplate/.template.config/template.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "EleCho", + "identity": "CurvaLauncher.PluginTemplate", + "classifications": [ + "CurvaLauncher", + "Library" + ], + "name": "CurvaLauncher Plugin", + "description": "Creates a CurvaLauncher plugin", + "tags": { + "language": "C#", + "type": "project" + }, + "shortName": "clp", + "sourceName": "CurvaLauncher.PluginTemplate", + "symbols": { + "EnableI18n": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "true" + } + }, + "sources": [ + { + "modifiers": [ + { + "condition": "(!EnableI18n)", + "exclude": [ + "I18n/EnUs.xaml", + "I18n/ZhHans.xaml" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Templates/CurvaLauncher.PluginTemplate/CurvaLauncher.PluginTemplate.csproj b/src/Templates/CurvaLauncher.PluginTemplate/CurvaLauncher.PluginTemplate.csproj new file mode 100644 index 0000000..b3183c3 --- /dev/null +++ b/src/Templates/CurvaLauncher.PluginTemplate/CurvaLauncher.PluginTemplate.csproj @@ -0,0 +1,21 @@ + + + + net8.0-windows + enable + + + + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + + diff --git a/src/Templates/CurvaLauncher.PluginTemplate/I18n/EnUs.xaml b/src/Templates/CurvaLauncher.PluginTemplate/I18n/EnUs.xaml new file mode 100644 index 0000000..a069afb --- /dev/null +++ b/src/Templates/CurvaLauncher.PluginTemplate/I18n/EnUs.xaml @@ -0,0 +1,9 @@ + + Sample Plugin + This is a plugin with i18n support + + Test Plugin + Your plugin is running well + \ No newline at end of file diff --git a/src/Templates/CurvaLauncher.PluginTemplate/I18n/ZhHans.xaml b/src/Templates/CurvaLauncher.PluginTemplate/I18n/ZhHans.xaml new file mode 100644 index 0000000..4bc7010 --- /dev/null +++ b/src/Templates/CurvaLauncher.PluginTemplate/I18n/ZhHans.xaml @@ -0,0 +1,9 @@ + + 示例插件 + 这是一个支持本地化的插件 + + 测试插件 + 你的插件运行良好 + \ No newline at end of file diff --git a/src/Templates/CurvaLauncher.PluginTemplate/SomePlugin.cs b/src/Templates/CurvaLauncher.PluginTemplate/SomePlugin.cs new file mode 100644 index 0000000..bfee57a --- /dev/null +++ b/src/Templates/CurvaLauncher.PluginTemplate/SomePlugin.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using CurvaLauncher; +using CurvaLauncher.Plugins; + +namespace CurvaLauncher.PluginTemplate +{ +#if (!EnableI18n) + public class SomePlugin : Plugin, ISyncPlugin + { + public override string Name => "Sample Plugin"; // Plugin Name + public override string Description => "Plugin Description"; // Plugin Description + + public override ImageSource Icon { get; } + = new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgra32, null); // Empty Icon + + public SomePlugin(CurvaLauncherContext context) : base(context) + { + // With CurvaLauncherContext, you can use some APIs + // provided by CurvaLauncher + + } + + public IEnumerable Query(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + yield break; + } + + yield return new EmptyQueryResult("Test Plugin OK", $"Your plugin {Name} is running well", 1, null); + } + } +#else + public class SomePlugin : SyncI18nPlugin + { + private readonly CurvaLauncherContext _context; + private readonly Assembly _currentAssembly; + + public override object NameKey => "StrPluginName"; // Plugin name resource key + public override object DescriptionKey => "StrPluginDescription"; // Plugin description resource key + + public override ImageSource Icon { get; } + = new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgra32, null); // Empty Icon + + public SomePlugin(CurvaLauncherContext context) : base(context) + { + _context = context; + _currentAssembly = Assembly.GetExecutingAssembly(); + + // With CurvaLauncherContext, you can use some APIs + // provided by CurvaLauncher + + } + + public override IEnumerable GetI18nResourceDictionaries() + { + yield return I18nResourceDictionary.Create(new CultureInfo("en-US"), "I18n/EnUs.xaml"); + yield return I18nResourceDictionary.Create(new CultureInfo("zh-Hans"), "I18n/ZhHans.xaml"); + } + + public override IEnumerable Query(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + yield break; + } + + yield return new EmptyQueryResult( + _context.GetI18nResourceString(_currentAssembly, "StrTestResultTitle")!, + _context.GetI18nResourceString(_currentAssembly, "StrTestResultDescription")!, + 1, + null); + } + } +#endif +} diff --git a/src/Templates/Templates.sln b/src/Templates/Templates.sln new file mode 100644 index 0000000..47b5534 --- /dev/null +++ b/src/Templates/Templates.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CurvaLauncher.I18nPluginTemplate", "CurvaLauncher.I18nPluginTemplate\CurvaLauncher.I18nPluginTemplate.csproj", "{6DE03DDB-7AD5-49B0-B212-07C15D8E1F8C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CurvaLauncher.PluginTemplate", "CurvaLauncher.PluginTemplate\CurvaLauncher.PluginTemplate.csproj", "{709E311C-73D1-438E-B77A-5919CEFDCD72}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6DE03DDB-7AD5-49B0-B212-07C15D8E1F8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DE03DDB-7AD5-49B0-B212-07C15D8E1F8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DE03DDB-7AD5-49B0-B212-07C15D8E1F8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DE03DDB-7AD5-49B0-B212-07C15D8E1F8C}.Release|Any CPU.Build.0 = Release|Any CPU + {709E311C-73D1-438E-B77A-5919CEFDCD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {709E311C-73D1-438E-B77A-5919CEFDCD72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {709E311C-73D1-438E-B77A-5919CEFDCD72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {709E311C-73D1-438E-B77A-5919CEFDCD72}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C1A7E26-DDBB-4487-ACEB-F681452D34C7} + EndGlobalSection +EndGlobal