mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-05-08 11:34:05 +02:00
Move PluginManager to Core lib & refactor plugin enums
This commit is contained in:
parent
c2f7e52d13
commit
26d2d7a51e
Core
FormBrowser.cs
Management
Notification
Other
TweetDeckBrowser.csPlugins
Resources
TweetDuck.csprojlib/TweetLib.Core/Features/Plugins
@ -14,9 +14,9 @@
|
||||
using TweetDuck.Core.Other.Analytics;
|
||||
using TweetDuck.Core.Other.Settings.Dialogs;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetDuck.Updates;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
using TweetLib.Core.Features.Updates;
|
||||
|
||||
@ -65,7 +65,7 @@ public FormBrowser(){
|
||||
|
||||
Text = Program.BrandName;
|
||||
|
||||
this.plugins = new PluginManager(this, Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
|
||||
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
|
||||
this.plugins.Reloaded += plugins_Reloaded;
|
||||
this.plugins.Executed += plugins_Executed;
|
||||
this.plugins.Reload();
|
||||
@ -236,7 +236,9 @@ private void trayIcon_ClickClose(object sender, EventArgs e){
|
||||
|
||||
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
|
||||
if (e.HasErrors){
|
||||
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
this.InvokeAsyncSafe(() => { // TODO not needed but makes code consistent...
|
||||
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoaded){
|
||||
@ -244,9 +246,11 @@ private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
|
||||
}
|
||||
}
|
||||
|
||||
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
|
||||
private void plugins_Executed(object sender, PluginErrorEventArgs e){
|
||||
if (e.HasErrors){
|
||||
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
this.InvokeAsyncSafe(() => {
|
||||
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TweetDuck.Core.Other;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
@ -141,7 +140,7 @@ public bool Import(Items items){
|
||||
|
||||
entry.WriteToFile(Path.Combine(Program.PluginDataPath, value[0], value[1]), true);
|
||||
|
||||
if (!plugins.IsPluginInstalled(value[0])){
|
||||
if (!plugins.Plugins.Any(plugin => plugin.Identifier.Equals(value[0]))){
|
||||
missingPlugins.Add(value[0]);
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Core.Notification.Example{
|
||||
sealed class FormNotificationExample : FormNotificationMain{
|
||||
|
@ -9,6 +9,7 @@
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
||||
namespace TweetDuck.Core.Notification{
|
||||
@ -73,7 +74,7 @@ protected FormNotificationMain(FormBrowser owner, PluginManager pluginManager, b
|
||||
browser.LoadingStateChanged += Browser_LoadingStateChanged;
|
||||
browser.FrameLoadEnd += Browser_FrameLoadEnd;
|
||||
|
||||
plugins.Register(browser, PluginEnvironment.Notification);
|
||||
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(this, browser));
|
||||
|
||||
mouseHookDelegate = MouseHookProc;
|
||||
Disposed += (sender, args) => StopMouseHook(true);
|
||||
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using TweetDuck.Plugins;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Core.Notification{
|
||||
sealed partial class FormNotificationTweet : FormNotificationMain{
|
||||
|
@ -6,9 +6,9 @@
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Other;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Core.Notification.Screenshot{
|
||||
sealed class FormNotificationScreenshotable : FormNotificationBase{
|
||||
|
@ -9,7 +9,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
#if GEN_SCREENSHOT_FRAMES
|
||||
using System.Drawing.Imaging;
|
||||
|
@ -8,8 +8,8 @@
|
||||
using System.Timers;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Core.Other.Analytics{
|
||||
|
@ -10,7 +10,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using TweetDuck.Core.Notification;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
@ -9,7 +9,7 @@
|
||||
using TweetDuck.Core.Other.Settings;
|
||||
using TweetDuck.Core.Other.Settings.Dialogs;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Updates;
|
||||
|
||||
namespace TweetDuck.Core.Other{
|
||||
|
@ -4,7 +4,7 @@
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Core.Management;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Core.Other.Settings.Dialogs{
|
||||
|
@ -3,7 +3,7 @@
|
||||
using TweetDuck.Core.Other.Analytics;
|
||||
using TweetDuck.Core.Other.Settings.Dialogs;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Core.Other.Settings{
|
||||
sealed partial class TabSettingsFeedback : BaseTabSettings{
|
||||
|
@ -13,6 +13,7 @@
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
||||
namespace TweetDuck.Core{
|
||||
@ -77,7 +78,7 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridg
|
||||
this.browser.SetupZoomEvents();
|
||||
|
||||
owner.Controls.Add(browser);
|
||||
plugins.Register(browser, PluginEnvironment.Browser, true);
|
||||
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(owner, browser));
|
||||
|
||||
Config.MuteToggled += Config_MuteToggled;
|
||||
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
|
||||
|
35
Plugins/PluginDispatcher.cs
Normal file
35
Plugins/PluginDispatcher.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Core.Adapters;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetLib.Core.Browser;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
|
||||
namespace TweetDuck.Plugins{
|
||||
sealed class PluginDispatcher : IPluginDispatcher{
|
||||
public event EventHandler<PluginDispatchEventArgs> Ready;
|
||||
|
||||
private readonly IWebBrowser browser;
|
||||
private readonly IScriptExecutor executor;
|
||||
|
||||
public PluginDispatcher(Control sync, IWebBrowser browser){
|
||||
this.browser = browser;
|
||||
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
|
||||
this.executor = new CefScriptExecutor(sync, browser);
|
||||
}
|
||||
|
||||
void IPluginDispatcher.AttachBridge(string name, object bridge){
|
||||
browser.RegisterAsyncJsObject(name, bridge);
|
||||
}
|
||||
|
||||
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
|
||||
IFrame frame = e.Frame;
|
||||
|
||||
if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){
|
||||
Ready?.Invoke(this, new PluginDispatchEventArgs(executor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
using CefSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
|
||||
namespace TweetDuck.Plugins{
|
||||
sealed class PluginManager : IPluginManager{
|
||||
private const string SetupScriptPrefix = "plugins.";
|
||||
|
||||
public string PathCustomPlugins => Path.Combine(pluginFolder, PluginGroup.Custom.GetSubFolder());
|
||||
|
||||
public IEnumerable<Plugin> Plugins => plugins;
|
||||
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
|
||||
|
||||
public IPluginConfig Config { get; }
|
||||
|
||||
public event EventHandler<PluginErrorEventArgs> Reloaded;
|
||||
public event EventHandler<PluginErrorEventArgs> Executed;
|
||||
|
||||
private readonly string pluginFolder;
|
||||
private readonly string pluginDataFolder;
|
||||
|
||||
private readonly Control sync;
|
||||
private readonly PluginBridge bridge;
|
||||
|
||||
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
|
||||
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
|
||||
private readonly Random rand = new Random();
|
||||
|
||||
private IWebBrowser mainBrowser;
|
||||
|
||||
public PluginManager(Control sync, IPluginConfig config, string pluginFolder, string pluginDataFolder){
|
||||
this.Config = config;
|
||||
this.Config.PluginChangedState += Config_PluginChangedState;
|
||||
|
||||
this.pluginFolder = pluginFolder;
|
||||
this.pluginDataFolder = pluginDataFolder;
|
||||
|
||||
this.sync = sync;
|
||||
this.bridge = new PluginBridge(this);
|
||||
}
|
||||
|
||||
public void Register(IWebBrowser browser, PluginEnvironment environment, bool asMainBrowser = false){
|
||||
browser.FrameLoadEnd += (sender, args) => {
|
||||
IFrame frame = args.Frame;
|
||||
|
||||
if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){
|
||||
ExecutePlugins(frame, environment);
|
||||
}
|
||||
};
|
||||
|
||||
browser.RegisterAsyncJsObject("$TDP", bridge);
|
||||
|
||||
if (asMainBrowser){
|
||||
mainBrowser = browser;
|
||||
}
|
||||
}
|
||||
|
||||
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
|
||||
mainBrowser?.ExecuteScriptAsync("TDPF_setPluginState", e.Plugin, e.IsEnabled);
|
||||
}
|
||||
|
||||
public bool IsPluginInstalled(string identifier){
|
||||
return plugins.Any(plugin => plugin.Identifier.Equals(identifier));
|
||||
}
|
||||
|
||||
public bool HasAnyPlugin(PluginEnvironment environment){
|
||||
return plugins.Any(plugin => plugin.Environments.HasFlag(environment));
|
||||
}
|
||||
|
||||
public bool IsPluginConfigurable(Plugin plugin){
|
||||
return plugin.HasConfig || bridge.WithConfigureFunction.Contains(plugin);
|
||||
}
|
||||
|
||||
public void ConfigurePlugin(Plugin plugin){
|
||||
if (bridge.WithConfigureFunction.Contains(plugin)){
|
||||
mainBrowser?.ExecuteScriptAsync("TDPF_configurePlugin", plugin);
|
||||
}
|
||||
else if (plugin.HasConfig){
|
||||
if (File.Exists(plugin.ConfigPath)){
|
||||
using(Process.Start("explorer.exe", "/select,\"" + plugin.ConfigPath.Replace('/', '\\') + "\"")){}
|
||||
}
|
||||
else{
|
||||
using(Process.Start("explorer.exe", '"' + plugin.GetPluginFolder(PluginFolder.Data).Replace('/', '\\') + '"')){}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetTokenFromPlugin(Plugin plugin){
|
||||
foreach(KeyValuePair<int, Plugin> kvp in tokens){
|
||||
if (kvp.Value.Equals(plugin)){
|
||||
return kvp.Key;
|
||||
}
|
||||
}
|
||||
|
||||
int token, attempts = 1000;
|
||||
|
||||
do{
|
||||
token = rand.Next();
|
||||
}while(tokens.ContainsKey(token) && --attempts >= 0);
|
||||
|
||||
if (attempts < 0){
|
||||
token = -tokens.Count - 1;
|
||||
}
|
||||
|
||||
tokens[token] = plugin;
|
||||
return token;
|
||||
}
|
||||
|
||||
public Plugin GetPluginFromToken(int token){
|
||||
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
|
||||
}
|
||||
|
||||
public void Reload(){
|
||||
plugins.Clear();
|
||||
tokens.Clear();
|
||||
|
||||
List<string> loadErrors = new List<string>(1);
|
||||
|
||||
foreach(var result in PluginGroupExtensions.Values.SelectMany(group => PluginLoader.AllInFolder(pluginFolder, pluginDataFolder, group))){
|
||||
if (result.HasValue){
|
||||
plugins.Add(result.Value);
|
||||
}
|
||||
else{
|
||||
loadErrors.Add(result.Exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
|
||||
}
|
||||
|
||||
private void ExecutePlugins(IFrame frame, PluginEnvironment environment){
|
||||
if (!HasAnyPlugin(environment) || !ScriptLoader.ExecuteFile(frame, SetupScriptPrefix + environment.GetPluginScriptFile(), sync)){
|
||||
return;
|
||||
}
|
||||
|
||||
bool includeDisabled = environment.IncludesDisabledPlugins();
|
||||
|
||||
if (includeDisabled){
|
||||
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GenerateConfig(Config), "gen:pluginconfig");
|
||||
}
|
||||
|
||||
List<string> failedPlugins = new List<string>(1);
|
||||
|
||||
foreach(Plugin plugin in Plugins){
|
||||
string path = plugin.GetScriptPath(environment);
|
||||
|
||||
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun){
|
||||
continue;
|
||||
}
|
||||
|
||||
string script;
|
||||
|
||||
try{
|
||||
script = File.ReadAllText(path);
|
||||
}catch(Exception e){
|
||||
failedPlugins.Add($"{plugin.Identifier} ({Path.GetFileName(path)}): {e.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, GetTokenFromPlugin(plugin), environment), $"plugin:{plugin}");
|
||||
}
|
||||
|
||||
sync.InvokeAsyncSafe(() => {
|
||||
Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using TweetDuck.Core;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
#endif
|
||||
|
||||
namespace TweetDuck.Resources{
|
||||
|
@ -246,11 +246,11 @@
|
||||
<Compile Include="Plugins\PluginControl.Designer.cs">
|
||||
<DependentUpon>PluginControl.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Plugins\PluginDispatcher.cs" />
|
||||
<Compile Include="Plugins\PluginListFlowLayout.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Configuration\PluginConfig.cs" />
|
||||
<Compile Include="Plugins\PluginManager.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
|
@ -2,23 +2,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins.Enums{
|
||||
[Flags]
|
||||
public enum PluginEnvironment{
|
||||
None = 0,
|
||||
Browser = 1,
|
||||
Notification = 2
|
||||
Browser,
|
||||
Notification
|
||||
}
|
||||
|
||||
public static class PluginEnvironmentExtensions{
|
||||
public static IEnumerable<PluginEnvironment> Values { get; } = new PluginEnvironment[]{
|
||||
public static class PluginEnvironments{
|
||||
public static IEnumerable<PluginEnvironment> All { get; } = new PluginEnvironment[]{
|
||||
PluginEnvironment.Browser,
|
||||
PluginEnvironment.Notification
|
||||
};
|
||||
|
||||
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
|
||||
return environment == PluginEnvironment.Browser;
|
||||
}
|
||||
|
||||
public static string? GetPluginScriptFile(this PluginEnvironment environment){
|
||||
return environment switch{
|
||||
PluginEnvironment.Browser => "browser.js",
|
||||
|
@ -6,8 +6,8 @@ public enum PluginGroup{
|
||||
Official, Custom
|
||||
}
|
||||
|
||||
public static class PluginGroupExtensions{
|
||||
public static IEnumerable<PluginGroup> Values { get; } = new PluginGroup[]{
|
||||
public static class PluginGroups{
|
||||
public static IEnumerable<PluginGroup> All { get; } = new PluginGroup[]{
|
||||
PluginGroup.Official,
|
||||
PluginGroup.Custom
|
||||
};
|
||||
|
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using TweetLib.Core.Browser;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins.Events{
|
||||
public sealed class PluginDispatchEventArgs : EventArgs{
|
||||
public IScriptExecutor Executor { get; }
|
||||
|
||||
public PluginDispatchEventArgs(IScriptExecutor executor){
|
||||
this.Executor = executor;
|
||||
}
|
||||
}
|
||||
}
|
9
lib/TweetLib.Core/Features/Plugins/IPluginDispatcher.cs
Normal file
9
lib/TweetLib.Core/Features/Plugins/IPluginDispatcher.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
public interface IPluginDispatcher{
|
||||
event EventHandler<PluginDispatchEventArgs> Ready;
|
||||
void AttachBridge(string name, object bridge);
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
public interface IPluginManager{
|
||||
IPluginConfig Config { get; }
|
||||
|
||||
event EventHandler<PluginErrorEventArgs> Reloaded;
|
||||
|
||||
int GetTokenFromPlugin(Plugin plugin);
|
||||
Plugin GetPluginFromToken(int token);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
@ -8,7 +10,6 @@ public sealed class Plugin{
|
||||
|
||||
public string Identifier { get; }
|
||||
public PluginGroup Group { get; }
|
||||
public PluginEnvironment Environments { get; }
|
||||
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
@ -39,14 +40,15 @@ public string DefaultConfigPath{
|
||||
|
||||
private readonly string pathRoot;
|
||||
private readonly string pathData;
|
||||
private readonly ISet<PluginEnvironment> environments;
|
||||
|
||||
private Plugin(PluginGroup group, string identifier, string pathRoot, string pathData, Builder builder){
|
||||
this.pathRoot = pathRoot;
|
||||
this.pathData = pathData;
|
||||
this.environments = builder.Environments;
|
||||
|
||||
this.Group = group;
|
||||
this.Identifier = identifier;
|
||||
this.Environments = builder.Environments;
|
||||
|
||||
this.Name = builder.Name;
|
||||
this.Description = builder.Description;
|
||||
@ -60,8 +62,12 @@ private Plugin(PluginGroup group, string identifier, string pathRoot, string pat
|
||||
this.CanRun = AppVersion >= RequiredVersion;
|
||||
}
|
||||
|
||||
public bool HasEnvironment(PluginEnvironment environment){
|
||||
return environments.Contains(environment);
|
||||
}
|
||||
|
||||
public string GetScriptPath(PluginEnvironment environment){
|
||||
if (Environments.HasFlag(environment)){
|
||||
if (environments.Contains(environment)){
|
||||
string? file = environment.GetPluginScriptFile();
|
||||
return file != null ? Path.Combine(pathRoot, file) : string.Empty;
|
||||
}
|
||||
@ -133,7 +139,7 @@ public sealed class Builder{
|
||||
public string ConfigDefault { get; set; } = string.Empty;
|
||||
public Version RequiredVersion { get; set; } = DefaultRequiredVersion;
|
||||
|
||||
public PluginEnvironment Environments { get; private set; } = PluginEnvironment.None;
|
||||
public ISet<PluginEnvironment> Environments { get; } = new HashSet<PluginEnvironment>();
|
||||
|
||||
private readonly PluginGroup group;
|
||||
private readonly string pathRoot;
|
||||
@ -148,7 +154,7 @@ public Builder(PluginGroup group, string name, string pathRoot, string pathData)
|
||||
}
|
||||
|
||||
public void AddEnvironment(PluginEnvironment environment){
|
||||
this.Environments |= environment;
|
||||
Environments.Add(environment);
|
||||
}
|
||||
|
||||
public Plugin BuildAndSetup(){
|
||||
@ -158,7 +164,7 @@ public Plugin BuildAndSetup(){
|
||||
throw new InvalidOperationException("Plugin is missing a name in the .meta file");
|
||||
}
|
||||
|
||||
if (plugin.Environments == PluginEnvironment.None){
|
||||
if (!PluginEnvironments.All.Any(plugin.HasEnvironment)){
|
||||
throw new InvalidOperationException("Plugin has no script files");
|
||||
}
|
||||
|
||||
|
@ -11,29 +11,56 @@
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
public sealed class PluginBridge{
|
||||
private readonly IPluginManager manager;
|
||||
internal sealed class PluginBridge{
|
||||
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
|
||||
private readonly Random rand = new Random();
|
||||
|
||||
private readonly FileCache fileCache = new FileCache();
|
||||
private readonly TwoKeyDictionary<int, string, InjectedHTML> notificationInjections = new TwoKeyDictionary<int, string, InjectedHTML>(4, 1);
|
||||
|
||||
public IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
|
||||
public ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
|
||||
internal IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
|
||||
internal ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
|
||||
|
||||
public PluginBridge(IPluginManager manager){
|
||||
this.manager = manager;
|
||||
this.manager.Reloaded += manager_Reloaded;
|
||||
this.manager.Config.PluginChangedState += Config_PluginChangedState;
|
||||
public PluginBridge(PluginManager manager){
|
||||
manager.Reloaded += manager_Reloaded;
|
||||
manager.Config.PluginChangedState += Config_PluginChangedState;
|
||||
}
|
||||
|
||||
internal int GetTokenFromPlugin(Plugin plugin){
|
||||
foreach(KeyValuePair<int, Plugin> kvp in tokens){
|
||||
if (kvp.Value.Equals(plugin)){
|
||||
return kvp.Key;
|
||||
}
|
||||
}
|
||||
|
||||
int token, attempts = 1000;
|
||||
|
||||
do{
|
||||
token = rand.Next();
|
||||
}while(tokens.ContainsKey(token) && --attempts >= 0);
|
||||
|
||||
if (attempts < 0){
|
||||
token = -tokens.Count - 1;
|
||||
}
|
||||
|
||||
tokens[token] = plugin;
|
||||
return token;
|
||||
}
|
||||
|
||||
private Plugin? GetPluginFromToken(int token){
|
||||
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
|
||||
private void manager_Reloaded(object sender, PluginErrorEventArgs e){
|
||||
tokens.Clear();
|
||||
fileCache.Clear();
|
||||
}
|
||||
|
||||
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
|
||||
if (!e.IsEnabled){
|
||||
int token = manager.GetTokenFromPlugin(e.Plugin);
|
||||
int token = GetTokenFromPlugin(e.Plugin);
|
||||
|
||||
fileCache.Remove(token);
|
||||
notificationInjections.Remove(token);
|
||||
@ -43,7 +70,7 @@ private void Config_PluginChangedState(object sender, PluginChangedStateEventArg
|
||||
// Utility methods
|
||||
|
||||
private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
|
||||
Plugin plugin = manager.GetPluginFromToken(token);
|
||||
Plugin? plugin = GetPluginFromToken(token);
|
||||
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(folder, path);
|
||||
|
||||
if (fullPath.Length == 0){
|
||||
@ -116,7 +143,7 @@ public void InjectIntoNotificationsAfter(int token, string key, string search, s
|
||||
}
|
||||
|
||||
public void SetConfigurable(int token){
|
||||
Plugin plugin = manager.GetPluginFromToken(token);
|
||||
Plugin? plugin = GetPluginFromToken(token);
|
||||
|
||||
if (plugin != null){
|
||||
WithConfigureFunction.Add(plugin);
|
||||
|
@ -79,7 +79,7 @@ public static Plugin FromFolder(string name, string pathRoot, string pathData, P
|
||||
}
|
||||
|
||||
private static PluginEnvironment EnvironmentFromFileName(string file){
|
||||
return PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
|
||||
return PluginEnvironments.All.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static void SetProperty(Plugin.Builder builder, string tag, string value){
|
||||
|
123
lib/TweetLib.Core/Features/Plugins/PluginManager.cs
Normal file
123
lib/TweetLib.Core/Features/Plugins/PluginManager.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TweetLib.Core.Browser;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
public sealed class PluginManager{
|
||||
public string PathCustomPlugins => Path.Combine(pluginFolder, PluginGroup.Custom.GetSubFolder());
|
||||
|
||||
public IEnumerable<Plugin> Plugins => plugins;
|
||||
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
|
||||
|
||||
public IPluginConfig Config { get; }
|
||||
|
||||
public event EventHandler<PluginErrorEventArgs> Reloaded;
|
||||
public event EventHandler<PluginErrorEventArgs> Executed;
|
||||
|
||||
private readonly string pluginFolder;
|
||||
private readonly string pluginDataFolder;
|
||||
|
||||
private readonly PluginBridge bridge;
|
||||
private IScriptExecutor? browserExecutor;
|
||||
|
||||
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
|
||||
|
||||
public PluginManager(IPluginConfig config, string pluginFolder, string pluginDataFolder){
|
||||
this.Config = config;
|
||||
this.Config.PluginChangedState += Config_PluginChangedState;
|
||||
|
||||
this.pluginFolder = pluginFolder;
|
||||
this.pluginDataFolder = pluginDataFolder;
|
||||
|
||||
this.bridge = new PluginBridge(this);
|
||||
}
|
||||
|
||||
public void Register(PluginEnvironment environment, IPluginDispatcher dispatcher){
|
||||
dispatcher.AttachBridge("$TDP", bridge);
|
||||
dispatcher.Ready += (sender, args) => {
|
||||
IScriptExecutor executor = args.Executor;
|
||||
|
||||
if (environment == PluginEnvironment.Browser){
|
||||
browserExecutor = executor;
|
||||
}
|
||||
|
||||
Execute(environment, executor);
|
||||
};
|
||||
}
|
||||
|
||||
public void Reload(){
|
||||
plugins.Clear();
|
||||
|
||||
List<string> errors = new List<string>(1);
|
||||
|
||||
foreach(var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(pluginFolder, pluginDataFolder, group))){
|
||||
if (result.HasValue){
|
||||
plugins.Add(result.Value);
|
||||
}
|
||||
else{
|
||||
errors.Add(result.Exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
Reloaded?.Invoke(this, new PluginErrorEventArgs(errors));
|
||||
}
|
||||
|
||||
private void Execute(PluginEnvironment environment, IScriptExecutor executor){
|
||||
if (!plugins.Any(plugin => plugin.HasEnvironment(environment)) || !executor.RunFile($"plugins.{environment.GetPluginScriptFile()}")){
|
||||
return;
|
||||
}
|
||||
|
||||
bool includeDisabled = environment == PluginEnvironment.Browser;
|
||||
|
||||
if (includeDisabled){
|
||||
executor.RunScript("gen:pluginconfig", PluginScriptGenerator.GenerateConfig(Config));
|
||||
}
|
||||
|
||||
List<string> errors = new List<string>(1);
|
||||
|
||||
foreach(Plugin plugin in Plugins){
|
||||
string path = plugin.GetScriptPath(environment);
|
||||
|
||||
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun){
|
||||
continue;
|
||||
}
|
||||
|
||||
string script;
|
||||
|
||||
try{
|
||||
script = File.ReadAllText(path);
|
||||
}catch(Exception e){
|
||||
errors.Add($"{plugin.Identifier} ({Path.GetFileName(path)}): {e.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
executor.RunScript($"plugin:{plugin}", PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, bridge.GetTokenFromPlugin(plugin), environment));
|
||||
}
|
||||
|
||||
Executed?.Invoke(this, new PluginErrorEventArgs(errors));
|
||||
}
|
||||
|
||||
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
|
||||
browserExecutor?.RunFunction("TDPF_setPluginState", e.Plugin, e.IsEnabled);
|
||||
}
|
||||
|
||||
public bool IsPluginConfigurable(Plugin plugin){
|
||||
return plugin.HasConfig || bridge.WithConfigureFunction.Contains(plugin);
|
||||
}
|
||||
|
||||
public void ConfigurePlugin(Plugin plugin){
|
||||
if (bridge.WithConfigureFunction.Contains(plugin) && browserExecutor != null){
|
||||
browserExecutor.RunFunction("TDPF_configurePlugin", plugin);
|
||||
}
|
||||
else if (plugin.HasConfig){
|
||||
App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(PluginFolder.Data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user