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

Continue refactoring and moving plugin code

This commit is contained in:
chylex 2019-05-27 19:46:39 +02:00
parent 108a0fefc3
commit 50bd526025
15 changed files with 155 additions and 134 deletions

View File

@ -65,7 +65,7 @@ public FormBrowser(){
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.Executed += plugins_Executed;
this.plugins.Reload();

View File

@ -82,7 +82,7 @@ public static AnalyticsReport Create(AnalyticsFile file, ExternalInfo info, Plug
{ "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
0,
{ "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,
{ "Theme" , Dict(editLayoutDesign, "_theme", "light/def") },
{ "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") },

View File

@ -1,4 +1,6 @@
namespace TweetDuck.Core.Other {
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other {
partial class FormPlugins {
/// <summary>
/// Required designer variable.
@ -27,7 +29,7 @@ private void InitializeComponent() {
this.btnClose = new System.Windows.Forms.Button();
this.btnReload = 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.SuspendLayout();
//
@ -117,7 +119,7 @@ private void InitializeComponent() {
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.Button btnReload;
private System.Windows.Forms.Button btnOpenFolder;
private Plugins.Controls.PluginListFlowLayout flowLayoutPlugins;
private PluginListFlowLayout flowLayoutPlugins;
private System.Windows.Forms.Timer timerLayout;
}
}

View File

@ -5,7 +5,6 @@
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Controls;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Other{

View File

@ -1,4 +1,4 @@
namespace TweetDuck.Plugins.Controls {
namespace TweetDuck.Plugins {
partial class PluginControl {
/// <summary>
/// Required designer variable.

View File

@ -6,7 +6,7 @@
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Plugins.Controls{
namespace TweetDuck.Plugins{
sealed partial class PluginControl : UserControl{
private readonly PluginManager pluginManager;
private readonly Plugin plugin;
@ -56,13 +56,13 @@ private void timerLayout_Tick(object sender, EventArgs e){
private void panelDescription_Resize(object sender, EventArgs e){
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);
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{
1 => MaximumSize.Height - 2 * (font.Height - 1),

View File

@ -1,7 +1,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Utils;
namespace TweetDuck.Plugins.Controls{
namespace TweetDuck.Plugins{
sealed class PluginListFlowLayout : FlowLayoutPanel{
public PluginListFlowLayout(){
FlowDirection = FlowDirection.TopDown;

View File

@ -5,6 +5,7 @@
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;
@ -14,21 +15,21 @@
using TweetLib.Core.Features.Plugins.Events;
namespace TweetDuck.Plugins{
sealed class PluginManager{
private static readonly IReadOnlyDictionary<PluginEnvironment, string> PluginSetupScriptNames = PluginEnvironmentExtensions.Map(null, "plugins.browser.js", "plugins.notification.js");
sealed class PluginManager : IPluginManager{
private const string SetupScriptPrefix = "plugins.";
public string PathOfficialPlugins => Path.Combine(rootPath, "official");
public string PathCustomPlugins => Path.Combine(rootPath, "user");
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 rootPath;
private readonly string pluginFolder;
private readonly string pluginDataFolder;
private readonly Control sync;
private readonly PluginBridge bridge;
@ -39,11 +40,12 @@ sealed class PluginManager{
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.PluginChangedState += Config_PluginChangedState;
this.rootPath = rootPath;
this.pluginFolder = pluginFolder;
this.pluginDataFolder = pluginDataFolder;
this.sync = sync;
this.bridge = new PluginBridge(this);
@ -87,10 +89,10 @@ public void ConfigurePlugin(Plugin plugin){
}
else if (plugin.HasConfig){
if (File.Exists(plugin.ConfigPath)){
using(Process.Start("explorer.exe", "/select,\""+plugin.ConfigPath.Replace('/', '\\')+"\"")){}
using(Process.Start("explorer.exe", "/select,\"" + plugin.ConfigPath.Replace('/', '\\') + "\"")){}
}
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);
if (attempts < 0){
token = -tokens.Count-1;
token = -tokens.Count - 1;
}
tokens[token] = plugin;
@ -124,42 +126,22 @@ public void Reload(){
plugins.Clear();
tokens.Clear();
List<string> loadErrors = new List<string>(2);
List<string> loadErrors = new List<string>(1);
IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
if (!Directory.Exists(path)){
yield break;
foreach(var result in PluginGroupExtensions.Values.SelectMany(group => PluginLoader.AllInFolder(pluginFolder, pluginDataFolder, group))){
if (result.HasValue){
plugins.Add(result.Value);
}
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
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;
else{
loadErrors.Add(result.Exception.Message);
}
}
plugins.UnionWith(LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official));
plugins.UnionWith(LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom));
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
}
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;
}
@ -183,11 +165,11 @@ private void ExecutePlugins(IFrame frame, PluginEnvironment environment){
try{
script = File.ReadAllText(path);
}catch(Exception e){
failedPlugins.Add(plugin.Identifier+" ("+Path.GetFileName(path)+"): "+e.Message);
failedPlugins.Add($"{plugin.Identifier} ({Path.GetFileName(path)}): {e.Message}");
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(() => {

View File

@ -145,7 +145,7 @@ public static void HotSwap(){
// ReSharper disable PossibleNullReferenceException
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...");
((PluginManager)instPluginManager).Reload();

View File

@ -238,16 +238,15 @@
<Compile Include="Core\Other\FormSettings.Designer.cs">
<DependentUpon>FormSettings.cs</DependentUpon>
</Compile>
<Compile Include="Plugins\Controls\PluginControl.cs">
<Compile Include="Plugins\PluginControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Plugins\Controls\PluginControl.Designer.cs">
<Compile Include="Plugins\PluginControl.Designer.cs">
<DependentUpon>PluginControl.cs</DependentUpon>
</Compile>
<Compile Include="Plugins\Controls\PluginListFlowLayout.cs">
<Compile Include="Plugins\PluginListFlowLayout.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Plugins\PluginBridge.cs" />
<Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Plugins\PluginManager.cs" />
<Compile Include="Properties\Resources.Designer.cs">

View File

@ -1,8 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace TweetLib.Core.Features.Plugins.Enums{
[Flags]
@ -13,12 +10,10 @@ public enum PluginEnvironment{
}
public static class PluginEnvironmentExtensions{
public static IEnumerable<PluginEnvironment> Values{
get{
yield return PluginEnvironment.Browser;
yield return PluginEnvironment.Notification;
}
}
public static IEnumerable<PluginEnvironment> Values { get; } = new PluginEnvironment[]{
PluginEnvironment.Browser,
PluginEnvironment.Notification
};
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
return environment == PluginEnvironment.Browser;
@ -28,7 +23,7 @@ public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
return environment switch{
PluginEnvironment.Browser => "browser.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
};
}
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();
}
}
}

View File

@ -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{
Official, Custom
}
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){
return group switch{
PluginGroup.Official => "official/",

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

View File

@ -5,26 +5,21 @@
using System.Text;
using TweetLib.Core.Collections;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Utils;
namespace TweetDuck.Plugins{
namespace TweetLib.Core.Features.Plugins{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
sealed class PluginBridge{
private static string SanitizeCacheKey(string key){
return key.Replace('\\', '/').Trim();
}
private readonly PluginManager manager;
private readonly TwoKeyDictionary<int, string, string> fileCache = new TwoKeyDictionary<int, string, string>(4, 2);
public sealed class PluginBridge{
private readonly IPluginManager manager;
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 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.Reloaded += manager_Reloaded;
this.manager.Config.PluginChangedState += Config_PluginChangedState;
@ -55,7 +50,7 @@ private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
switch(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.");
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{
@ -63,15 +58,15 @@ private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
}
}
private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool readCached){
cacheKey = SanitizeCacheKey(cacheKey);
if (readCached && fileCache.TryGetValue(token, cacheKey, out string cachedContents)){
private string ReadFileUnsafe(int token, PluginFolder folder, string path, bool readCached){
string fullPath = GetFullPathOrThrow(token, folder, path);
if (readCached && fileCache.TryGetValue(token, folder, path, out string cachedContents)){
return cachedContents;
}
try{
return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
return fileCache[token, folder, path] = File.ReadAllText(fullPath, Encoding.UTF8);
}catch(FileNotFoundException){
throw new FileNotFoundException("File not found.");
}catch(DirectoryNotFoundException){
@ -86,17 +81,17 @@ public void WriteFile(int token, string path, string contents){
FileUtils.CreateDirectoryForFile(fullPath);
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){
return ReadFileUnsafe(token, path, GetFullPathOrThrow(token, PluginFolder.Data, path), cache);
return ReadFileUnsafe(token, PluginFolder.Data, path, cache);
}
public void DeleteFile(int token, string path){
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
fileCache.Remove(token, SanitizeCacheKey(path));
fileCache.Remove(token, PluginFolder.Data, path);
File.Delete(fullPath);
}
@ -105,7 +100,7 @@ public bool CheckFileExists(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){
@ -127,5 +122,39 @@ public void SetConfigurable(int token){
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();
}
}
}
}

View File

@ -1,13 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetLib.Core.Features.Plugins{
public static class PluginLoader{
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){
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);