1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-23 12:15:48 +02:00

Add a DLL extension API

This commit is contained in:
chylex 2021-08-07 18:17:16 +02:00
parent c5a42e74d9
commit 095c23b472
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
14 changed files with 220 additions and 2 deletions

View File

@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetImpl.CefSharp", "windo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.WinForms.Legacy", "windows\TweetLib.WinForms.Legacy\TweetLib.WinForms.Legacy.csproj", "{B54E732A-4090-4DAA-9ABD-311368C17B68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Api", "lib\TweetLib.Api\TweetLib.Api.csproj", "{85596C10-F76E-4619-9CC6-6C1593880F83}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
@ -54,6 +56,10 @@ Global
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.Build.0 = Debug|x86
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.ActiveCfg = Release|x86
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.Build.0 = Release|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.ActiveCfg = Debug|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.Build.0 = Debug|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.ActiveCfg = Release|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.Build.0 = Release|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86

View File

@ -0,0 +1,37 @@
namespace TweetLib.Api.Data {
public readonly struct NamespacedResource {
public Resource Namespace { get; }
public Resource Path { get; }
public NamespacedResource(Resource ns, Resource path) {
Namespace = ns;
Path = path;
}
private bool Equals(NamespacedResource other) {
return Namespace.Equals(other.Namespace) && Path.Equals(other.Path);
}
public override bool Equals(object? obj) {
return obj is NamespacedResource other && Equals(other);
}
public static bool operator ==(NamespacedResource left, NamespacedResource right) {
return left.Equals(right);
}
public static bool operator !=(NamespacedResource left, NamespacedResource right) {
return !left.Equals(right);
}
public override int GetHashCode() {
unchecked {
return (Namespace.GetHashCode() * 397) ^ Path.GetHashCode();
}
}
public override string ToString() {
return $"{Namespace}:{Path}";
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Text.RegularExpressions;
namespace TweetLib.Api.Data {
public readonly struct Resource {
private const string ValidCharacterPattern = "^[a-z0-9_]+$";
private static readonly Regex ValidCharacterRegex = new Regex(ValidCharacterPattern, RegexOptions.Compiled);
public string Name { get; }
public Resource(string name) {
if (!ValidCharacterRegex.IsMatch(name)) {
throw new ArgumentException("Resource name must match the regex: " + ValidCharacterPattern);
}
Name = name;
}
private bool Equals(Resource other) {
return Name == other.Name;
}
public override bool Equals(object? obj) {
return obj is Resource other && Equals(other);
}
public static bool operator ==(Resource left, Resource right) {
return left.Equals(right);
}
public static bool operator !=(Resource left, Resource right) {
return !left.Equals(right);
}
public override int GetHashCode() {
return Name.GetHashCode();
}
public override string ToString() {
return Name;
}
}
}

View File

@ -0,0 +1,5 @@
namespace TweetLib.Api {
public interface ITweetDuckApi {
T? FindService<T>() where T : class, ITweetDuckService;
}
}

View File

@ -0,0 +1,3 @@
namespace TweetLib.Api {
public interface ITweetDuckService {}
}

View File

@ -0,0 +1,15 @@
using TweetLib.Api.Data;
namespace TweetLib.Api {
public abstract class TweetDuckExtension {
/// <summary>
/// Unique identifier of the extension.
/// </summary>
public abstract Resource Id { get; }
/// <summary>
/// Called when the extension is loaded on startup, or enabled at runtime.
/// </summary>
public abstract void Enable(ITweetDuckApi api);
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Platforms>x86;x64</Platforms>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Version.cs" Link="Version.cs" />
</ItemGroup>
</Project>

View File

@ -5,6 +5,8 @@
using TweetLib.Core.Application;
using TweetLib.Core.Features;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Resources;
using TweetLib.Core.Systems.Api;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Core.Systems.Logging;
using TweetLib.Utils.Static;
@ -21,8 +23,11 @@ public static class App {
internal static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
internal static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
public static readonly string LogoPath = Path.Combine(ResourcesPath, "images/logo.png");
public static readonly string ExtensionPath = Path.Combine(ProgramPath, "extensions");
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
public static readonly string LogoPath = Path.Combine(ResourcesPath, "images/logo.png");
public static ApiImplementation Api { get; } = new ();
public static Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);

View File

@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Reflection;
using TweetLib.Api;
using TweetLib.Core.Systems.Api;
namespace TweetLib.Core.Features.Extensions {
public static class ExtensionLoader {
public static void LoadAllInFolder(string extensionFolder) {
if (!Directory.Exists(extensionFolder)) {
return;
}
try {
foreach (string file in Directory.EnumerateFiles(extensionFolder, "*.dll", SearchOption.TopDirectoryOnly)) {
try {
Assembly assembly = Assembly.LoadFile(file);
foreach (Type type in assembly.GetTypes()) {
if (typeof(TweetDuckExtension).IsAssignableFrom(type) && Activator.CreateInstance(type) is TweetDuckExtension extension) {
EnableExtension(extension);
}
}
} catch (Exception e) {
App.ErrorHandler.HandleException("Extension Error", "Could not load extension: " + Path.GetFileNameWithoutExtension(file), true, e);
}
}
} catch (DirectoryNotFoundException) {
// ignore
} catch (Exception e) {
App.ErrorHandler.HandleException("Extension Error", "Could not load extensions.", true, e);
}
}
private static void EnableExtension(TweetDuckExtension extension) {
ApiImplementation apiImplementation = App.Api;
apiImplementation.CurrentExtension = extension;
extension.Enable(apiImplementation);
apiImplementation.CurrentExtension = null;
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using TweetLib.Api;
namespace TweetLib.Core.Systems.Api {
public class ApiImplementation : ITweetDuckApi {
public TweetDuckExtension? CurrentExtension { get; internal set; }
private readonly Dictionary<Type, ITweetDuckService> services = new Dictionary<Type, ITweetDuckService>();
internal ApiImplementation() {}
public void RegisterService<T>(T service) where T : class, ITweetDuckService {
if (!typeof(T).IsInterface) {
throw new ArgumentException("Api service implementation must be registered with its interface type.");
}
services.Add(typeof(T), service);
}
public T? FindService<T>() where T : class, ITweetDuckService {
return services.TryGetValue(typeof(T), out ITweetDuckService? service) ? service as T : null;
}
}
}

View File

@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TweetLib.Api\TweetLib.Api.csproj" />
<ProjectReference Include="..\TweetLib.Browser\TweetLib.Browser.csproj" />
<ProjectReference Include="..\TweetLib.Utils\TweetLib.Utils.csproj" />
</ItemGroup>

View File

@ -0,0 +1,16 @@
using System;
using TweetLib.Api;
using TweetLib.Api.Data;
using TweetLib.Core;
namespace TweetDuck.Application {
static class ApiServices {
public static void Register() {
}
internal static NamespacedResource Namespace(Resource path) {
TweetDuckExtension currentExtension = App.Api.CurrentExtension ?? throw new InvalidOperationException("Cannot use API services outside of designated method calls.");
return new NamespacedResource(currentExtension.Id, path);
}
}
}

View File

@ -16,6 +16,7 @@
using TweetLib.Browser.Request;
using TweetLib.Core;
using TweetLib.Core.Application;
using TweetLib.Core.Features.Extensions;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.TweetDeck;
@ -137,6 +138,9 @@ public void Launch(ResourceCache resourceCache, PluginManager pluginManager) {
Cef.Initialize(settings, false, new BrowserProcessHandler());
Win.Application.ApplicationExit += static (_, _) => ExitCleanup();
ApiServices.Register();
ExtensionLoader.LoadAllInFolder(App.ExtensionPath);
var updateCheckClient = new UpdateCheckClient(Path.Combine(storagePath, InstallerFolder));
var mainForm = new FormBrowser(resourceCache, pluginManager, updateCheckClient, lockManager.WindowRestoreMessage);
Win.Application.Run(mainForm);

View File

@ -40,6 +40,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\lib\TweetLib.Api\TweetLib.Api.csproj" />
<ProjectReference Include="..\..\lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj" />
<ProjectReference Include="..\..\lib\TweetLib.Browser\TweetLib.Browser.csproj" />
<ProjectReference Include="..\..\lib\TweetLib.Communication\TweetLib.Communication.csproj" />