1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-09-14 19:32:10 +02:00

Compare commits

..

18 Commits
1.18 ... 1.18.1

Author SHA1 Message Date
4fdf7fc958 Release 1.18.1 2019-07-13 19:50:49 +02:00
42a5e72f19 Revert README change & lock Inno Setup to version 5.6.1 2019-07-13 19:39:08 +02:00
f7359ebc8a Update README with instructions for fixing Inno Download Plugin 2019-07-13 18:44:02 +02:00
f395ac53dc Fix wrong colors in dropdown menus w/ black theme 2019-07-13 18:22:38 +02:00
1113e0b559 Fix new image url parser not checking if an extension already exists 2019-07-13 18:16:11 +02:00
5e3bd31862 Delete corrupted downloads after an error 2019-07-13 18:10:16 +02:00
11d978dad1 Fix GIF thumbnails not loading after Twitter changed image urls
Closes #271
2019-07-13 17:51:16 +02:00
f7961024d7 Enable popup for linking another account
Closes #269
2019-07-13 06:17:30 +02:00
72973a8707 Restore smooth scrolling in columns
Fixes #251
2019-07-13 06:07:01 +02:00
68254f48d5 Fix TweetDeck bug with broken DM image previews
References #271
2019-07-13 00:48:53 +02:00
eac4f30c50 Support new image urls & fix missing filename features w/o Best Image Quality
Fixes #270
2019-07-13 00:40:27 +02:00
25680fa980 Add StringUtils.SplitInTwo & use it in RequestHandlerBase 2019-07-12 22:17:29 +02:00
ff5e1da14d Fix wrong 'X columns on screen' width calculation after a TweetDeck update 2019-06-03 11:30:14 +02:00
95afff7879 Update F# compiler location 2019-06-03 10:33:57 +02:00
50bd526025 Continue refactoring and moving plugin code 2019-05-27 19:46:39 +02:00
108a0fefc3 Fix PluginManager crashing after error(s) during plugin execution 2019-05-27 19:38:53 +02:00
dd8c5d27be Update code to use C# 8 switch expression 2019-05-27 16:04:08 +02:00
b2937bc776 Fix broken image upload dialog in new composer 2019-05-27 12:37:30 +02:00
39 changed files with 453 additions and 337 deletions

View File

@@ -3,9 +3,9 @@ using System.Drawing;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification; using TweetDuck.Core.Notification;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Data; using TweetDuck.Data;
using TweetLib.Core.Features.Configuration; using TweetLib.Core.Features.Configuration;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Configuration{ namespace TweetDuck.Configuration{
sealed class UserConfig : BaseConfig{ sealed class UserConfig : BaseConfig{
@@ -80,7 +80,7 @@ namespace TweetDuck.Configuration{
public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty; public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty; public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty;
public TwitterUtils.ImageQuality TwitterImageQuality => BestImageQuality ? TwitterUtils.ImageQuality.Orig : TwitterUtils.ImageQuality.Default; public ImageQuality TwitterImageQuality => BestImageQuality ? ImageQuality.Best : ImageQuality.Default;
public string NotificationSoundPath{ public string NotificationSoundPath{
get => _notificationSoundPath ?? string.Empty; get => _notificationSoundPath ?? string.Empty;

View File

@@ -119,14 +119,12 @@ namespace TweetDuck.Core.Bridge{
} }
public void Alert(string type, string contents){ public void Alert(string type, string contents){
MessageBoxIcon icon; MessageBoxIcon icon = type switch{
"error" => MessageBoxIcon.Error,
switch(type){ "warning" => MessageBoxIcon.Warning,
case "error": icon = MessageBoxIcon.Error; break; "info" => MessageBoxIcon.Information,
case "warning": icon = MessageBoxIcon.Warning; break; _ => MessageBoxIcon.None
case "info": icon = MessageBoxIcon.Information; break; };
default: icon = MessageBoxIcon.None; break;
}
FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK); FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
} }

View File

@@ -65,7 +65,7 @@ namespace TweetDuck.Core{
Text = Program.BrandName; Text = Program.BrandName;
this.plugins = new PluginManager(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();

View File

@@ -12,13 +12,14 @@ using TweetDuck.Core.Notification;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics; using TweetDuck.Core.Other.Analytics;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils; using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{ namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{ abstract class ContextMenuBase : IContextMenuHandler{
protected static UserConfig Config => Program.Config.User; protected static UserConfig Config => Program.Config.User;
private static TwitterUtils.ImageQuality ImageQuality => Config.TwitterImageQuality; private static ImageQuality ImageQuality => Config.TwitterImageQuality;
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500; private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501; private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501;

View File

@@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
@@ -9,7 +8,7 @@ namespace TweetDuck.Core.Handling.General{
sealed class FileDialogHandler : IDialogHandler{ sealed class FileDialogHandler : IDialogHandler{
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback){ public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback){
if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple){ if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple){
string allFilters = string.Join(";", acceptFilters.Select(filter => "*"+filter)); string allFilters = string.Join(";", acceptFilters.SelectMany(ParseFileType).Where(filter => !string.IsNullOrEmpty(filter)).Select(filter => "*" + filter));
using(OpenFileDialog dialog = new OpenFileDialog{ using(OpenFileDialog dialog = new OpenFileDialog{
AutoUpgradeEnabled = true, AutoUpgradeEnabled = true,
@@ -19,8 +18,8 @@ namespace TweetDuck.Core.Handling.General{
Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*" Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*"
}){ }){
if (dialog.ShowDialog() == DialogResult.OK){ if (dialog.ShowDialog() == DialogResult.OK){
string ext = Path.GetExtension(dialog.FileName); string ext = Path.GetExtension(dialog.FileName)?.ToLower();
callback.Continue(acceptFilters.FindIndex(filter => filter.Equals(ext, StringComparison.OrdinalIgnoreCase)), dialog.FileNames.ToList()); callback.Continue(acceptFilters.FindIndex(filter => ParseFileType(filter).Contains(ext)), dialog.FileNames.ToList());
} }
else{ else{
callback.Cancel(); callback.Cancel();
@@ -36,5 +35,27 @@ namespace TweetDuck.Core.Handling.General{
return false; return false;
} }
} }
private static IEnumerable<string> ParseFileType(string type){
if (string.IsNullOrEmpty(type)){
return new string[0];
}
if (type[0] == '.'){
return new string[]{ type };
}
switch(type){
case "image/jpeg": return new string[]{ ".jpg", ".jpeg" };
case "image/png": return new string[]{ ".png" };
case "image/gif": return new string[]{ ".gif" };
case "image/webp": return new string[]{ ".webp" };
case "video/mp4": return new string[]{ ".mp4" };
case "video/quicktime": return new string[]{ ".mov", ".qt" };
}
System.Diagnostics.Debugger.Break();
return new string[0];
}
} }
} }

View File

@@ -12,15 +12,17 @@ namespace TweetDuck.Core.Handling.General{
int pipe = text.IndexOf('|'); int pipe = text.IndexOf('|');
if (pipe != -1){ if (pipe != -1){
switch(text.Substring(0, pipe)){ icon = text.Substring(0, pipe) switch{
case "error": icon = MessageBoxIcon.Error; break; "error" => MessageBoxIcon.Error,
case "warning": icon = MessageBoxIcon.Warning; break; "warning" => MessageBoxIcon.Warning,
case "info": icon = MessageBoxIcon.Information; break; "info" => MessageBoxIcon.Information,
case "question": icon = MessageBoxIcon.Question; break; "question" => MessageBoxIcon.Question,
default: return new FormMessage(caption, text, icon); _ => MessageBoxIcon.None
} };
text = text.Substring(pipe+1); if (icon != MessageBoxIcon.None){
text = text.Substring(pipe + 1);
}
} }
return new FormMessage(caption, text, icon); return new FormMessage(caption, text, icon);

View File

@@ -4,11 +4,15 @@ using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling.General{ namespace TweetDuck.Core.Handling.General{
sealed class LifeSpanHandler : ILifeSpanHandler{ sealed class LifeSpanHandler : ILifeSpanHandler{
private static bool IsPopupAllowed(string url){
return url.StartsWith("https://twitter.com/teams/authorize?");
}
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl){ public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl){
switch(targetDisposition){ switch(targetDisposition){
case WindowOpenDisposition.NewBackgroundTab: case WindowOpenDisposition.NewBackgroundTab:
case WindowOpenDisposition.NewForegroundTab: case WindowOpenDisposition.NewForegroundTab:
case WindowOpenDisposition.NewPopup: case WindowOpenDisposition.NewPopup when !IsPopupAllowed(targetUrl):
case WindowOpenDisposition.NewWindow: case WindowOpenDisposition.NewWindow:
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl)); browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
return true; return true;

View File

@@ -7,6 +7,7 @@ using CefSharp;
using CefSharp.Handler; using CefSharp.Handler;
using TweetDuck.Core.Handling.General; using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{ namespace TweetDuck.Core.Handling{
class RequestHandlerBase : DefaultRequestHandler{ class RequestHandlerBase : DefaultRequestHandler{
@@ -21,21 +22,13 @@ namespace TweetDuck.Core.Handling{
TweetDeckHashes.Clear(); TweetDeckHashes.Clear();
foreach(string rule in rules.Replace(" ", "").ToLower().Split(',')){ foreach(string rule in rules.Replace(" ", "").ToLower().Split(',')){
string[] split = rule.Split('='); var (key, hash) = StringUtils.SplitInTwo(rule, '=') ?? throw new ArgumentException("A rule must have one '=' character: " + rule);
if (split.Length == 2){
string key = split[0];
string hash = split[1];
if (hash.All(chr => char.IsDigit(chr) || (chr >= 'a' && chr <= 'f'))){ if (hash.All(chr => char.IsDigit(chr) || (chr >= 'a' && chr <= 'f'))){
TweetDeckHashes.Add(key, hash); TweetDeckHashes.Add(key, hash);
} }
else{ else{
throw new ArgumentException("Invalid hash characters: "+rule); throw new ArgumentException("Invalid hash characters: " + rule);
}
}
else{
throw new ArgumentException("A rule must have exactly one '=' character: "+rule);
} }
} }
} }

View File

@@ -14,15 +14,13 @@ namespace TweetDuck.Core.Notification{
protected static UserConfig Config => Program.Config.User; protected static UserConfig Config => Program.Config.User;
protected static int FontSizeLevel{ protected static int FontSizeLevel{
get{ get => TweetDeckBridge.FontSize switch{
switch(TweetDeckBridge.FontSize){ "largest" => 4,
case "largest": return 4; "large" => 3,
case "large": return 3; "small" => 1,
case "small": return 1; "smallest" => 0,
case "smallest": return 0; _ => 2
default: return 2; };
}
}
} }
protected virtual Point PrimaryLocation{ protected virtual Point PrimaryLocation{

View File

@@ -44,27 +44,17 @@ namespace TweetDuck.Core.Notification{
} }
private int BaseClientWidth{ private int BaseClientWidth{
get{ get => Config.NotificationSize switch{
switch(Config.NotificationSize){ TweetNotification.Size.Custom => Config.CustomNotificationSize.Width,
default: _ => BrowserUtils.Scale(284, SizeScale * (1.0 + 0.05 * FontSizeLevel))
return BrowserUtils.Scale(284, SizeScale*(1.0+0.05*FontSizeLevel)); };
case TweetNotification.Size.Custom:
return Config.CustomNotificationSize.Width;
}
}
} }
private int BaseClientHeight{ private int BaseClientHeight{
get{ get => Config.NotificationSize switch{
switch(Config.NotificationSize){ TweetNotification.Size.Custom => Config.CustomNotificationSize.Height,
default: _ => BrowserUtils.Scale(122, SizeScale * (1.0 + 0.08 * FontSizeLevel))
return BrowserUtils.Scale(122, SizeScale*(1.0+0.08*FontSizeLevel)); };
case TweetNotification.Size.Custom:
return Config.CustomNotificationSize.Height;
}
}
} }
protected virtual string BodyClasses => IsCursorOverBrowser ? "td-notification td-hover" : "td-notification"; protected virtual string BodyClasses => IsCursorOverBrowser ? "td-notification td-hover" : "td-notification";
@@ -83,7 +73,7 @@ namespace TweetDuck.Core.Notification{
browser.LoadingStateChanged += Browser_LoadingStateChanged; browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd; browser.FrameLoadEnd += Browser_FrameLoadEnd;
plugins.Register(browser, PluginEnvironment.Notification, this); plugins.Register(browser, PluginEnvironment.Notification);
mouseHookDelegate = MouseHookProc; mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook(true); Disposed += (sender, args) => StopMouseHook(true);

View File

@@ -11,18 +11,16 @@ namespace TweetDuck.Core.Notification{
public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm"; public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
public static IResourceHandler CreateFileHandler(string path){ public static IResourceHandler CreateFileHandler(string path){
string mimeType; string mimeType = Path.GetExtension(path) switch{
".weba" => "audio/webm",
switch(Path.GetExtension(path)){ ".webm" => "audio/webm",
case ".weba": ".wav" => "audio/wav",
case ".webm": mimeType = "audio/webm"; break; ".ogg" => "audio/ogg",
case ".wav": mimeType = "audio/wav"; break; ".mp3" => "audio/mp3",
case ".ogg": mimeType = "audio/ogg"; break; ".flac" => "audio/flac",
case ".mp3": mimeType = "audio/mp3"; break; ".opus" => "audio/ogg; codecs=opus",
case ".flac": mimeType = "audio/flac"; break; _ => null
case ".opus": mimeType = "audio/ogg; codecs=opus"; break; };
default: mimeType = null; break;
}
try{ try{
return ResourceHandler.FromFilePath(path, mimeType); return ResourceHandler.FromFilePath(path, mimeType);

View File

@@ -82,7 +82,7 @@ namespace TweetDuck.Core.Other.Analytics{
{ "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") },
@@ -207,36 +207,30 @@ namespace TweetDuck.Core.Other.Analytics{
} }
private static string TrayMode{ private static string TrayMode{
get{ get => UserConfig.TrayBehavior switch{
switch(UserConfig.TrayBehavior){ TrayIcon.Behavior.DisplayOnly => "icon",
case TrayIcon.Behavior.DisplayOnly: return "icon"; TrayIcon.Behavior.MinimizeToTray => "minimize",
case TrayIcon.Behavior.MinimizeToTray: return "minimize"; TrayIcon.Behavior.CloseToTray => "close",
case TrayIcon.Behavior.CloseToTray: return "close"; TrayIcon.Behavior.Combined => "combined",
case TrayIcon.Behavior.Combined: return "combined"; _ => "off"
default: return "off"; };
}
}
} }
private static string NotificationPosition{ private static string NotificationPosition{
get{ get => UserConfig.NotificationPosition switch{
switch(UserConfig.NotificationPosition){ TweetNotification.Position.TopLeft => "top left",
case TweetNotification.Position.TopLeft: return "top left"; TweetNotification.Position.TopRight => "top right",
case TweetNotification.Position.TopRight: return "top right"; TweetNotification.Position.BottomLeft => "bottom left",
case TweetNotification.Position.BottomLeft: return "bottom left"; TweetNotification.Position.BottomRight => "bottom right",
case TweetNotification.Position.BottomRight: return "bottom right"; _ => "custom"
default: return "custom"; };
}
}
} }
private static string NotificationSize{ private static string NotificationSize{
get{ get => UserConfig.NotificationSize switch{
switch(UserConfig.NotificationSize){ TweetNotification.Size.Auto => "auto",
case TweetNotification.Size.Auto: return "auto"; _ => RoundUp(UserConfig.CustomNotificationSize.Width, 20) + "x" + RoundUp(UserConfig.CustomNotificationSize.Height, 20)
default: return RoundUp(UserConfig.CustomNotificationSize.Width, 20)+"x"+RoundUp(UserConfig.CustomNotificationSize.Height, 20); };
}
}
} }
private static string NotificationTimer{ private static string NotificationTimer{

View File

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

View File

@@ -5,7 +5,6 @@ using System.Linq;
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{

View File

@@ -77,7 +77,7 @@ namespace TweetDuck.Core{
this.browser.SetupZoomEvents(); this.browser.SetupZoomEvents();
owner.Controls.Add(browser); owner.Controls.Add(browser);
plugins.Register(browser, PluginEnvironment.Browser, owner, true); plugins.Register(browser, PluginEnvironment.Browser, true);
Config.MuteToggled += Config_MuteToggled; Config.MuteToggled += Config_MuteToggled;
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged; Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;

View File

@@ -10,6 +10,7 @@ using TweetDuck.Data;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils; using TweetLib.Core.Utils;
using Cookie = CefSharp.Cookie; using Cookie = CefSharp.Cookie;
@@ -29,14 +30,6 @@ namespace TweetDuck.Core.Utils{
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD" "tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
}; };
public static readonly string[] ValidImageExtensions = {
".jpg", ".jpeg", ".png", ".gif"
};
public enum ImageQuality{
Default, Orig
}
public static bool IsTweetDeckWebsite(IFrame frame){ public static bool IsTweetDeckWebsite(IFrame frame){
return frame.Url.Contains("//tweetdeck.twitter.com/"); return frame.Url.Contains("//tweetdeck.twitter.com/");
} }
@@ -49,38 +42,19 @@ namespace TweetDuck.Core.Utils{
return frame.Url.Contains("//twitter.com/account/login_verification"); return frame.Url.Contains("//twitter.com/account/login_verification");
} }
private static string ExtractMediaBaseLink(string url){
int slash = url.LastIndexOf('/');
return slash == -1 ? url : StringUtils.ExtractBefore(url, ':', slash);
}
public static string GetMediaLink(string url, ImageQuality quality){ public static string GetMediaLink(string url, ImageQuality quality){
if (quality == ImageQuality.Orig){ return ImageUrl.TryParse(url, out var obj) ? obj.WithQuality(quality) : url;
string result = ExtractMediaBaseLink(url);
if (url.Contains("//ton.twitter.com/") && url.Contains("/ton/data/dm/")){
result += ":large";
}
else if (result != url || url.Contains("//pbs.twimg.com/media/")){
result += ":orig";
}
return result;
}
else{
return url;
}
} }
public static string GetImageFileName(string url){ public static string GetImageFileName(string url){
return UrlUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url)); return UrlUtils.GetFileNameFromUrl(ImageUrl.TryParse(url, out var obj) ? obj.WithNoQuality : url);
} }
public static void ViewImage(string url, ImageQuality quality){ public static void ViewImage(string url, ImageQuality quality){
void ViewImageInternal(string path){ void ViewImageInternal(string path){
string ext = Path.GetExtension(path); string ext = Path.GetExtension(path);
if (ValidImageExtensions.Contains(ext)){ if (ImageUrl.ValidExtensions.Contains(ext)){
WindowsUtils.OpenAssociatedProgram(path); WindowsUtils.OpenAssociatedProgram(path);
} }
else{ else{

View File

@@ -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.

View File

@@ -6,7 +6,7 @@ using TweetDuck.Core.Utils;
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,19 +56,19 @@ namespace TweetDuck.Plugins.Controls{
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));
switch(requiredLines){ nextHeight = requiredLines switch{
case 1: nextHeight = MaximumSize.Height-2*(font.Height-1); break; 1 => MaximumSize.Height - 2 * (font.Height - 1),
case 2: nextHeight = MaximumSize.Height-(font.Height-1); break; 2 => MaximumSize.Height - 1 * (font.Height - 1),
default: nextHeight = MaximumSize.Height; break; _ => MaximumSize.Height
} };
if (nextHeight != Height){ if (nextHeight != Height){
timerLayout.Start(); timerLayout.Start();

View File

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

View File

@@ -5,6 +5,7 @@ using System.Diagnostics;
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,11 +15,10 @@ using TweetLib.Core.Features.Plugins.Enums;
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;
@@ -28,7 +28,10 @@ namespace TweetDuck.Plugins{
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 PluginBridge bridge; private readonly PluginBridge bridge;
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>(); private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
@@ -37,20 +40,23 @@ namespace TweetDuck.Plugins{
private IWebBrowser mainBrowser; private IWebBrowser mainBrowser;
public PluginManager(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.bridge = new PluginBridge(this); this.bridge = new PluginBridge(this);
} }
public void Register(IWebBrowser browser, PluginEnvironment environment, Control sync, bool asMainBrowser = false){ public void Register(IWebBrowser browser, PluginEnvironment environment, bool asMainBrowser = false){
browser.FrameLoadEnd += (sender, args) => { browser.FrameLoadEnd += (sender, args) => {
IFrame frame = args.Frame; IFrame frame = args.Frame;
if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){ if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){
ExecutePlugins(frame, environment, sync); ExecutePlugins(frame, environment);
} }
}; };
@@ -83,10 +89,10 @@ namespace TweetDuck.Plugins{
} }
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('/', '\\') + '"')){}
} }
} }
} }
@@ -105,7 +111,7 @@ namespace TweetDuck.Plugins{
}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;
@@ -120,42 +126,22 @@ namespace TweetDuck.Plugins{
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, Control sync){ 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;
} }
@@ -179,14 +165,16 @@ namespace TweetDuck.Plugins{
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(() => {
Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins)); Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
});
} }
} }
} }

View File

@@ -22,7 +22,7 @@ PM> Install-Package CefSharp.WinForms -Version 67.0.0
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field. The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads. An F# compiler must be present when building the project to enable this feature: `C:\Program Files (x86)\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe` While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads.
### Release ### Release
@@ -44,11 +44,11 @@ If you decide to publicly release a custom version, please make it clear that it
### Installers ### Installers
TweetDuck uses **Inno Setup** for installers and updates. First, download and install [InnoSetup QuickStart Pack](http://www.jrsoftware.org/isdl.php) (non-unicode; editor and encryption support not required) and the [Inno Download Plugin](https://code.google.com/archive/p/inno-download-plugin). TweetDuck uses **Inno Setup** for installers and updates. First, download and install [InnoSetup 5.6.1](http://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) (with Preprocessor support) and the [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list).
Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer for the change to take place. Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer and Visual Studio for the change to take place.
Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio! Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio first!
After the window closes, three installers will be generated inside the `bld/Output` folder: After the window closes, three installers will be generated inside the `bld/Output` folder:
* **TweetDuck.exe** * **TweetDuck.exe**

View File

@@ -578,7 +578,7 @@ ${iconData.map(entry => `#tduck .icon-${entry[0]}:before{content:\"\\f0${entry[1
let cols = this.config.columnWidth.slice(1); let cols = this.config.columnWidth.slice(1);
this.css.insert(".column { width: calc((100vw - 205px) / "+cols+" - 6px) !important; min-width: 160px }"); this.css.insert(".column { width: calc((100vw - 205px) / "+cols+" - 6px) !important; min-width: 160px }");
this.css.insert(".is-condensed .column { width: calc((100vw - 55px) / "+cols+" - 6px) !important }"); this.css.insert(".is-condensed .column { width: calc((100vw - 65px) / "+cols+" - 6px) !important }");
} }
else{ else{
this.css.insert(".column { width: "+this.config.columnWidth+" !important }"); this.css.insert(".column { width: "+this.config.columnWidth+" !important }");

View File

@@ -506,18 +506,6 @@ html.dark .lst-group .selected a:hover{background:#55acee}
html.dark .lst-group .selected .fullname,html.dark .lst-group .selected .inner strong,html.dark .lst-group .selected .list-link,html.dark .lst-group .selected .list-twitter-list,html.dark .lst-group .selected .list-subtitle,html.dark .lst-group .selected .list-account,html.dark .lst-group .selected .list-listmember,html.dark .lst-group .selected .txt-ellipsis{color:#F5F8FA} html.dark .lst-group .selected .fullname,html.dark .lst-group .selected .inner strong,html.dark .lst-group .selected .list-link,html.dark .lst-group .selected .list-twitter-list,html.dark .lst-group .selected .list-subtitle,html.dark .lst-group .selected .list-account,html.dark .lst-group .selected .list-listmember,html.dark .lst-group .selected .txt-ellipsis{color:#F5F8FA}
html.dark .lst-group .selected .username,html.dark .lst-group .selected .bytext,html.dark .lst-group .selected .subtitle,html.dark .lst-group .selected .icon-protected{color:#eef3f7} html.dark .lst-group .selected .username,html.dark .lst-group .selected .bytext,html.dark .lst-group .selected .subtitle,html.dark .lst-group .selected .icon-protected{color:#eef3f7}
html.dark .itm-remove{border-top:1px solid #ddd} html.dark .itm-remove{border-top:1px solid #ddd}
html.dark .caret-outer{border-bottom:7px solid rgba(17,17,17,0.1)}
html.dark .caret-inner{border-bottom:6px solid #fff}
html.dark .drp-h-divider{border-bottom:1px solid #ddd}
html.dark .dropdown-menu .typeahead-item,html.dark .dropdown-menu [data-action]{color:#292F33}
html.dark .dropdown-menu .is-selected{background:#55acee;color:#fff}
html.dark .dropdown-menu .is-selected [data-action]{color:#fff}
html.dark .dropdown-menu .is-selected a:not(:hover):not(:focus){color:#fff}
html.dark .dropdown-menu a:not(:hover):not(:focus){color:#292F33}
html.dark .dropdown-menu-old li:hover{background:#55acee}
html.dark .dropdown-menu-old li:hover a{color:#fff}
html.dark .dropdown-menu-old li:hover .attribution{color:#fff}
html.dark .non-selectable-item{color:#292F33}
html.dark .update-available-item:before{background-color:#FFAD1F} html.dark .update-available-item:before{background-color:#FFAD1F}
html.dark .is-selected .update-available-item:before{background-color:rgba(41,47,51,0.2)} html.dark .is-selected .update-available-item:before{background-color:rgba(41,47,51,0.2)}
html.dark .popover{background-color:#fff;box-shadow:0 0 10px rgba(17,17,17,0.7)} html.dark .popover{background-color:#fff;box-shadow:0 0 10px rgba(17,17,17,0.7)}

View File

@@ -145,7 +145,7 @@ namespace TweetDuck.Resources{
// 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();

View File

@@ -1298,6 +1298,31 @@
}; };
}); });
//
// Block: Fix DM image previews and GIF thumbnails not loading due to new URLs.
//
if (ensurePropertyExists(TD, "services", "TwitterMedia", "prototype", "getTwitterPreviewUrl")){
const prevFunc = TD.services.TwitterMedia.prototype.getTwitterPreviewUrl;
TD.services.TwitterMedia.prototype.getTwitterPreviewUrl = function(){
const url = prevFunc.apply(this, arguments);
if (url.startsWith("https://ton.twitter.com/1.1/ton/data/dm/") || url.startsWith("https://pbs.twimg.com/tweet_video_thumb/")){
const format = url.match(/\?.*format=(\w+)/);
if (format && format.length === 2){
const fix = `.${format[1]}?`;
if (!url.includes(fix)){
return url.replace("?", fix);
}
}
}
return url;
};
}
// //
// Block: Fix youtu.be previews not showing up for https links. // Block: Fix youtu.be previews not showing up for https links.
// //
@@ -1592,7 +1617,7 @@
} }
// //
// Block: Fix broken horizontal scrolling of column container when holding Shift. TODO Fix broken smooth scrolling. // Block: Fix broken horizontal scrolling of column container when holding Shift.
// //
if (ensurePropertyExists(TD, "ui", "columns", "setupColumnScrollListeners")){ if (ensurePropertyExists(TD, "ui", "columns", "setupColumnScrollListeners")){
TD.ui.columns.setupColumnScrollListeners = appendToFunction(TD.ui.columns.setupColumnScrollListeners, function(column){ TD.ui.columns.setupColumnScrollListeners = appendToFunction(TD.ui.columns.setupColumnScrollListeners, function(column){
@@ -1600,9 +1625,7 @@
return if !ele.length; return if !ele.length;
ele.off("onmousewheel").on("mousewheel", ".scroll-v", function(e){ ele.off("onmousewheel").on("mousewheel", ".scroll-v", function(e){
if (e.shiftKey){
e.stopImmediatePropagation(); e.stopImmediatePropagation();
}
}); });
window.TDGF_prioritizeNewestEvent(ele[0], "mousewheel"); window.TDGF_prioritizeNewestEvent(ele[0], "mousewheel");

View File

@@ -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">
@@ -385,7 +384,7 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
</PostBuildEvent> </PostBuildEvent>
</PropertyGroup> </PropertyGroup>
<Target Name="BeforeBuild" Condition="(!$([System.IO.File]::Exists(&quot;$(ProjectDir)\bld\post_build.exe&quot;)) OR ($([System.IO.File]::GetLastWriteTime(&quot;$(ProjectDir)\Resources\PostBuild.fsx&quot;).Ticks) &gt; $([System.IO.File]::GetLastWriteTime(&quot;$(ProjectDir)\bld\post_build.exe&quot;).Ticks)))"> <Target Name="BeforeBuild" Condition="(!$([System.IO.File]::Exists(&quot;$(ProjectDir)\bld\post_build.exe&quot;)) OR ($([System.IO.File]::GetLastWriteTime(&quot;$(ProjectDir)\Resources\PostBuild.fsx&quot;).Ticks) &gt; $([System.IO.File]::GetLastWriteTime(&quot;$(ProjectDir)\bld\post_build.exe&quot;).Ticks)))">
<Exec Command="&quot;$(ProjectDir)bld\POST BUILD.bat&quot;" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" /> <Exec Command="&quot;$(ProjectDir)bld\POST BUILD.bat&quot; &quot;$(DevEnvDir)CommonExtensions\Microsoft\FSharp\fsc.exe&quot;" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
</Target> </Target>
<Target Name="AfterBuild" Condition="$(ConfigurationName) == Release"> <Target Name="AfterBuild" Condition="$(ConfigurationName) == Release">
<Exec Command="del &quot;$(TargetDir)*.pdb&quot;" /> <Exec Command="del &quot;$(TargetDir)*.pdb&quot;" />

View File

@@ -1,16 +1,12 @@
@ECHO OFF @ECHO OFF
DEL "post_build.exe" IF EXIST "post_build.exe" (
DEL "post_build.exe"
SET fsc="%PROGRAMFILES(x86)%\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe"
IF NOT EXIST %fsc% (
SET fsc="%PROGRAMFILES%\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe"
) )
IF NOT EXIST %fsc% ( IF NOT EXIST %1 (
ECHO fsc.exe not found ECHO fsc.exe not found
EXIT 1 EXIT 1
) )
%fsc% --standalone --deterministic --preferreduilang:en-US --platform:x86 --target:exe --out:post_build.exe "%~dp0..\Resources\PostBuild.fsx" %1 --standalone --deterministic --preferreduilang:en-US --platform:x86 --target:exe --out:post_build.exe "%~dp0..\Resources\PostBuild.fsx"

View File

@@ -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,76 +10,29 @@ namespace TweetLib.Core.Features.Plugins.Enums{
} }
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;
} }
public static string? GetPluginScriptFile(this PluginEnvironment environment){ public static string? GetPluginScriptFile(this PluginEnvironment environment){
switch(environment){ return environment switch{
case PluginEnvironment.Browser: return "browser.js"; PluginEnvironment.Browser => "browser.js",
case PluginEnvironment.Notification: return "notification.js"; PluginEnvironment.Notification => "notification.js",
default: return null; _ => throw new InvalidOperationException($"Invalid plugin environment: {environment}")
} };
} }
public static string GetPluginScriptVariables(this PluginEnvironment environment){ public static string GetPluginScriptVariables(this PluginEnvironment environment){
switch(environment){ return environment switch{
case PluginEnvironment.Browser: return "$,$TD,$TDP,TD"; PluginEnvironment.Browser => "$,$TD,$TDP,TD",
case PluginEnvironment.Notification: return "$TD,$TDP"; PluginEnvironment.Notification => "$TD,$TDP",
default: return 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();
} }
} }
} }

View File

@@ -1,23 +1,39 @@
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 string GetIdentifierPrefix(this PluginGroup group){ public static IEnumerable<PluginGroup> Values { get; } = new PluginGroup[]{
switch(group){ PluginGroup.Official,
case PluginGroup.Official: return "official/"; PluginGroup.Custom
case PluginGroup.Custom: return "custom/"; };
default: return "unknown/";
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/",
PluginGroup.Custom => "custom/",
_ => "unknown/"
};
} }
public static string GetIdentifierPrefixShort(this PluginGroup group){ public static string GetIdentifierPrefixShort(this PluginGroup group){
switch(group){ return group switch{
case PluginGroup.Official: return "o/"; PluginGroup.Official => "o/",
case PluginGroup.Custom: return "c/"; PluginGroup.Custom => "c/",
default: return "?/"; _ => "?/"
} };
} }
} }
} }

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

@@ -71,11 +71,11 @@ namespace TweetLib.Core.Features.Plugins{
} }
public string GetPluginFolder(PluginFolder folder){ public string GetPluginFolder(PluginFolder folder){
switch(folder){ return folder switch{
case PluginFolder.Root: return pathRoot; PluginFolder.Root => pathRoot,
case PluginFolder.Data: return pathData; PluginFolder.Data => pathData,
default: return string.Empty; _ => string.Empty
} };
} }
public string GetFullPathIfSafe(PluginFolder folder, string relativePath){ public string GetFullPathIfSafe(PluginFolder folder, string relativePath){

View File

@@ -5,26 +5,21 @@ using System.IO;
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 @@ namespace TweetDuck.Plugins{
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 @@ namespace TweetDuck.Plugins{
} }
} }
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 @@ namespace TweetDuck.Plugins{
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 @@ namespace TweetDuck.Plugins{
} }
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 @@ namespace TweetDuck.Plugins{
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();
}
}
} }
} }

View File

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

View File

@@ -0,0 +1,5 @@
namespace TweetLib.Core.Features.Twitter{
public enum ImageQuality{
Default, Best
}
}

View File

@@ -0,0 +1,88 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using TweetLib.Core.Utils;
namespace TweetLib.Core.Features.Twitter{
public class ImageUrl{
private static readonly Regex RegexImageUrlParams = new Regex(@"(format|name)=(\w+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static readonly string[] ValidExtensions = {
".jpg", ".jpeg", ".png", ".gif"
};
public static bool TryParse(string url, out ImageUrl obj){
obj = default!;
int slash = url.LastIndexOf('/');
if (slash == -1){
return false;
}
int question = url.IndexOf('?', slash);
if (question == -1){
var oldStyleUrl = StringUtils.SplitInTwo(url, ':', slash);
if (oldStyleUrl.HasValue){
var (baseUrl, quality) = oldStyleUrl.Value;
obj = new ImageUrl(baseUrl, quality);
return true;
}
return false;
}
string? imageExtension = null;
string? imageQuality = null;
foreach(Match match in RegexImageUrlParams.Matches(url, question)){
string value = match.Groups[2].Value;
switch(match.Groups[1].Value){
case "format":
imageExtension = '.' + value;
break;
case "name":
imageQuality = value;
break;
}
}
if (!ValidExtensions.Contains(imageExtension) || imageQuality == null){
return false;
}
string originalUrl = url.Substring(0, question);
obj = new ImageUrl(Path.HasExtension(originalUrl) ? originalUrl : originalUrl + imageExtension, imageQuality);
return true;
}
private readonly string baseUrl;
private readonly string quality;
private ImageUrl(string baseUrl, string quality){
this.baseUrl = baseUrl;
this.quality = quality;
}
public string WithNoQuality => baseUrl;
public string WithQuality(ImageQuality newQuality){
if (newQuality == ImageQuality.Best){
if (baseUrl.Contains("//ton.twitter.com/") && baseUrl.Contains("/ton/data/dm/")){
return baseUrl + ":large";
}
else if (baseUrl.Contains("//pbs.twimg.com/media/")){
return baseUrl + ":orig";
}
}
return baseUrl + ':' + quality;
}
}
}

View File

@@ -4,7 +4,7 @@ using System.Threading;
namespace TweetLib.Core{ namespace TweetLib.Core{
public static class Lib{ public static class Lib{
public const string BrandName = "TweetDuck"; public const string BrandName = "TweetDuck";
public const string VersionTag = "1.18"; public const string VersionTag = "1.18.1";
public static CultureInfo Culture { get; private set; } public static CultureInfo Culture { get; private set; }

View File

@@ -6,6 +6,16 @@ namespace TweetLib.Core.Utils{
public static class StringUtils{ public static class StringUtils{
public static readonly string[] EmptyArray = new string[0]; public static readonly string[] EmptyArray = new string[0];
public static (string before, string after)? SplitInTwo(string str, char search, int startIndex = 0){
int index = str.IndexOf(search, startIndex);
if (index == -1){
return null;
}
return (str.Substring(0, index), str.Substring(index + 1));
}
public static string ExtractBefore(string str, char search, int startIndex = 0){ public static string ExtractBefore(string str, char search, int startIndex = 0){
int index = str.IndexOf(search, startIndex); int index = str.IndexOf(search, startIndex);
return index == -1 ? str : str.Substring(0, index); return index == -1 ? str : str.Substring(0, index);

View File

@@ -26,13 +26,10 @@ namespace TweetLib.Core.Utils{
public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){ public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){
return (sender, args) => { return (sender, args) => {
if (args.Cancelled){ if (args.Cancelled){
try{ TryDeleteFile(file);
File.Delete(file);
}catch{
// didn't want it deleted anyways
}
} }
else if (args.Error != null){ else if (args.Error != null){
TryDeleteFile(file);
onFailure?.Invoke(args.Error); onFailure?.Invoke(args.Error);
} }
else{ else{
@@ -40,5 +37,13 @@ namespace TweetLib.Core.Utils{
} }
}; };
} }
private static void TryDeleteFile(string file){
try{
File.Delete(file);
}catch{
// didn't want it deleted anyways
}
}
} }
} }

View File

@@ -2,6 +2,7 @@
open Xunit open Xunit
open TweetDuck.Core.Utils open TweetDuck.Core.Utils
open TweetLib.Core.Features.Twitter
[<Collection("RegexAccount")>] [<Collection("RegexAccount")>]
@@ -60,7 +61,7 @@ module RegexAccount_Match =
module GetMediaLink_Default = module GetMediaLink_Default =
let getMediaLinkDefault url = TwitterUtils.GetMediaLink(url, TwitterUtils.ImageQuality.Default) let getMediaLinkDefault url = TwitterUtils.GetMediaLink(url, ImageQuality.Default)
let domain = "https://pbs.twimg.com" let domain = "https://pbs.twimg.com"
[<Fact>] [<Fact>]
@@ -77,7 +78,7 @@ module GetMediaLink_Default =
module GetMediaLink_Orig = module GetMediaLink_Orig =
let getMediaLinkOrig url = TwitterUtils.GetMediaLink(url, TwitterUtils.ImageQuality.Orig) let getMediaLinkOrig url = TwitterUtils.GetMediaLink(url, ImageQuality.Best)
let domain = "https://pbs.twimg.com" let domain = "https://pbs.twimg.com"
[<Fact>] [<Fact>]