mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-05-28 08:34:06 +02:00
Continue refactoring and moving plugin code
This commit is contained in:
parent
108a0fefc3
commit
50bd526025
@ -65,7 +65,7 @@ public FormBrowser(){
|
|||||||
|
|
||||||
Text = Program.BrandName;
|
Text = Program.BrandName;
|
||||||
|
|
||||||
this.plugins = new PluginManager(this, Program.Config.Plugins, Program.PluginPath);
|
this.plugins = new PluginManager(this, 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();
|
||||||
|
@ -82,7 +82,7 @@ public static AnalyticsReport Create(AnalyticsFile file, ExternalInfo info, Plug
|
|||||||
{ "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
|
{ "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
|
||||||
0,
|
0,
|
||||||
{ "Plugins All" , List(plugins.Plugins.Select(Plugin)) },
|
{ "Plugins All" , List(plugins.Plugins.Select(Plugin)) },
|
||||||
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugin => plugins.Config.IsEnabled(plugin)).Select(Plugin)) },
|
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugins.Config.IsEnabled).Select(Plugin)) },
|
||||||
0,
|
0,
|
||||||
{ "Theme" , Dict(editLayoutDesign, "_theme", "light/def") },
|
{ "Theme" , Dict(editLayoutDesign, "_theme", "light/def") },
|
||||||
{ "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") },
|
{ "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") },
|
||||||
|
8
Core/Other/FormPlugins.Designer.cs
generated
8
Core/Other/FormPlugins.Designer.cs
generated
@ -1,4 +1,6 @@
|
|||||||
namespace TweetDuck.Core.Other {
|
using TweetDuck.Plugins;
|
||||||
|
|
||||||
|
namespace TweetDuck.Core.Other {
|
||||||
partial class FormPlugins {
|
partial class FormPlugins {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required designer variable.
|
/// Required designer variable.
|
||||||
@ -27,7 +29,7 @@ private void InitializeComponent() {
|
|||||||
this.btnClose = new System.Windows.Forms.Button();
|
this.btnClose = new System.Windows.Forms.Button();
|
||||||
this.btnReload = new System.Windows.Forms.Button();
|
this.btnReload = new System.Windows.Forms.Button();
|
||||||
this.btnOpenFolder = new System.Windows.Forms.Button();
|
this.btnOpenFolder = new System.Windows.Forms.Button();
|
||||||
this.flowLayoutPlugins = new TweetDuck.Plugins.Controls.PluginListFlowLayout();
|
this.flowLayoutPlugins = new PluginListFlowLayout();
|
||||||
this.timerLayout = new System.Windows.Forms.Timer(this.components);
|
this.timerLayout = new System.Windows.Forms.Timer(this.components);
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
@ -117,7 +119,7 @@ private void InitializeComponent() {
|
|||||||
private System.Windows.Forms.Button btnClose;
|
private System.Windows.Forms.Button btnClose;
|
||||||
private System.Windows.Forms.Button btnReload;
|
private System.Windows.Forms.Button btnReload;
|
||||||
private System.Windows.Forms.Button btnOpenFolder;
|
private System.Windows.Forms.Button btnOpenFolder;
|
||||||
private Plugins.Controls.PluginListFlowLayout flowLayoutPlugins;
|
private PluginListFlowLayout flowLayoutPlugins;
|
||||||
private System.Windows.Forms.Timer timerLayout;
|
private System.Windows.Forms.Timer timerLayout;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,6 @@
|
|||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using TweetDuck.Configuration;
|
using TweetDuck.Configuration;
|
||||||
using TweetDuck.Plugins;
|
using TweetDuck.Plugins;
|
||||||
using TweetDuck.Plugins.Controls;
|
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
|
|
||||||
namespace TweetDuck.Core.Other{
|
namespace TweetDuck.Core.Other{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
namespace TweetDuck.Plugins.Controls {
|
namespace TweetDuck.Plugins {
|
||||||
partial class PluginControl {
|
partial class PluginControl {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required designer variable.
|
/// Required designer variable.
|
@ -6,7 +6,7 @@
|
|||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
using TweetLib.Core.Features.Plugins.Enums;
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
|
|
||||||
namespace TweetDuck.Plugins.Controls{
|
namespace TweetDuck.Plugins{
|
||||||
sealed partial class PluginControl : UserControl{
|
sealed partial class PluginControl : UserControl{
|
||||||
private readonly PluginManager pluginManager;
|
private readonly PluginManager pluginManager;
|
||||||
private readonly Plugin plugin;
|
private readonly Plugin plugin;
|
||||||
@ -56,13 +56,13 @@ private void timerLayout_Tick(object sender, EventArgs e){
|
|||||||
private void panelDescription_Resize(object sender, EventArgs e){
|
private void panelDescription_Resize(object sender, EventArgs e){
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
|
|
||||||
int maxWidth = panelDescription.Width-(panelDescription.VerticalScroll.Visible ? SystemInformation.VerticalScrollBarWidth : 0);
|
int maxWidth = panelDescription.Width - (panelDescription.VerticalScroll.Visible ? SystemInformation.VerticalScrollBarWidth : 0);
|
||||||
labelDescription.MaximumSize = new Size(maxWidth, int.MaxValue);
|
labelDescription.MaximumSize = new Size(maxWidth, int.MaxValue);
|
||||||
|
|
||||||
Font font = labelDescription.Font;
|
Font font = labelDescription.Font;
|
||||||
int descriptionLines = TextRenderer.MeasureText(labelDescription.Text, font, new Size(maxWidth, int.MaxValue), TextFormatFlags.WordBreak).Height/(font.Height-1);
|
int descriptionLines = TextRenderer.MeasureText(labelDescription.Text, font, new Size(maxWidth, int.MaxValue), TextFormatFlags.WordBreak).Height / (font.Height - 1);
|
||||||
|
|
||||||
int requiredLines = Math.Max(descriptionLines, 1+(string.IsNullOrEmpty(labelVersion.Text) ? 0 : 1)+(isConfigurable ? 1 : 0));
|
int requiredLines = Math.Max(descriptionLines, 1 + (string.IsNullOrEmpty(labelVersion.Text) ? 0 : 1) + (isConfigurable ? 1 : 0));
|
||||||
|
|
||||||
nextHeight = requiredLines switch{
|
nextHeight = requiredLines switch{
|
||||||
1 => MaximumSize.Height - 2 * (font.Height - 1),
|
1 => MaximumSize.Height - 2 * (font.Height - 1),
|
@ -1,7 +1,7 @@
|
|||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using TweetDuck.Core.Utils;
|
using TweetDuck.Core.Utils;
|
||||||
|
|
||||||
namespace TweetDuck.Plugins.Controls{
|
namespace TweetDuck.Plugins{
|
||||||
sealed class PluginListFlowLayout : FlowLayoutPanel{
|
sealed class PluginListFlowLayout : FlowLayoutPanel{
|
||||||
public PluginListFlowLayout(){
|
public PluginListFlowLayout(){
|
||||||
FlowDirection = FlowDirection.TopDown;
|
FlowDirection = FlowDirection.TopDown;
|
@ -5,6 +5,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using TweetDuck.Core.Controls;
|
||||||
using TweetDuck.Core.Utils;
|
using TweetDuck.Core.Utils;
|
||||||
using TweetDuck.Resources;
|
using TweetDuck.Resources;
|
||||||
using TweetLib.Core.Data;
|
using TweetLib.Core.Data;
|
||||||
@ -14,21 +15,21 @@
|
|||||||
using TweetLib.Core.Features.Plugins.Events;
|
using TweetLib.Core.Features.Plugins.Events;
|
||||||
|
|
||||||
namespace TweetDuck.Plugins{
|
namespace TweetDuck.Plugins{
|
||||||
sealed class PluginManager{
|
sealed class PluginManager : IPluginManager{
|
||||||
private static readonly IReadOnlyDictionary<PluginEnvironment, string> PluginSetupScriptNames = PluginEnvironmentExtensions.Map(null, "plugins.browser.js", "plugins.notification.js");
|
private const string SetupScriptPrefix = "plugins.";
|
||||||
|
|
||||||
public string PathOfficialPlugins => Path.Combine(rootPath, "official");
|
public string PathCustomPlugins => Path.Combine(pluginFolder, PluginGroup.Custom.GetSubFolder());
|
||||||
public string PathCustomPlugins => Path.Combine(rootPath, "user");
|
|
||||||
|
|
||||||
public IEnumerable<Plugin> Plugins => plugins;
|
public IEnumerable<Plugin> Plugins => plugins;
|
||||||
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
|
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
|
||||||
|
|
||||||
public IPluginConfig Config { get; }
|
public IPluginConfig Config { get; }
|
||||||
|
|
||||||
public event EventHandler<PluginErrorEventArgs> Reloaded;
|
public event EventHandler<PluginErrorEventArgs> Reloaded;
|
||||||
public event EventHandler<PluginErrorEventArgs> Executed;
|
public event EventHandler<PluginErrorEventArgs> Executed;
|
||||||
|
|
||||||
private readonly string rootPath;
|
private readonly string pluginFolder;
|
||||||
|
private readonly string pluginDataFolder;
|
||||||
|
|
||||||
private readonly Control sync;
|
private readonly Control sync;
|
||||||
private readonly PluginBridge bridge;
|
private readonly PluginBridge bridge;
|
||||||
@ -39,11 +40,12 @@ sealed class PluginManager{
|
|||||||
|
|
||||||
private IWebBrowser mainBrowser;
|
private IWebBrowser mainBrowser;
|
||||||
|
|
||||||
public PluginManager(Control sync, IPluginConfig config, string rootPath){
|
public PluginManager(Control sync, IPluginConfig config, string pluginFolder, string pluginDataFolder){
|
||||||
this.Config = config;
|
this.Config = config;
|
||||||
this.Config.PluginChangedState += Config_PluginChangedState;
|
this.Config.PluginChangedState += Config_PluginChangedState;
|
||||||
|
|
||||||
this.rootPath = rootPath;
|
this.pluginFolder = pluginFolder;
|
||||||
|
this.pluginDataFolder = pluginDataFolder;
|
||||||
|
|
||||||
this.sync = sync;
|
this.sync = sync;
|
||||||
this.bridge = new PluginBridge(this);
|
this.bridge = new PluginBridge(this);
|
||||||
@ -87,10 +89,10 @@ public void ConfigurePlugin(Plugin plugin){
|
|||||||
}
|
}
|
||||||
else if (plugin.HasConfig){
|
else if (plugin.HasConfig){
|
||||||
if (File.Exists(plugin.ConfigPath)){
|
if (File.Exists(plugin.ConfigPath)){
|
||||||
using(Process.Start("explorer.exe", "/select,\""+plugin.ConfigPath.Replace('/', '\\')+"\"")){}
|
using(Process.Start("explorer.exe", "/select,\"" + plugin.ConfigPath.Replace('/', '\\') + "\"")){}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
using(Process.Start("explorer.exe", '"'+plugin.GetPluginFolder(PluginFolder.Data).Replace('/', '\\')+'"')){}
|
using(Process.Start("explorer.exe", '"' + plugin.GetPluginFolder(PluginFolder.Data).Replace('/', '\\') + '"')){}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +111,7 @@ public int GetTokenFromPlugin(Plugin plugin){
|
|||||||
}while(tokens.ContainsKey(token) && --attempts >= 0);
|
}while(tokens.ContainsKey(token) && --attempts >= 0);
|
||||||
|
|
||||||
if (attempts < 0){
|
if (attempts < 0){
|
||||||
token = -tokens.Count-1;
|
token = -tokens.Count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens[token] = plugin;
|
tokens[token] = plugin;
|
||||||
@ -124,42 +126,22 @@ public void Reload(){
|
|||||||
plugins.Clear();
|
plugins.Clear();
|
||||||
tokens.Clear();
|
tokens.Clear();
|
||||||
|
|
||||||
List<string> loadErrors = new List<string>(2);
|
List<string> loadErrors = new List<string>(1);
|
||||||
|
|
||||||
IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
|
foreach(var result in PluginGroupExtensions.Values.SelectMany(group => PluginLoader.AllInFolder(pluginFolder, pluginDataFolder, group))){
|
||||||
if (!Directory.Exists(path)){
|
if (result.HasValue){
|
||||||
yield break;
|
plugins.Add(result.Value);
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
|
loadErrors.Add(result.Exception.Message);
|
||||||
string name = Path.GetFileName(fullDir);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(name)){
|
|
||||||
loadErrors.Add($"{group.GetIdentifierPrefix()}(?): Could not extract directory name from path: {fullDir}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin plugin;
|
|
||||||
|
|
||||||
try{
|
|
||||||
plugin = PluginLoader.FromFolder(name, fullDir, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name), group);
|
|
||||||
}catch(Exception e){
|
|
||||||
loadErrors.Add($"{group.GetIdentifierPrefix()}{name}: {e.Message}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return plugin;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.UnionWith(LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official));
|
|
||||||
plugins.UnionWith(LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom));
|
|
||||||
|
|
||||||
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
|
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecutePlugins(IFrame frame, PluginEnvironment environment){
|
private void ExecutePlugins(IFrame frame, PluginEnvironment environment){
|
||||||
if (!HasAnyPlugin(environment) || !ScriptLoader.ExecuteFile(frame, PluginSetupScriptNames[environment], sync)){
|
if (!HasAnyPlugin(environment) || !ScriptLoader.ExecuteFile(frame, SetupScriptPrefix + environment.GetPluginScriptFile(), sync)){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,11 +165,11 @@ private void ExecutePlugins(IFrame frame, PluginEnvironment environment){
|
|||||||
try{
|
try{
|
||||||
script = File.ReadAllText(path);
|
script = File.ReadAllText(path);
|
||||||
}catch(Exception e){
|
}catch(Exception e){
|
||||||
failedPlugins.Add(plugin.Identifier+" ("+Path.GetFileName(path)+"): "+e.Message);
|
failedPlugins.Add($"{plugin.Identifier} ({Path.GetFileName(path)}): {e.Message}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, GetTokenFromPlugin(plugin), environment), "plugin:"+plugin);
|
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, GetTokenFromPlugin(plugin), environment), $"plugin:{plugin}");
|
||||||
}
|
}
|
||||||
|
|
||||||
sync.InvokeAsyncSafe(() => {
|
sync.InvokeAsyncSafe(() => {
|
||||||
|
@ -145,7 +145,7 @@ public static void HotSwap(){
|
|||||||
|
|
||||||
// ReSharper disable PossibleNullReferenceException
|
// ReSharper disable PossibleNullReferenceException
|
||||||
object instPluginManager = typeFormBrowser.GetField("plugins", flagsInstance).GetValue(FormManager.TryFind<FormBrowser>());
|
object instPluginManager = typeFormBrowser.GetField("plugins", flagsInstance).GetValue(FormManager.TryFind<FormBrowser>());
|
||||||
typePluginManager.GetField("rootPath", flagsInstance).SetValue(instPluginManager, newPluginRoot);
|
typePluginManager.GetField("pluginFolder", flagsInstance).SetValue(instPluginManager, newPluginRoot);
|
||||||
|
|
||||||
Debug.WriteLine("Reloading hot swapped plugins...");
|
Debug.WriteLine("Reloading hot swapped plugins...");
|
||||||
((PluginManager)instPluginManager).Reload();
|
((PluginManager)instPluginManager).Reload();
|
||||||
|
@ -238,16 +238,15 @@
|
|||||||
<Compile Include="Core\Other\FormSettings.Designer.cs">
|
<Compile Include="Core\Other\FormSettings.Designer.cs">
|
||||||
<DependentUpon>FormSettings.cs</DependentUpon>
|
<DependentUpon>FormSettings.cs</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Plugins\Controls\PluginControl.cs">
|
<Compile Include="Plugins\PluginControl.cs">
|
||||||
<SubType>UserControl</SubType>
|
<SubType>UserControl</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Plugins\Controls\PluginControl.Designer.cs">
|
<Compile Include="Plugins\PluginControl.Designer.cs">
|
||||||
<DependentUpon>PluginControl.cs</DependentUpon>
|
<DependentUpon>PluginControl.cs</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Plugins\Controls\PluginListFlowLayout.cs">
|
<Compile Include="Plugins\PluginListFlowLayout.cs">
|
||||||
<SubType>Component</SubType>
|
<SubType>Component</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Plugins\PluginBridge.cs" />
|
|
||||||
<Compile Include="Configuration\PluginConfig.cs" />
|
<Compile Include="Configuration\PluginConfig.cs" />
|
||||||
<Compile Include="Plugins\PluginManager.cs" />
|
<Compile Include="Plugins\PluginManager.cs" />
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
<Compile Include="Properties\Resources.Designer.cs">
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins.Enums{
|
namespace TweetLib.Core.Features.Plugins.Enums{
|
||||||
[Flags]
|
[Flags]
|
||||||
@ -13,12 +10,10 @@ public enum PluginEnvironment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class PluginEnvironmentExtensions{
|
public static class PluginEnvironmentExtensions{
|
||||||
public static IEnumerable<PluginEnvironment> Values{
|
public static IEnumerable<PluginEnvironment> Values { get; } = new PluginEnvironment[]{
|
||||||
get{
|
PluginEnvironment.Browser,
|
||||||
yield return PluginEnvironment.Browser;
|
PluginEnvironment.Notification
|
||||||
yield return PluginEnvironment.Notification;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
|
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
|
||||||
return environment == PluginEnvironment.Browser;
|
return environment == PluginEnvironment.Browser;
|
||||||
@ -28,7 +23,7 @@ public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
|
|||||||
return environment switch{
|
return environment switch{
|
||||||
PluginEnvironment.Browser => "browser.js",
|
PluginEnvironment.Browser => "browser.js",
|
||||||
PluginEnvironment.Notification => "notification.js",
|
PluginEnvironment.Notification => "notification.js",
|
||||||
_ => null
|
_ => throw new InvalidOperationException($"Invalid plugin environment: {environment}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,50 +34,5 @@ public static string GetPluginScriptVariables(this PluginEnvironment environment
|
|||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyDictionary<PluginEnvironment, T> Map<T>(T forNone, T forBrowser, T forNotification){
|
|
||||||
return new PluginEnvironmentDictionary<T>(forNone, forBrowser, forNotification);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")]
|
|
||||||
private sealed class PluginEnvironmentDictionary<T> : IReadOnlyDictionary<PluginEnvironment, T>{
|
|
||||||
private const int TotalKeys = 3;
|
|
||||||
|
|
||||||
public IEnumerable<PluginEnvironment> Keys => Enum.GetValues(typeof(PluginEnvironment)).Cast<PluginEnvironment>();
|
|
||||||
public IEnumerable<T> Values => data;
|
|
||||||
public int Count => TotalKeys;
|
|
||||||
|
|
||||||
public T this[PluginEnvironment key] => data[(int)key];
|
|
||||||
|
|
||||||
private readonly T[] data;
|
|
||||||
|
|
||||||
public PluginEnvironmentDictionary(T forNone, T forBrowser, T forNotification){
|
|
||||||
this.data = new T[TotalKeys];
|
|
||||||
this.data[(int)PluginEnvironment.None] = forNone;
|
|
||||||
this.data[(int)PluginEnvironment.Browser] = forBrowser;
|
|
||||||
this.data[(int)PluginEnvironment.Notification] = forNotification;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ContainsKey(PluginEnvironment key){
|
|
||||||
return key >= 0 && (int)key < TotalKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetValue(PluginEnvironment key, out T value){
|
|
||||||
if (ContainsKey(key)){
|
|
||||||
value = this[key];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<PluginEnvironment, T>> GetEnumerator(){
|
|
||||||
return Keys.Select(key => new KeyValuePair<PluginEnvironment, T>(key, this[key])).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,25 @@
|
|||||||
namespace TweetLib.Core.Features.Plugins.Enums{
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace TweetLib.Core.Features.Plugins.Enums{
|
||||||
public enum PluginGroup{
|
public enum PluginGroup{
|
||||||
Official, Custom
|
Official, Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PluginGroupExtensions{
|
public static class PluginGroupExtensions{
|
||||||
|
public static IEnumerable<PluginGroup> Values { get; } = new PluginGroup[]{
|
||||||
|
PluginGroup.Official,
|
||||||
|
PluginGroup.Custom
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetSubFolder(this PluginGroup group){
|
||||||
|
return group switch{
|
||||||
|
PluginGroup.Official => "official",
|
||||||
|
PluginGroup.Custom => "user",
|
||||||
|
_ => throw new InvalidOperationException($"Invalid plugin group: {group}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetIdentifierPrefix(this PluginGroup group){
|
public static string GetIdentifierPrefix(this PluginGroup group){
|
||||||
return group switch{
|
return group switch{
|
||||||
PluginGroup.Official => "official/",
|
PluginGroup.Official => "official/",
|
||||||
|
14
lib/TweetLib.Core/Features/Plugins/IPluginManager.cs
Normal file
14
lib/TweetLib.Core/Features/Plugins/IPluginManager.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -5,26 +5,21 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using TweetLib.Core.Collections;
|
using TweetLib.Core.Collections;
|
||||||
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;
|
||||||
using TweetLib.Core.Features.Plugins.Events;
|
using TweetLib.Core.Features.Plugins.Events;
|
||||||
using TweetLib.Core.Utils;
|
using TweetLib.Core.Utils;
|
||||||
|
|
||||||
namespace TweetDuck.Plugins{
|
namespace TweetLib.Core.Features.Plugins{
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
sealed class PluginBridge{
|
public sealed class PluginBridge{
|
||||||
private static string SanitizeCacheKey(string key){
|
private readonly IPluginManager manager;
|
||||||
return key.Replace('\\', '/').Trim();
|
private readonly FileCache fileCache = new FileCache();
|
||||||
}
|
|
||||||
|
|
||||||
private readonly PluginManager manager;
|
|
||||||
private readonly TwoKeyDictionary<int, string, string> fileCache = new TwoKeyDictionary<int, string, string>(4, 2);
|
|
||||||
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;
|
public IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
|
||||||
public HashSet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
|
public ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
|
||||||
|
|
||||||
public PluginBridge(PluginManager manager){
|
public PluginBridge(IPluginManager manager){
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.manager.Reloaded += manager_Reloaded;
|
this.manager.Reloaded += manager_Reloaded;
|
||||||
this.manager.Config.PluginChangedState += Config_PluginChangedState;
|
this.manager.Config.PluginChangedState += Config_PluginChangedState;
|
||||||
@ -55,7 +50,7 @@ private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
|
|||||||
switch(folder){
|
switch(folder){
|
||||||
case PluginFolder.Data: throw new ArgumentException("File path has to be relative to the plugin data folder.");
|
case PluginFolder.Data: throw new ArgumentException("File path has to be relative to the plugin data folder.");
|
||||||
case PluginFolder.Root: throw new ArgumentException("File path has to be relative to the plugin root folder.");
|
case PluginFolder.Root: throw new ArgumentException("File path has to be relative to the plugin root folder.");
|
||||||
default: throw new ArgumentException("Invalid folder type "+folder+", this is a TweetDuck error.");
|
default: throw new ArgumentException($"Invalid folder type {folder}, this is a TweetDuck error.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@ -63,15 +58,15 @@ private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool readCached){
|
private string ReadFileUnsafe(int token, PluginFolder folder, string path, bool readCached){
|
||||||
cacheKey = SanitizeCacheKey(cacheKey);
|
string fullPath = GetFullPathOrThrow(token, folder, path);
|
||||||
|
|
||||||
if (readCached && fileCache.TryGetValue(token, cacheKey, out string cachedContents)){
|
if (readCached && fileCache.TryGetValue(token, folder, path, out string cachedContents)){
|
||||||
return cachedContents;
|
return cachedContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
try{
|
||||||
return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
|
return fileCache[token, folder, path] = File.ReadAllText(fullPath, Encoding.UTF8);
|
||||||
}catch(FileNotFoundException){
|
}catch(FileNotFoundException){
|
||||||
throw new FileNotFoundException("File not found.");
|
throw new FileNotFoundException("File not found.");
|
||||||
}catch(DirectoryNotFoundException){
|
}catch(DirectoryNotFoundException){
|
||||||
@ -86,17 +81,17 @@ public void WriteFile(int token, string path, string contents){
|
|||||||
|
|
||||||
FileUtils.CreateDirectoryForFile(fullPath);
|
FileUtils.CreateDirectoryForFile(fullPath);
|
||||||
File.WriteAllText(fullPath, contents, Encoding.UTF8);
|
File.WriteAllText(fullPath, contents, Encoding.UTF8);
|
||||||
fileCache[token, SanitizeCacheKey(path)] = contents;
|
fileCache[token, PluginFolder.Data, path] = contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReadFile(int token, string path, bool cache){
|
public string ReadFile(int token, string path, bool cache){
|
||||||
return ReadFileUnsafe(token, path, GetFullPathOrThrow(token, PluginFolder.Data, path), cache);
|
return ReadFileUnsafe(token, PluginFolder.Data, path, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteFile(int token, string path){
|
public void DeleteFile(int token, string path){
|
||||||
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
|
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
|
||||||
|
|
||||||
fileCache.Remove(token, SanitizeCacheKey(path));
|
fileCache.Remove(token, PluginFolder.Data, path);
|
||||||
File.Delete(fullPath);
|
File.Delete(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +100,7 @@ public bool CheckFileExists(int token, string path){
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string ReadFileRoot(int token, string path){
|
public string ReadFileRoot(int token, string path){
|
||||||
return ReadFileUnsafe(token, "root*"+path, GetFullPathOrThrow(token, PluginFolder.Root, path), true);
|
return ReadFileUnsafe(token, PluginFolder.Root, path, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckFileExistsRoot(int token, string path){
|
public bool CheckFileExistsRoot(int token, string path){
|
||||||
@ -127,5 +122,39 @@ public void SetConfigurable(int token){
|
|||||||
WithConfigureFunction.Add(plugin);
|
WithConfigureFunction.Add(plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class FileCache{
|
||||||
|
private readonly TwoKeyDictionary<int, string, string> cache = new TwoKeyDictionary<int, string, string>(4, 2);
|
||||||
|
|
||||||
|
public string this[int token, PluginFolder folder, string path]{
|
||||||
|
set => cache[token, Key(folder, path)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear(){
|
||||||
|
cache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValue(int token, PluginFolder folder, string path, out string contents){
|
||||||
|
return cache.TryGetValue(token, Key(folder, path), out contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(int token){
|
||||||
|
cache.Remove(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(int token, PluginFolder folder, string path){
|
||||||
|
cache.Remove(token, Key(folder, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Key(PluginFolder folder, string path){
|
||||||
|
string prefix = folder switch{
|
||||||
|
PluginFolder.Root => "root/",
|
||||||
|
PluginFolder.Data => "data/",
|
||||||
|
_ => throw new InvalidOperationException($"Invalid folder type {folder}, this is a TweetDuck error.")
|
||||||
|
};
|
||||||
|
|
||||||
|
return prefix + path.Replace('\\', '/').Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,43 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using TweetLib.Core.Data;
|
||||||
using TweetLib.Core.Features.Plugins.Enums;
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins{
|
namespace TweetLib.Core.Features.Plugins{
|
||||||
public static class PluginLoader{
|
public static class PluginLoader{
|
||||||
private static readonly string[] EndTag = { "[END]" };
|
private static readonly string[] EndTag = { "[END]" };
|
||||||
|
|
||||||
|
public static IEnumerable<Result<Plugin>> AllInFolder(string pluginFolder, string pluginDataFolder, PluginGroup group){
|
||||||
|
string path = Path.Combine(pluginFolder, group.GetSubFolder());
|
||||||
|
|
||||||
|
if (!Directory.Exists(path)){
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
|
||||||
|
string name = Path.GetFileName(fullDir);
|
||||||
|
string prefix = group.GetIdentifierPrefix();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(name)){
|
||||||
|
yield return new Result<Plugin>(new DirectoryNotFoundException($"{prefix}(?): Could not extract directory name from path: {fullDir}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Plugin> result;
|
||||||
|
|
||||||
|
try{
|
||||||
|
result = new Result<Plugin>(FromFolder(name, fullDir, Path.Combine(pluginDataFolder, prefix, name), group));
|
||||||
|
}catch(Exception e){
|
||||||
|
result = new Result<Plugin>(new Exception($"{prefix}{name}: {e.Message}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group){
|
public static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group){
|
||||||
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user