1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-21 05:34:10 +02:00

Move PluginManager to Core lib & refactor plugin enums

This commit is contained in:
chylex 2019-07-14 20:41:18 +02:00
parent c2f7e52d13
commit 26d2d7a51e
26 changed files with 260 additions and 244 deletions

View File

@ -14,9 +14,9 @@
using TweetDuck.Core.Other.Analytics; using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs; using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetDuck.Updates; using TweetDuck.Updates;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events; using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Updates; using TweetLib.Core.Features.Updates;
@ -65,7 +65,7 @@ public FormBrowser(){
Text = Program.BrandName; 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.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed; this.plugins.Executed += plugins_Executed;
this.plugins.Reload(); this.plugins.Reload();
@ -236,7 +236,9 @@ private void trayIcon_ClickClose(object sender, EventArgs e){
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){ private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
if (e.HasErrors){ 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){ 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){ 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);
});
} }
} }

View File

@ -3,7 +3,6 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Plugins;
using TweetLib.Core.Data; using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums; 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); 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]); missingPlugins.Add(value[0]);
} }
} }

View File

@ -2,8 +2,8 @@
using System.Windows.Forms; using System.Windows.Forms;
using CefSharp; using CefSharp;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification.Example{ namespace TweetDuck.Core.Notification.Example{
sealed class FormNotificationExample : FormNotificationMain{ sealed class FormNotificationExample : FormNotificationMain{

View File

@ -9,6 +9,7 @@
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Data; using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Core.Notification{ namespace TweetDuck.Core.Notification{
@ -73,7 +74,7 @@ protected FormNotificationMain(FormBrowser owner, PluginManager pluginManager, b
browser.LoadingStateChanged += Browser_LoadingStateChanged; browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd; browser.FrameLoadEnd += Browser_FrameLoadEnd;
plugins.Register(browser, PluginEnvironment.Notification); plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(this, browser));
mouseHookDelegate = MouseHookProc; mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook(true); Disposed += (sender, args) => StopMouseHook(true);

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using TweetDuck.Plugins;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification{ namespace TweetDuck.Core.Notification{
sealed partial class FormNotificationTweet : FormNotificationMain{ sealed partial class FormNotificationTweet : FormNotificationMain{

View File

@ -6,9 +6,9 @@
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Data; using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification.Screenshot{ namespace TweetDuck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotificationBase{ sealed class FormNotificationScreenshotable : FormNotificationBase{

View File

@ -9,7 +9,7 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Plugins; using TweetLib.Core.Features.Plugins;
#if GEN_SCREENSHOT_FRAMES #if GEN_SCREENSHOT_FRAMES
using System.Drawing.Imaging; using System.Drawing.Imaging;

View File

@ -8,8 +8,8 @@
using System.Timers; using System.Timers;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetLib.Core; using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils; using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Analytics{ namespace TweetDuck.Core.Other.Analytics{

View File

@ -10,7 +10,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using TweetDuck.Core.Notification; using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetLib.Core; using TweetLib.Core;
using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;

View File

@ -9,7 +9,7 @@
using TweetDuck.Core.Other.Settings; using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Other.Settings.Dialogs; using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Updates; using TweetLib.Core.Features.Updates;
namespace TweetDuck.Core.Other{ namespace TweetDuck.Core.Other{

View File

@ -4,7 +4,7 @@
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Core.Management; using TweetDuck.Core.Management;
using TweetDuck.Plugins; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils; using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Settings.Dialogs{ namespace TweetDuck.Core.Other.Settings.Dialogs{

View File

@ -3,7 +3,7 @@
using TweetDuck.Core.Other.Analytics; using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs; using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Other.Settings{ namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsFeedback : BaseTabSettings{ sealed partial class TabSettingsFeedback : BaseTabSettings{

View File

@ -13,6 +13,7 @@
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Core{ namespace TweetDuck.Core{
@ -77,7 +78,7 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridg
this.browser.SetupZoomEvents(); this.browser.SetupZoomEvents();
owner.Controls.Add(browser); owner.Controls.Add(browser);
plugins.Register(browser, PluginEnvironment.Browser, true); plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(owner, browser));
Config.MuteToggled += Config_MuteToggled; Config.MuteToggled += Config_MuteToggled;
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged; Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;

View 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));
}
}
}
}

View File

@ -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));
});
}
}
}

View File

@ -11,7 +11,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using TweetDuck.Core; using TweetDuck.Core;
using TweetDuck.Plugins; using TweetLib.Core.Features.Plugins;
#endif #endif
namespace TweetDuck.Resources{ namespace TweetDuck.Resources{

View File

@ -246,11 +246,11 @@
<Compile Include="Plugins\PluginControl.Designer.cs"> <Compile Include="Plugins\PluginControl.Designer.cs">
<DependentUpon>PluginControl.cs</DependentUpon> <DependentUpon>PluginControl.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Plugins\PluginDispatcher.cs" />
<Compile Include="Plugins\PluginListFlowLayout.cs"> <Compile Include="Plugins\PluginListFlowLayout.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
<Compile Include="Configuration\PluginConfig.cs" /> <Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Plugins\PluginManager.cs" />
<Compile Include="Properties\Resources.Designer.cs"> <Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>

View File

@ -2,23 +2,17 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace TweetLib.Core.Features.Plugins.Enums{ namespace TweetLib.Core.Features.Plugins.Enums{
[Flags]
public enum PluginEnvironment{ public enum PluginEnvironment{
None = 0, Browser,
Browser = 1, Notification
Notification = 2
} }
public static class PluginEnvironmentExtensions{ public static class PluginEnvironments{
public static IEnumerable<PluginEnvironment> Values { get; } = new PluginEnvironment[]{ public static IEnumerable<PluginEnvironment> All { get; } = new PluginEnvironment[]{
PluginEnvironment.Browser, PluginEnvironment.Browser,
PluginEnvironment.Notification PluginEnvironment.Notification
}; };
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
return environment == PluginEnvironment.Browser;
}
public static string? GetPluginScriptFile(this PluginEnvironment environment){ public static string? GetPluginScriptFile(this PluginEnvironment environment){
return environment switch{ return environment switch{
PluginEnvironment.Browser => "browser.js", PluginEnvironment.Browser => "browser.js",

View File

@ -6,8 +6,8 @@ public enum PluginGroup{
Official, Custom Official, Custom
} }
public static class PluginGroupExtensions{ public static class PluginGroups{
public static IEnumerable<PluginGroup> Values { get; } = new PluginGroup[]{ public static IEnumerable<PluginGroup> All { get; } = new PluginGroup[]{
PluginGroup.Official, PluginGroup.Official,
PluginGroup.Custom PluginGroup.Custom
}; };

View File

@ -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;
}
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using TweetLib.Core.Features.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;
namespace TweetLib.Core.Features.Plugins{ namespace TweetLib.Core.Features.Plugins{
@ -8,7 +10,6 @@ public sealed class Plugin{
public string Identifier { get; } public string Identifier { get; }
public PluginGroup Group { get; } public PluginGroup Group { get; }
public PluginEnvironment Environments { get; }
public string Name { get; } public string Name { get; }
public string Description { get; } public string Description { get; }
@ -39,14 +40,15 @@ public string DefaultConfigPath{
private readonly string pathRoot; private readonly string pathRoot;
private readonly string pathData; private readonly string pathData;
private readonly ISet<PluginEnvironment> environments;
private Plugin(PluginGroup group, string identifier, string pathRoot, string pathData, Builder builder){ private Plugin(PluginGroup group, string identifier, string pathRoot, string pathData, Builder builder){
this.pathRoot = pathRoot; this.pathRoot = pathRoot;
this.pathData = pathData; this.pathData = pathData;
this.environments = builder.Environments;
this.Group = group; this.Group = group;
this.Identifier = identifier; this.Identifier = identifier;
this.Environments = builder.Environments;
this.Name = builder.Name; this.Name = builder.Name;
this.Description = builder.Description; this.Description = builder.Description;
@ -60,8 +62,12 @@ private Plugin(PluginGroup group, string identifier, string pathRoot, string pat
this.CanRun = AppVersion >= RequiredVersion; this.CanRun = AppVersion >= RequiredVersion;
} }
public bool HasEnvironment(PluginEnvironment environment){
return environments.Contains(environment);
}
public string GetScriptPath(PluginEnvironment environment){ public string GetScriptPath(PluginEnvironment environment){
if (Environments.HasFlag(environment)){ if (environments.Contains(environment)){
string? file = environment.GetPluginScriptFile(); string? file = environment.GetPluginScriptFile();
return file != null ? Path.Combine(pathRoot, file) : string.Empty; 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 string ConfigDefault { get; set; } = string.Empty;
public Version RequiredVersion { get; set; } = DefaultRequiredVersion; 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 PluginGroup group;
private readonly string pathRoot; private readonly string pathRoot;
@ -148,7 +154,7 @@ public Builder(PluginGroup group, string name, string pathRoot, string pathData)
} }
public void AddEnvironment(PluginEnvironment environment){ public void AddEnvironment(PluginEnvironment environment){
this.Environments |= environment; Environments.Add(environment);
} }
public Plugin BuildAndSetup(){ public Plugin BuildAndSetup(){
@ -158,7 +164,7 @@ public Plugin BuildAndSetup(){
throw new InvalidOperationException("Plugin is missing a name in the .meta file"); 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"); throw new InvalidOperationException("Plugin has no script files");
} }

View File

@ -11,29 +11,56 @@
namespace TweetLib.Core.Features.Plugins{ namespace TweetLib.Core.Features.Plugins{
[SuppressMessage("ReSharper", "UnusedMember.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")]
public sealed class PluginBridge{ internal sealed class PluginBridge{
private readonly IPluginManager manager; private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
private readonly Random rand = new Random();
private readonly FileCache fileCache = new FileCache(); private readonly FileCache fileCache = new FileCache();
private readonly TwoKeyDictionary<int, string, InjectedHTML> notificationInjections = new TwoKeyDictionary<int, string, InjectedHTML>(4, 1); private readonly TwoKeyDictionary<int, string, InjectedHTML> notificationInjections = new TwoKeyDictionary<int, string, InjectedHTML>(4, 1);
public IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues; internal IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
public ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>(); internal ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
public PluginBridge(IPluginManager manager){ public PluginBridge(PluginManager manager){
this.manager = manager; manager.Reloaded += manager_Reloaded;
this.manager.Reloaded += manager_Reloaded; manager.Config.PluginChangedState += Config_PluginChangedState;
this.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 // Event handlers
private void manager_Reloaded(object sender, PluginErrorEventArgs e){ private void manager_Reloaded(object sender, PluginErrorEventArgs e){
tokens.Clear();
fileCache.Clear(); fileCache.Clear();
} }
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){ private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
if (!e.IsEnabled){ if (!e.IsEnabled){
int token = manager.GetTokenFromPlugin(e.Plugin); int token = GetTokenFromPlugin(e.Plugin);
fileCache.Remove(token); fileCache.Remove(token);
notificationInjections.Remove(token); notificationInjections.Remove(token);
@ -43,7 +70,7 @@ private void Config_PluginChangedState(object sender, PluginChangedStateEventArg
// Utility methods // Utility methods
private string GetFullPathOrThrow(int token, PluginFolder folder, string path){ 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); string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(folder, path);
if (fullPath.Length == 0){ if (fullPath.Length == 0){
@ -116,7 +143,7 @@ public void InjectIntoNotificationsAfter(int token, string key, string search, s
} }
public void SetConfigurable(int token){ public void SetConfigurable(int token){
Plugin plugin = manager.GetPluginFromToken(token); Plugin? plugin = GetPluginFromToken(token);
if (plugin != null){ if (plugin != null){
WithConfigureFunction.Add(plugin); WithConfigureFunction.Add(plugin);

View File

@ -79,7 +79,7 @@ public static Plugin FromFolder(string name, string pathRoot, string pathData, P
} }
private static PluginEnvironment EnvironmentFromFileName(string file){ 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){ private static void SetProperty(Plugin.Builder builder, string tag, string value){

View 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));
}
}
}
}