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

Compare commits

..

20 Commits

Author SHA1 Message Date
a369c65451 Release 1.18.2 2019-10-23 02:26:53 +02:00
318f65f187 Merge branch 'master' of https://github.com/chylex/TweetDuck 2019-10-17 13:04:19 +02:00
1cd60e831c Add a few missing translation languages 2019-10-12 18:30:12 +02:00
b988959eaa Only activate mouse hook while cursor is over the notification window 2019-10-07 04:49:39 +02:00
1eb1f9848a Prepare login/logout page scripts and styles for Twitter redesign & minor fixes 2019-10-07 03:01:17 +02:00
7f6cc0da01 Fix mouse back/forward button triggering navigation if history wasn't empty
Closes #286
2019-10-06 14:47:49 +02:00
19fcb69525 Fix prebuild event not killing hung browser processes reliably 2019-09-05 01:27:36 +02:00
22cef0a44c Fix C# version in secondary projects 2019-09-05 00:48:18 +02:00
2459d31bff Remove RegexOptions.Compiled where not needed 2019-09-05 00:16:25 +02:00
19f104239a Fix missing spaces in C#/F# code 2019-08-23 01:56:31 +02:00
bd0be65038 Minor refactoring & removal of unnecessary code 2019-08-23 01:56:18 +02:00
bbb7907e54 Move LockManager to TweetLib.Core & remove WindowsUtils.CurrentProcessID 2019-08-22 06:29:32 +02:00
a6963a18d4 Move debug resource hot swap into a separate class 2019-08-21 10:31:30 +02:00
92716ea3e0 Move URL-related code from UrlUtils & TwitterUtils to TwitterUrls 2019-08-21 10:12:19 +02:00
aec4c1feea Move TweetNotification to TweetLib.Core as DesktopNotification 2019-08-21 10:12:19 +02:00
d505b3305b Initial refactoring of ScriptLoader & making it accessible in TweetLib.Core 2019-08-21 10:12:19 +02:00
a34a02e14d Generalize PluginListFlowLayout and move it 2019-07-15 00:49:28 +02:00
26d2d7a51e Move PluginManager to Core lib & refactor plugin enums 2019-07-14 20:44:25 +02:00
c2f7e52d13 Add IAppSystemHandler w/ OpenFileExplorer and update existing code to use it 2019-07-14 20:44:25 +02:00
de68d8934d Add IScriptExecutor w/ implementation for CefSharp browser 2019-07-14 17:15:14 +02:00
107 changed files with 1470 additions and 1250 deletions

View File

@@ -1,10 +1,10 @@
using System;
using System.Drawing;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Data;
using TweetLib.Core.Features.Configuration;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Configuration{
@@ -57,12 +57,12 @@ namespace TweetDuck.Configuration{
public bool NotificationTimerCountDown { get; set; } = false;
public int NotificationDurationValue { get; set; } = 25;
public TweetNotification.Position NotificationPosition { get; set; } = TweetNotification.Position.TopRight;
public DesktopNotification.Position NotificationPosition { get; set; } = DesktopNotification.Position.TopRight;
public Point CustomNotificationPosition { get; set; } = ControlExtensions.InvisibleLocation;
public int NotificationDisplay { get; set; } = 0;
public int NotificationEdgeDistance { get; set; } = 8;
public TweetNotification.Size NotificationSize { get; set; } = TweetNotification.Size.Auto;
public DesktopNotification.Size NotificationSize { get; set; } = DesktopNotification.Size.Auto;
public Size CustomNotificationSize { get; set; } = Size.Empty;
public int NotificationScrollSpeed { get; set; } = 100;

View File

@@ -0,0 +1,41 @@
using System.IO;
using CefSharp;
using TweetLib.Core.Browser;
namespace TweetDuck.Core.Adapters{
sealed class CefScriptExecutor : IScriptExecutor{
private readonly IWebBrowser browser;
public CefScriptExecutor(IWebBrowser browser){
this.browser = browser;
}
public void RunFunction(string name, params object[] args){
browser.ExecuteScriptAsync(name, args);
}
public void RunScript(string identifier, string script){
using IFrame frame = browser.GetMainFrame();
RunScript(frame, script, identifier);
}
public bool RunFile(string file){
using IFrame frame = browser.GetMainFrame();
return RunFile(frame, file);
}
// Helpers
public static void RunScript(IFrame frame, string script, string identifier){
if (script != null){
frame.ExecuteJavaScriptAsync(script, identifier, 1);
}
}
public static bool RunFile(IFrame frame, string file){
string script = Program.Resources.Load(file);
RunScript(frame, script, "root:" + Path.GetFileNameWithoutExtension(file));
return script != null;
}
}
}

View File

@@ -8,8 +8,8 @@ namespace TweetDuck.Core.Bridge{
}
public static string GenerateScript(Environment environment){
string Bool(bool value) => value ? "true;" : "false;";
string Str(string value) => '"'+value+"\";";
static string Bool(bool value) => value ? "true;" : "false;";
static string Str(string value) => $"\"{value}\";";
UserConfig config = Program.Config.User;
StringBuilder build = new StringBuilder(128).Append("(function(x){");

View File

@@ -1,20 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Management;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Notifications;
namespace TweetDuck.Core.Bridge{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class TweetDeckBridge{
public static string FontSize { get; private set; }
public static string NotificationHeadLayout { get; private set; }
public static readonly ContextInfo ContextInfo = new ContextInfo();
public static void ResetStaticProperties(){
FontSize = NotificationHeadLayout = null;
FormNotificationBase.FontSize = null;
FormNotificationBase.HeadLayout = null;
}
private readonly FormBrowser form;
@@ -46,17 +44,17 @@ namespace TweetDuck.Core.Bridge{
public void LoadNotificationLayout(string fontSize, string headLayout){
form.InvokeAsyncSafe(() => {
FontSize = fontSize;
NotificationHeadLayout = headLayout;
FormNotificationBase.FontSize = fontSize;
FormNotificationBase.HeadLayout = headLayout;
});
}
public void SetRightClickedLink(string type, string url){
ContextInfo.SetLink(type, url);
ContextMenuBase.CurrentInfo.SetLink(type, url);
}
public void SetRightClickedChirp(string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages){
ContextInfo.SetChirp(tweetUrl, quoteUrl, chirpAuthors, chirpImages);
ContextMenuBase.CurrentInfo.SetChirp(tweetUrl, quoteUrl, chirpAuthors, chirpImages);
}
public void DisplayTooltip(string text){
@@ -87,7 +85,7 @@ namespace TweetDuck.Core.Bridge{
public void OnTweetPopup(string columnId, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl){
notification.InvokeAsyncSafe(() => {
form.OnTweetNotification();
notification.ShowNotification(new TweetNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
notification.ShowNotification(new DesktopNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
});
}

View File

@@ -13,7 +13,6 @@ namespace TweetDuck.Core.Bridge{
private UpdateInfo nextUpdate = null;
public event EventHandler<UpdateInfo> UpdateAccepted;
public event EventHandler<UpdateInfo> UpdateDelayed;
public event EventHandler<UpdateInfo> UpdateDismissed;
public UpdateBridge(UpdateHandler updates, Control sync){
@@ -56,10 +55,6 @@ namespace TweetDuck.Core.Bridge{
HandleInteractionEvent(UpdateAccepted);
}
public void OnUpdateDelayed(){
HandleInteractionEvent(UpdateDelayed);
}
public void OnUpdateDismissed(){
HandleInteractionEvent(UpdateDismissed);

View File

@@ -21,17 +21,16 @@ namespace TweetDuck.Core.Controls{
}
public static float GetDPIScale(this Control control){
using(Graphics graphics = control.CreateGraphics()){
using Graphics graphics = control.CreateGraphics();
return graphics.DpiY / 96F;
}
}
public static bool IsFullyOutsideView(this Form form){
return !Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(form.Bounds));
}
public static void MoveToCenter(this Form targetForm, Form parentForm){
targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2);
targetForm.Location = new Point(parentForm.Location.X + (parentForm.Width / 2) - (targetForm.Width / 2), parentForm.Location.Y + (parentForm.Height / 2) - (targetForm.Height / 2));
}
public static void SetValueInstant(this ProgressBar bar, int value){
@@ -63,7 +62,8 @@ namespace TweetDuck.Core.Controls{
trackBar.Value = trackBar.SmallChange * (int)Math.Floor(((double)trackBar.Value / trackBar.SmallChange) + 0.5);
return false;
}
else return true;
return true;
}
public static void EnableMultilineShortcuts(this TextBox textBox){

View File

@@ -1,13 +1,8 @@
using System.Windows.Forms;
using TweetDuck.Core.Utils;
namespace TweetDuck.Plugins{
sealed class PluginListFlowLayout : FlowLayoutPanel{
public PluginListFlowLayout(){
FlowDirection = FlowDirection.TopDown;
WrapContents = false;
}
namespace TweetDuck.Core.Controls{
sealed class FlowLayoutPanelNoHScroll : FlowLayoutPanel{
protected override void WndProc(ref Message m){
if (m.Msg == 0x85){ // WM_NCPAINT
NativeMethods.ShowScrollBar(Handle, NativeMethods.SB_HORZ, false); // basically fuck the horizontal scrollbar very much

View File

@@ -8,8 +8,8 @@ namespace TweetDuck.Core.Controls{
protected override void OnPaint(PaintEventArgs e){
int y = (int)Math.Floor((ClientRectangle.Height - Text.Length * LineHeight) / 2F) - 1;
using Brush brush = new SolidBrush(ForeColor);
using(Brush brush = new SolidBrush(ForeColor)){
foreach(char chr in Text){
string str = chr.ToString();
float x = (ClientRectangle.Width - e.Graphics.MeasureString(str, Font).Width) / 2F;
@@ -20,4 +20,3 @@ namespace TweetDuck.Core.Controls{
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -14,9 +15,8 @@ using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetDuck.Updates;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Updates;
@@ -65,7 +65,7 @@ namespace TweetDuck.Core{
Text = Program.BrandName;
this.plugins = new PluginManager(this, Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
this.plugins.Reload();
@@ -78,7 +78,6 @@ namespace TweetDuck.Core{
this.updateBridge = new UpdateBridge(updates, this);
this.updateBridge.UpdateAccepted += updateBridge_UpdateAccepted;
this.updateBridge.UpdateDelayed += updateBridge_UpdateDelayed;
this.updateBridge.UpdateDismissed += updateBridge_UpdateDismissed;
this.browser = new TweetDeckBrowser(this, plugins, new TweetDeckBridge.Browser(this, notification), updateBridge);
@@ -236,7 +235,9 @@ namespace TweetDuck.Core{
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
this.InvokeAsyncSafe(() => { // TODO not needed but makes code consistent...
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
});
}
if (isLoaded){
@@ -244,9 +245,11 @@ namespace TweetDuck.Core{
}
}
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
private void plugins_Executed(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
this.InvokeAsyncSafe(() => {
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
});
}
}
@@ -319,10 +322,6 @@ namespace TweetDuck.Core{
}
}
private void updateBridge_UpdateDelayed(object sender, UpdateInfo update){
// stops the timer
}
private void updateBridge_UpdateDismissed(object sender, UpdateInfo update){
Config.DismissedUpdate = update.VersionTag;
Config.Save();
@@ -330,7 +329,9 @@ namespace TweetDuck.Core{
protected override void WndProc(ref Message m){
if (isLoaded && m.Msg == Program.WindowRestoreMessage){
if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
using Process me = Process.GetCurrentProcess();
if (me.Id == m.WParam.ToInt32()){
trayIcon_ClickRestore(trayIcon, EventArgs.Empty);
}
@@ -367,14 +368,7 @@ namespace TweetDuck.Core{
}
public void ReloadToTweetDeck(){
#if DEBUG
ScriptLoader.HotSwap();
#else
if (ModifierKeys.HasFlag(Keys.Shift)){
ScriptLoader.ClearCache();
}
#endif
Program.Resources.OnReloadTriggered();
ignoreUpdateCheckError = false;
browser.ReloadToTweetDeck();
AnalyticsFile.BrowserReloads.Trigger();

View File

@@ -6,19 +6,19 @@ using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using System.Linq;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Management;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Resources;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{
protected static UserConfig Config => Program.Config.User;
public static ContextInfo CurrentInfo { get; } = new ContextInfo();
protected static UserConfig Config => Program.Config.User;
private static ImageQuality ImageQuality => Config.TwitterImageQuality;
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
@@ -42,11 +42,11 @@ namespace TweetDuck.Core.Handling{
}
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
Context = TweetDeckBridge.ContextInfo.Reset();
if (!TwitterUrls.IsTweetDeck(frame.Url) || browser.IsLoading){
Context = CurrentInfo.Reset();
}
else{
Context = TweetDeckBridge.ContextInfo.Create(parameters);
Context = CurrentInfo.Create(parameters);
}
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection) && !parameters.TypeFlags.HasFlag(ContextMenuType.Editable)){
@@ -56,12 +56,12 @@ namespace TweetDuck.Core.Handling{
model.AddSeparator();
}
string TextOpen(string name) => "Open "+name+" in browser";
string TextCopy(string name) => "Copy "+name+" address";
string TextSave(string name) => "Save "+name+" as...";
static string TextOpen(string name) => "Open " + name + " in browser";
static string TextCopy(string name) => "Copy " + name + " address";
static string TextSave(string name) => "Save " + name + " as...";
if (Context.Types.HasFlag(ContextInfo.ContextType.Link) && !Context.UnsafeLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal)){
if (TwitterUtils.RegexAccount.IsMatch(Context.UnsafeLinkUrl)){
if (TwitterUrls.RegexAccount.IsMatch(Context.UnsafeLinkUrl)){
model.AddItem(MenuOpenLinkUrl, TextOpen("account"));
model.AddItem(MenuCopyLinkUrl, TextCopy("account"));
model.AddItem(MenuCopyUsername, "Copy account username");
@@ -80,7 +80,7 @@ namespace TweetDuck.Core.Handling{
model.AddItem(MenuSaveMedia, TextSave("video"));
model.AddSeparator();
}
else if (Context.Types.HasFlag(ContextInfo.ContextType.Image) && Context.MediaUrl != TweetNotification.AppLogo.Url){
else if (Context.Types.HasFlag(ContextInfo.ContextType.Image) && Context.MediaUrl != FormNotificationBase.AppLogo.Url){
model.AddItem(MenuViewImage, "View image in photo viewer");
model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
@@ -108,7 +108,7 @@ namespace TweetDuck.Core.Handling{
case MenuCopyUsername: {
string url = Context.UnsafeLinkUrl;
Match match = TwitterUtils.RegexAccount.Match(url);
Match match = TwitterUrls.RegexAccount.Match(url);
SetClipboardText(control, match.Success ? match.Groups[1].Value : url);
control.InvokeAsyncSafe(analytics.AnalyticsFile.CopiedUsernames.Trigger);
@@ -116,11 +116,11 @@ namespace TweetDuck.Core.Handling{
}
case MenuOpenMediaUrl:
OpenBrowser(control, TwitterUtils.GetMediaLink(Context.MediaUrl, ImageQuality));
OpenBrowser(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
break;
case MenuCopyMediaUrl:
SetClipboardText(control, TwitterUtils.GetMediaLink(Context.MediaUrl, ImageQuality));
SetClipboardText(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
break;
case MenuViewImage: {
@@ -186,7 +186,7 @@ namespace TweetDuck.Core.Handling{
}
public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
Context = TweetDeckBridge.ContextInfo.Reset();
Context = CurrentInfo.Reset();
}
public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback){
@@ -194,7 +194,7 @@ namespace TweetDuck.Core.Handling{
}
protected static void DeselectAll(IFrame frame){
ScriptLoader.ExecuteScript(frame, "window.getSelection().removeAllRanges()", "gen:deselect");
CefScriptExecutor.RunScript(frame, "window.getSelection().removeAllRanges()", "gen:deselect");
}
protected static void OpenBrowser(Control control, string url){

View File

@@ -2,7 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Management;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuBrowser : ContextMenuBase{
@@ -53,7 +53,7 @@ namespace TweetDuck.Core.Handling{
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (isSelecting && !isEditing && TwitterUtils.IsTweetDeckWebsite(frame)){
if (isSelecting && !isEditing && TwitterUrls.IsTweetDeck(frame.Url)){
InsertSelectionSearchItem(model, MenuSearchInColumn, "Search in a column");
}

View File

@@ -11,13 +11,12 @@ namespace TweetDuck.Core.Handling.General{
private static void UpdatePrefsInternal(){
UserConfig config = Program.Config.User;
using IRequestContext ctx = Cef.GetGlobalRequestContext();
using(IRequestContext ctx = Cef.GetGlobalRequestContext()){
ctx.SetPreference("browser.enable_spellchecking", config.EnableSpellCheck, out string _);
ctx.SetPreference("spellcheck.dictionary", config.SpellCheckLanguage, out string _);
ctx.SetPreference("settings.a11y.animation_policy", config.EnableAnimatedImages ? "allowed" : "none", out string _);
}
}
void IBrowserProcessHandler.OnContextInitialized(){
UpdatePrefsInternal();

View File

@@ -11,7 +11,7 @@ using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{
class RequestHandlerBase : DefaultRequestHandler{
private static readonly Regex TweetDeckResourceUrl = new Regex(@"/dist/(.*?)\.(.*?)\.(css|js)$", RegexOptions.Compiled);
private static readonly Regex TweetDeckResourceUrl = new Regex(@"/dist/(.*?)\.(.*?)\.(css|js)$");
private static readonly SortedList<string, string> TweetDeckHashes = new SortedList<string, string>(4);
public static void LoadResourceRewriteRules(string rules){

View File

@@ -2,6 +2,7 @@
using CefSharp;
using TweetDuck.Core.Handling.Filters;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Handling{
sealed class RequestHandlerBrowser : RequestHandlerBase{
@@ -15,7 +16,7 @@ namespace TweetDuck.Core.Handling{
public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){
if (request.ResourceType == ResourceType.MainFrame){
if (request.Url.EndsWith("//twitter.com/")){
request.Url = TwitterUtils.TweetDeckURL; // redirect plain twitter.com requests, fixes bugs with login 2FA
request.Url = TwitterUrls.TweetDeck; // redirect plain twitter.com requests, fixes bugs with login 2FA
}
}
else if (request.ResourceType == ResourceType.Script){
@@ -41,6 +42,9 @@ namespace TweetDuck.Core.Handling{
BlockNextUserNavUrl = string.Empty;
return block;
}
else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)){
return true;
}
return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using TweetDuck.Core.Other;
using TweetDuck.Plugins;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
@@ -74,7 +73,7 @@ namespace TweetDuck.Core.Management{
Items items = Items.None;
try{
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))){
using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None));
string key;
while((key = stream.SkipFile()) != null){
@@ -97,7 +96,6 @@ namespace TweetDuck.Core.Management{
break;
}
}
}
}catch(Exception){
items = Items.None;
}
@@ -141,7 +139,7 @@ namespace TweetDuck.Core.Management{
entry.WriteToFile(Path.Combine(Program.PluginDataPath, value[0], value[1]), true);
if (!plugins.IsPluginInstalled(value[0])){
if (!plugins.Plugins.Any(plugin => plugin.Identifier.Equals(value[0]))){
missingPlugins.Add(value[0]);
}
}

View File

@@ -2,17 +2,17 @@
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification.Example{
sealed class FormNotificationExample : FormNotificationMain{
public override bool RequiresResize => true;
protected override bool CanDragWindow => Config.NotificationPosition == TweetNotification.Position.Custom;
protected override bool CanDragWindow => Config.NotificationPosition == DesktopNotification.Position.Custom;
protected override FormBorderStyle NotificationBorderStyle{
get{
if (Config.NotificationSize == TweetNotification.Size.Custom){
if (Config.NotificationSize == DesktopNotification.Size.Custom){
switch(base.NotificationBorderStyle){
case FormBorderStyle.FixedSingle: return FormBorderStyle.Sizable;
case FormBorderStyle.FixedToolWindow: return FormBorderStyle.SizableToolWindow;
@@ -27,18 +27,18 @@ namespace TweetDuck.Core.Notification.Example{
public event EventHandler Ready;
private readonly TweetNotification exampleNotification;
private readonly DesktopNotification exampleNotification;
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false){
browser.LoadingStateChanged += browser_LoadingStateChanged;
string exampleTweetHTML = ScriptLoader.LoadResourceSilent("pages/example.html")?.Replace("{avatar}", TweetNotification.AppLogo.Url) ?? string.Empty;
string exampleTweetHTML = Program.Resources.LoadSilent("pages/example.html")?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
#if DEBUG
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
#endif
exampleNotification = new TweetNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
exampleNotification = new DesktopNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
}
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){

View File

@@ -1,20 +1,28 @@
using CefSharp.WinForms;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Notification{
abstract partial class FormNotificationBase : Form, AnalyticsFile.IProvider{
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandler.FromByteArray(Properties.Resources.avatar, "image/png"));
public static string FontSize = null;
public static string HeadLayout = null;
protected static UserConfig Config => Program.Config.User;
protected static int FontSizeLevel{
get => TweetDeckBridge.FontSize switch{
get => FontSize switch{
"largest" => 4,
"large" => 3,
"small" => 1,
@@ -37,19 +45,19 @@ namespace TweetDuck.Core.Notification{
int edgeDist = Config.NotificationEdgeDistance;
switch(Config.NotificationPosition){
case TweetNotification.Position.TopLeft:
case DesktopNotification.Position.TopLeft:
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + edgeDist);
case TweetNotification.Position.TopRight:
case DesktopNotification.Position.TopRight:
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
case TweetNotification.Position.BottomLeft:
case DesktopNotification.Position.BottomLeft:
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
case TweetNotification.Position.BottomRight:
case DesktopNotification.Position.BottomRight:
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
case TweetNotification.Position.Custom:
case DesktopNotification.Position.Custom:
if (!Config.IsCustomNotificationPositionSet){
Config.CustomNotificationPosition = new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
Config.Save();
@@ -99,7 +107,7 @@ namespace TweetDuck.Core.Notification{
private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification();
private TweetNotification currentNotification;
private DesktopNotification currentNotification;
private int pauseCounter;
public string CurrentTweetUrl => currentNotification?.TweetUrl;
@@ -120,8 +128,8 @@ namespace TweetDuck.Core.Notification{
this.owner.FormClosed += owner_FormClosed;
ResourceHandlerFactory resourceHandlerFactory = new ResourceHandlerFactory();
resourceHandlerFactory.RegisterHandler(TwitterUtils.TweetDeckURL, this.resourceHandler);
resourceHandlerFactory.RegisterHandler(TweetNotification.AppLogo);
resourceHandlerFactory.RegisterHandler(TwitterUrls.TweetDeck, this.resourceHandler);
resourceHandlerFactory.RegisterHandler(AppLogo);
this.browser = new ChromiumWebBrowser("about:blank"){
MenuHandler = new ContextMenuNotification(this, enableContextMenu),
@@ -186,13 +194,13 @@ namespace TweetDuck.Core.Notification{
}
}
protected abstract string GetTweetHTML(TweetNotification tweet);
protected abstract string GetTweetHTML(DesktopNotification tweet);
protected virtual void LoadTweet(TweetNotification tweet){
protected virtual void LoadTweet(DesktopNotification tweet){
currentNotification = tweet;
resourceHandler.SetHTML(GetTweetHTML(tweet));
browser.Load(TwitterUtils.TweetDeckURL);
browser.Load(TwitterUrls.TweetDeck);
DisplayTooltip(null);
}

View File

@@ -2,13 +2,15 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Core.Notification{
@@ -45,14 +47,14 @@ namespace TweetDuck.Core.Notification{
private int BaseClientWidth{
get => Config.NotificationSize switch{
TweetNotification.Size.Custom => Config.CustomNotificationSize.Width,
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Width,
_ => BrowserUtils.Scale(284, SizeScale * (1.0 + 0.05 * FontSizeLevel))
};
}
private int BaseClientHeight{
get => Config.NotificationSize switch{
TweetNotification.Size.Custom => Config.CustomNotificationSize.Height,
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Height,
_ => BrowserUtils.Scale(122, SizeScale * (1.0 + 0.08 * FontSizeLevel))
};
}
@@ -73,7 +75,7 @@ namespace TweetDuck.Core.Notification{
browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd;
plugins.Register(browser, PluginEnvironment.Notification);
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser));
mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook(true);
@@ -154,7 +156,7 @@ namespace TweetDuck.Core.Notification{
if (frame.IsMain && browser.Address != "about:blank"){
frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification));
ScriptLoader.ExecuteFile(frame, "notification.js", this);
CefScriptExecutor.RunFile(frame, "notification.js");
}
}
@@ -164,7 +166,16 @@ namespace TweetDuck.Core.Notification{
}
private void timerHideProgress_Tick(object sender, EventArgs e){
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen){
bool isCursorInside = Bounds.Contains(Cursor.Position);
if (isCursorInside){
StartMouseHook();
}
else{
StopMouseHook(false);
}
if (isCursorInside || FreezeTimer || ContextMenuOpen){
return;
}
@@ -180,7 +191,7 @@ namespace TweetDuck.Core.Notification{
// notification methods
public virtual void ShowNotification(TweetNotification notification){
public virtual void ShowNotification(DesktopNotification notification){
LoadTweet(notification);
}
@@ -217,8 +228,8 @@ namespace TweetDuck.Core.Notification{
}
}
protected override string GetTweetHTML(TweetNotification tweet){
string html = tweet.GenerateHtml(BodyClasses, this);
protected override string GetTweetHTML(DesktopNotification tweet){
string html = tweet.GenerateHtml(BodyClasses, HeadLayout, Config.CustomNotificationCSS);
foreach(InjectedHTML injection in plugins.NotificationInjections){
html = injection.InjectInto(html);
@@ -227,7 +238,7 @@ namespace TweetDuck.Core.Notification{
return html;
}
protected override void LoadTweet(TweetNotification tweet){
protected override void LoadTweet(DesktopNotification tweet){
timerProgress.Stop();
totalTime = timeLeft = tweet.GetDisplayDuration(Config.NotificationDurationValue);
progressBarTimer.Value = Config.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum;
@@ -255,7 +266,6 @@ namespace TweetDuck.Core.Notification{
}
MoveToVisibleLocation();
StartMouseHook();
}
protected virtual void OnNotificationReady(){

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using TweetDuck.Plugins;
using System.Windows.Forms;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification{
sealed partial class FormNotificationTweet : FormNotificationMain{
@@ -25,7 +26,7 @@ namespace TweetDuck.Core.Notification{
}
}
private readonly Queue<TweetNotification> tweetQueue = new Queue<TweetNotification>(4);
private readonly Queue<DesktopNotification> tweetQueue = new Queue<DesktopNotification>(4);
private bool needsTrim;
private bool hasTemporarilyMoved;
@@ -81,7 +82,7 @@ namespace TweetDuck.Core.Notification{
// notification methods
public override void ShowNotification(TweetNotification notification){
public override void ShowNotification(DesktopNotification notification){
tweetQueue.Enqueue(notification);
if (!IsPaused){

View File

@@ -3,12 +3,13 @@ using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotificationBase{
@@ -29,24 +30,23 @@ namespace TweetDuck.Core.Notification.Screenshot{
return;
}
string script = ScriptLoader.LoadResourceSilent("screenshot.js");
string script = Program.Resources.LoadSilent("screenshot.js");
if (script == null){
this.InvokeAsyncSafe(callback);
return;
}
using(IFrame frame = args.Browser.MainFrame){
ScriptLoader.ExecuteScript(frame, script.Replace("{width}", realWidth.ToString()).Replace("{frames}", TweetScreenshotManager.WaitFrames.ToString()), "gen:screenshot");
}
using IFrame frame = args.Browser.MainFrame;
CefScriptExecutor.RunScript(frame, script.Replace("{width}", realWidth.ToString()).Replace("{frames}", TweetScreenshotManager.WaitFrames.ToString()), "gen:screenshot");
};
SetNotificationSize(realWidth, 1024);
LoadTweet(new TweetNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty));
LoadTweet(new DesktopNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty));
}
protected override string GetTweetHTML(TweetNotification tweet){
string html = tweet.GenerateHtml("td-screenshot", this);
protected override string GetTweetHTML(DesktopNotification tweet){
string html = tweet.GenerateHtml("td-screenshot", HeadLayout, Config.CustomNotificationCSS);
foreach(InjectedHTML injection in plugins.NotificationInjections){
html = injection.InjectInto(html);

View File

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

View File

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

View File

@@ -8,10 +8,9 @@ using TweetDuck.Configuration;
using System.Linq;
using System.Management;
using System.Text.RegularExpressions;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetLib.Core;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Utils;
@@ -144,7 +143,8 @@ namespace TweetDuck.Core.Other.Analytics{
string osName, osEdition, osBuild;
try{
using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false)){
using RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false);
// ReSharper disable once PossibleNullReferenceException
osName = key.GetValue("ProductName") as string;
osBuild = key.GetValue("CurrentBuild") as string;
@@ -158,7 +158,6 @@ namespace TweetDuck.Core.Other.Analytics{
osEdition = match.Groups[2].Value;
}
}
}
}catch{
osName = osEdition = osBuild = null;
}
@@ -168,11 +167,11 @@ namespace TweetDuck.Core.Other.Analytics{
SystemBuild = osBuild ?? "(unknown)";
try{
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory")){
using ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory");
foreach(ManagementBaseObject obj in searcher.Get()){
RamSize += (int)((ulong)obj["Capacity"] / (1024L * 1024L));
}
}
}catch{
RamSize = 0;
}
@@ -180,7 +179,8 @@ namespace TweetDuck.Core.Other.Analytics{
string gpu = null;
try{
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController")){
using ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController");
foreach(ManagementBaseObject obj in searcher.Get()){
string vendor = obj["Caption"] as string;
@@ -188,7 +188,6 @@ namespace TweetDuck.Core.Other.Analytics{
gpu = vendor;
}
}
}
}catch{
// rip
}
@@ -218,17 +217,17 @@ namespace TweetDuck.Core.Other.Analytics{
private static string NotificationPosition{
get => UserConfig.NotificationPosition switch{
TweetNotification.Position.TopLeft => "top left",
TweetNotification.Position.TopRight => "top right",
TweetNotification.Position.BottomLeft => "bottom left",
TweetNotification.Position.BottomRight => "bottom right",
DesktopNotification.Position.TopLeft => "top left",
DesktopNotification.Position.TopRight => "top right",
DesktopNotification.Position.BottomLeft => "bottom left",
DesktopNotification.Position.BottomRight => "bottom right",
_ => "custom"
};
}
private static string NotificationSize{
get => UserConfig.NotificationSize switch{
TweetNotification.Size.Auto => "auto",
DesktopNotification.Size.Auto => "auto",
_ => RoundUp(UserConfig.CustomNotificationSize.Width, 20) + "x" + RoundUp(UserConfig.CustomNotificationSize.Height, 20)
};
}

View File

@@ -7,8 +7,8 @@ using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Utils;
using System.Text.RegularExpressions;
using TweetDuck.Core.Adapters;
using TweetDuck.Data;
using TweetDuck.Resources;
namespace TweetDuck.Core.Other{
sealed partial class FormGuide : Form, FormManager.IAppDialog{
@@ -116,7 +116,7 @@ namespace TweetDuck.Core.Other{
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
ScriptLoader.ExecuteScript(e.Frame, "Array.prototype.forEach.call(document.getElementsByTagName('A'), ele => ele.addEventListener('click', e => { e.preventDefault(); window.open(ele.getAttribute('href')); }))", "gen:links");
CefScriptExecutor.RunScript(e.Frame, "Array.prototype.forEach.call(document.getElementsByTagName('A'), ele => ele.addEventListener('click', e => { e.preventDefault(); window.open(ele.getAttribute('href')); }))", "gen:links");
}
}
}

View File

@@ -1,4 +1,4 @@
using TweetDuck.Plugins;
using TweetDuck.Core.Controls;
namespace TweetDuck.Core.Other {
partial class FormPlugins {
@@ -29,7 +29,7 @@ namespace TweetDuck.Core.Other {
this.btnClose = new System.Windows.Forms.Button();
this.btnReload = new System.Windows.Forms.Button();
this.btnOpenFolder = new System.Windows.Forms.Button();
this.flowLayoutPlugins = new PluginListFlowLayout();
this.flowLayoutPlugins = new FlowLayoutPanelNoHScroll();
this.timerLayout = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout();
//
@@ -119,7 +119,7 @@ namespace TweetDuck.Core.Other {
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.Button btnReload;
private System.Windows.Forms.Button btnOpenFolder;
private PluginListFlowLayout flowLayoutPlugins;
private FlowLayoutPanelNoHScroll flowLayoutPlugins;
private System.Windows.Forms.Timer timerLayout;
}
}

View File

@@ -1,10 +1,10 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Plugins;
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Other{
@@ -95,7 +95,7 @@ namespace TweetDuck.Core.Other{
}
private void btnOpenFolder_Click(object sender, EventArgs e){
using(Process.Start("explorer.exe", '"'+pluginManager.PathCustomPlugins+'"')){}
App.SystemHandler.OpenFileExplorer(pluginManager.PathCustomPlugins);
}
private void btnReload_Click(object sender, EventArgs e){

View File

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

View File

@@ -9,7 +9,7 @@ namespace TweetDuck.Core.Other.Settings{
public IEnumerable<Control> InteractiveControls{
get{
IEnumerable<Control> FindInteractiveControls(Control parent){
static IEnumerable<Control> FindInteractiveControls(Control parent){
foreach(Control control in parent.Controls){
if (control is Panel subPanel){
foreach(Control subControl in FindInteractiveControls(subPanel)){

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Forms;
using TweetDuck.Configuration;
@@ -7,6 +6,7 @@ using TweetDuck.Core.Controls;
using TweetDuck.Core.Management;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetLib.Core;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsAdvanced : BaseTabSettings{
@@ -67,11 +67,11 @@ namespace TweetDuck.Core.Other.Settings{
#region Application
private void btnOpenAppFolder_Click(object sender, EventArgs e){
using(Process.Start("explorer.exe", "\""+Program.ProgramPath+"\"")){}
App.SystemHandler.OpenFileExplorer(Program.ProgramPath);
}
private void btnOpenDataFolder_Click(object sender, EventArgs e){
using(Process.Start("explorer.exe", "\""+Program.StoragePath+"\"")){}
App.SystemHandler.OpenFileExplorer(Program.StoragePath);
}
private void btnRestart_Click(object sender, EventArgs e){

View File

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

View File

@@ -1,8 +1,8 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Example;
using TweetLib.Core.Features.Notifications;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsNotifications : BaseTabSettings{
@@ -66,18 +66,18 @@ namespace TweetDuck.Core.Other.Settings{
toolTip.SetToolTip(radioLocCustom, "Drag the example notification window to the desired location.");
switch(Config.NotificationPosition){
case TweetNotification.Position.TopLeft: radioLocTL.Checked = true; break;
case TweetNotification.Position.TopRight: radioLocTR.Checked = true; break;
case TweetNotification.Position.BottomLeft: radioLocBL.Checked = true; break;
case TweetNotification.Position.BottomRight: radioLocBR.Checked = true; break;
case TweetNotification.Position.Custom: radioLocCustom.Checked = true; break;
case DesktopNotification.Position.TopLeft: radioLocTL.Checked = true; break;
case DesktopNotification.Position.TopRight: radioLocTR.Checked = true; break;
case DesktopNotification.Position.BottomLeft: radioLocBL.Checked = true; break;
case DesktopNotification.Position.BottomRight: radioLocBR.Checked = true; break;
case DesktopNotification.Position.Custom: radioLocCustom.Checked = true; break;
}
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = !radioLocCustom.Checked;
comboBoxDisplay.Items.Add("(Same as TweetDuck)");
foreach(Screen screen in Screen.AllScreens){
comboBoxDisplay.Items.Add(screen.DeviceName.TrimStart('\\', '.')+" ("+screen.Bounds.Width+"x"+screen.Bounds.Height+")");
comboBoxDisplay.Items.Add($"{screen.DeviceName.TrimStart('\\', '.')} ({screen.Bounds.Width}x{screen.Bounds.Height})");
}
comboBoxDisplay.SelectedIndex = Math.Min(comboBoxDisplay.Items.Count - 1, Config.NotificationDisplay);
@@ -91,8 +91,8 @@ namespace TweetDuck.Core.Other.Settings{
toolTip.SetToolTip(radioSizeCustom, "Resize the example notification window to the desired size.");
switch(Config.NotificationSize){
case TweetNotification.Size.Auto: radioSizeAuto.Checked = true; break;
case TweetNotification.Size.Custom: radioSizeCustom.Checked = true; break;
case DesktopNotification.Size.Auto: radioSizeAuto.Checked = true; break;
case DesktopNotification.Size.Custom: radioSizeCustom.Checked = true; break;
}
trackBarScrollSpeed.SetValueSafe(Config.NotificationScrollSpeed);
@@ -219,10 +219,10 @@ namespace TweetDuck.Core.Other.Settings{
#region Location
private void radioLoc_CheckedChanged(object sender, EventArgs e){
if (radioLocTL.Checked)Config.NotificationPosition = TweetNotification.Position.TopLeft;
else if (radioLocTR.Checked)Config.NotificationPosition = TweetNotification.Position.TopRight;
else if (radioLocBL.Checked)Config.NotificationPosition = TweetNotification.Position.BottomLeft;
else if (radioLocBR.Checked)Config.NotificationPosition = TweetNotification.Position.BottomRight;
if (radioLocTL.Checked)Config.NotificationPosition = DesktopNotification.Position.TopLeft;
else if (radioLocTR.Checked)Config.NotificationPosition = DesktopNotification.Position.TopRight;
else if (radioLocBL.Checked)Config.NotificationPosition = DesktopNotification.Position.BottomLeft;
else if (radioLocBR.Checked)Config.NotificationPosition = DesktopNotification.Position.BottomRight;
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = true;
notification.ShowExampleNotification(false);
@@ -233,18 +233,18 @@ namespace TweetDuck.Core.Other.Settings{
Config.CustomNotificationPosition = notification.Location;
}
Config.NotificationPosition = TweetNotification.Position.Custom;
Config.NotificationPosition = DesktopNotification.Position.Custom;
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = false;
notification.ShowExampleNotification(false);
if (notification.IsFullyOutsideView() && FormMessage.Question("Notification is Outside View", "The notification seems to be outside of view, would you like to reset its position?", FormMessage.Yes, FormMessage.No)){
Config.NotificationPosition = TweetNotification.Position.TopRight;
Config.NotificationPosition = DesktopNotification.Position.TopRight;
notification.MoveToVisibleLocation();
Config.CustomNotificationPosition = notification.Location;
Config.NotificationPosition = TweetNotification.Position.Custom;
Config.NotificationPosition = DesktopNotification.Position.Custom;
notification.MoveToVisibleLocation();
}
}
@@ -265,7 +265,7 @@ namespace TweetDuck.Core.Other.Settings{
private void radioSize_CheckedChanged(object sender, EventArgs e){
if (radioSizeAuto.Checked){
Config.NotificationSize = TweetNotification.Size.Auto;
Config.NotificationSize = DesktopNotification.Size.Auto;
}
notification.ShowExampleNotification(false);
@@ -276,7 +276,7 @@ namespace TweetDuck.Core.Other.Settings{
Config.CustomNotificationSize = notification.BrowserSize;
}
Config.NotificationSize = TweetNotification.Size.Custom;
Config.NotificationSize = DesktopNotification.Size.Custom;
notification.ShowExampleNotification(false);
}

View File

@@ -5,6 +5,7 @@ using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Configuration;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
@@ -12,8 +13,10 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
namespace TweetDuck.Core{
sealed class TweetDeckBrowser : IDisposable{
@@ -35,9 +38,8 @@ namespace TweetDuck.Core{
return false;
}
using(IFrame frame = browser.GetBrowser().MainFrame){
return TwitterUtils.IsTweetDeckWebsite(frame);
}
using IFrame frame = browser.GetBrowser().MainFrame;
return TwitterUrls.IsTweetDeck(frame.Url);
}
}
@@ -47,12 +49,12 @@ namespace TweetDuck.Core{
private string prevSoundNotificationPath = null;
public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge tdBridge, UpdateBridge updateBridge){
resourceHandlerFactory.RegisterHandler(TweetNotification.AppLogo);
resourceHandlerFactory.RegisterHandler(FormNotificationBase.AppLogo);
resourceHandlerFactory.RegisterHandler(TwitterUtils.LoadingSpinner);
RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
this.browser = new ChromiumWebBrowser(TwitterUtils.TweetDeckURL){
this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck){
DialogHandler = new FileDialogHandler(),
DragHandler = new DragHandlerBrowser(requestHandler),
MenuHandler = new ContextMenuBrowser(owner),
@@ -77,7 +79,7 @@ namespace TweetDuck.Core{
this.browser.SetupZoomEvents();
owner.Controls.Add(browser);
plugins.Register(browser, PluginEnvironment.Browser, true);
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser));
Config.MuteToggled += Config_MuteToggled;
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
@@ -121,14 +123,16 @@ namespace TweetDuck.Core{
IFrame frame = e.Frame;
if (frame.IsMain){
if (TwitterUtils.IsTwitterWebsite(frame)){
string css = ScriptLoader.LoadResource("styles/twitter.css", browser);
string url = frame.Url;
if (TwitterUrls.IsTwitter(url)){
string css = Program.Resources.Load("styles/twitter.css");
resourceHandlerFactory.RegisterHandler(TwitterStyleUrl, ResourceHandler.FromString(css, mimeType: "text/css"));
ScriptLoader.ExecuteFile(frame, "twitter.js", browser);
CefScriptExecutor.RunFile(frame, "twitter.js");
}
if (!TwitterUtils.IsTwitterLogin2FactorWebsite(frame)){
if (!TwitterUrls.IsTwitterLogin2Factor(url)){
frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorOverride);
}
}
@@ -136,11 +140,12 @@ namespace TweetDuck.Core{
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
IFrame frame = e.Frame;
string url = frame.Url;
if (frame.IsMain){
if (TwitterUtils.IsTweetDeckWebsite(frame)){
if (TwitterUrls.IsTweetDeck(url)){
UpdateProperties();
ScriptLoader.ExecuteFile(frame, "code.js", browser);
CefScriptExecutor.RunFile(frame, "code.js");
InjectBrowserCSS();
ReinjectCustomCSS(Config.CustomBrowserCSS);
@@ -149,18 +154,18 @@ namespace TweetDuck.Core{
TweetDeckBridge.ResetStaticProperties();
if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)){
ScriptLoader.ExecuteScript(frame, "TD.storage.Account.prototype.requiresConsent = function(){ return false; }", "gen:gdpr");
CefScriptExecutor.RunScript(frame, "TD.storage.Account.prototype.requiresConsent = function(){ return false; }", "gen:gdpr");
}
if (Config.FirstRun){
ScriptLoader.ExecuteFile(frame, "introduction.js", browser);
CefScriptExecutor.RunFile(frame, "introduction.js");
}
}
ScriptLoader.ExecuteFile(frame, "update.js", browser);
CefScriptExecutor.RunFile(frame, "update.js");
}
if (frame.Url == ErrorUrl){
if (url == ErrorUrl){
resourceHandlerFactory.UnregisterHandler(ErrorUrl);
}
}
@@ -171,10 +176,13 @@ namespace TweetDuck.Core{
}
if (!e.FailedUrl.StartsWith("http://td/", StringComparison.Ordinal)){
string errorPage = ScriptLoader.LoadResourceSilent("pages/error.html");
string errorPage = Program.Resources.LoadSilent("pages/error.html");
if (errorPage != null){
resourceHandlerFactory.RegisterHandler(ErrorUrl, ResourceHandler.FromString(errorPage.Replace("{err}", BrowserUtils.GetErrorName(e.ErrorCode))));
string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
resourceHandlerFactory.RegisterHandler(ErrorUrl, ResourceHandler.FromString(errorPage.Replace("{err}", errorTitle)));
browser.Load(ErrorUrl);
}
}
@@ -217,7 +225,7 @@ namespace TweetDuck.Core{
// javascript calls
public void ReloadToTweetDeck(){
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUtils.TweetDeckURL}'");
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUrls.TweetDeck}'");
}
public void UpdateProperties(){
@@ -225,7 +233,7 @@ namespace TweetDuck.Core{
}
public void InjectBrowserCSS(){
browser.ExecuteScriptAsync("TDGF_injectBrowserCSS", ScriptLoader.LoadResource("styles/browser.css", browser)?.TrimEnd() ?? string.Empty);
browser.ExecuteScriptAsync("TDGF_injectBrowserCSS", Program.Resources.Load("styles/browser.css")?.TrimEnd() ?? string.Empty);
}
public void ReinjectCustomCSS(string css){

View File

@@ -7,7 +7,7 @@ using System.Windows.Forms;
using CefSharp.WinForms;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using TweetLib.Core.Utils;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Utils{
static class BrowserUtils{
@@ -60,8 +60,12 @@ namespace TweetDuck.Core.Utils{
}
public static void SetupZoomEvents(this ChromiumWebBrowser browser){
static void SetZoomLevel(IBrowserHost host, int percentage){
host.SetZoomLevel(Math.Log(percentage / 100.0, 1.2));
}
void UpdateZoomLevel(object sender, EventArgs args){
SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
SetZoomLevel(browser.GetBrowserHost(), Config.ZoomLevel);
}
Config.ZoomLevelChanged += UpdateZoomLevel;
@@ -69,7 +73,7 @@ namespace TweetDuck.Core.Utils{
browser.FrameLoadStart += (sender, args) => {
if (args.Frame.IsMain && Config.ZoomLevel != 100){
SetZoomLevel(args.Browser, Config.ZoomLevel);
SetZoomLevel(args.Browser.GetHost(), Config.ZoomLevel);
}
};
}
@@ -77,8 +81,8 @@ namespace TweetDuck.Core.Utils{
public static void OpenExternalBrowser(string url){
if (string.IsNullOrWhiteSpace(url))return;
switch(UrlUtils.Check(url)){
case UrlUtils.CheckResult.Fine:
switch(TwitterUrls.Check(url)){
case TwitterUrls.UrlType.Fine:
if (FormGuide.CheckGuideUrl(url, out string hash)){
FormGuide.Show(hash);
}
@@ -99,9 +103,9 @@ namespace TweetDuck.Core.Utils{
break;
case UrlUtils.CheckResult.Tracking:
case TwitterUrls.UrlType.Tracking:
if (Config.IgnoreTrackingUrlWarning){
goto case UrlUtils.CheckResult.Fine;
goto case TwitterUrls.UrlType.Fine;
}
using(FormMessage form = new FormMessage("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n" + url, MessageBoxIcon.Warning)){
@@ -117,20 +121,22 @@ namespace TweetDuck.Core.Utils{
}
if (result == DialogResult.Ignore || result == DialogResult.Yes){
goto case UrlUtils.CheckResult.Fine;
goto case TwitterUrls.UrlType.Fine;
}
}
break;
case UrlUtils.CheckResult.Invalid:
case TwitterUrls.UrlType.Invalid:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n" + url, FormMessage.OK);
break;
}
}
public static void OpenExternalSearch(string query){
if (string.IsNullOrWhiteSpace(query))return;
if (string.IsNullOrWhiteSpace(query)){
return;
}
string searchUrl = Config.SearchEngineUrl;
@@ -156,16 +162,8 @@ namespace TweetDuck.Core.Utils{
}
}
public static string GetErrorName(CefErrorCode code){
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
}
public static int Scale(int baseValue, double scaleFactor){
return (int)Math.Round(baseValue * scaleFactor);
}
public static void SetZoomLevel(IBrowser browser, int percentage){
browser.GetHost().SetZoomLevel(Math.Log(percentage/100.0, 1.2));
}
}
}

View File

@@ -2,7 +2,6 @@
using CefSharp;
using System.Drawing;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using TweetDuck.Core.Management;
using TweetDuck.Core.Other;
@@ -16,42 +15,17 @@ using Cookie = CefSharp.Cookie;
namespace TweetDuck.Core.Utils{
static class TwitterUtils{
public const string TweetDeckURL = "https://tweetdeck.twitter.com";
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153);
public const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
public static readonly ResourceLink LoadingSpinner = new ResourceLink("https://ton.twimg.com/tduck/spinner", ResourceHandler.FromByteArray(Properties.Resources.spinner, "image/apng"));
private static readonly Lazy<Regex> RegexAccountLazy = new Lazy<Regex>(() => new Regex(@"^https?://twitter\.com/(?!signup$|tos$|privacy$|search$|search-)([^/?]+)/?$", RegexOptions.Compiled), false);
public static Regex RegexAccount => RegexAccountLazy.Value;
public static readonly string[] DictionaryWords = {
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
};
public static bool IsTweetDeckWebsite(IFrame frame){
return frame.Url.Contains("//tweetdeck.twitter.com/");
}
public static bool IsTwitterWebsite(IFrame frame){
return frame.Url.Contains("//twitter.com/");
}
public static bool IsTwitterLogin2FactorWebsite(IFrame frame){
return frame.Url.Contains("//twitter.com/account/login_verification");
}
public static string GetMediaLink(string url, ImageQuality quality){
return ImageUrl.TryParse(url, out var obj) ? obj.WithQuality(quality) : url;
}
public static string GetImageFileName(string url){
return UrlUtils.GetFileNameFromUrl(ImageUrl.TryParse(url, out var obj) ? obj.WithNoQuality : url);
}
public static void ViewImage(string url, ImageQuality quality){
void ViewImageInternal(string path){
static void ViewImageInternal(string path){
string ext = Path.GetExtension(path);
if (ImageUrl.ValidExtensions.Contains(ext)){
@@ -62,13 +36,13 @@ namespace TweetDuck.Core.Utils{
}
}
string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
string file = Path.Combine(BrowserCache.CacheFolder, TwitterUrls.GetImageFileName(url) ?? Path.GetRandomFileName());
if (FileUtils.FileExistsAndNotEmpty(file)){
ViewImageInternal(file);
}
else{
DownloadFileAuth(GetMediaLink(url, quality), file, () => {
DownloadFileAuth(TwitterUrls.GetMediaLink(url, quality), file, () => {
ViewImageInternal(file);
}, ex => {
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
@@ -85,10 +59,10 @@ namespace TweetDuck.Core.Utils{
return;
}
string firstImageLink = GetMediaLink(urls[0], quality);
string firstImageLink = TwitterUrls.GetMediaLink(urls[0], quality);
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
string filename = GetImageFileName(firstImageLink);
string filename = TwitterUrls.GetImageFileName(firstImageLink);
string ext = Path.GetExtension(filename); // includes dot
using(SaveFileDialog dialog = new SaveFileDialog{
@@ -99,7 +73,7 @@ namespace TweetDuck.Core.Utils{
Filter = (urls.Length == 1 ? "Image" : "Images") + (string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){
if (dialog.ShowDialog() == DialogResult.OK){
void OnFailure(Exception ex){
static void OnFailure(Exception ex){
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
}
@@ -111,7 +85,7 @@ namespace TweetDuck.Core.Utils{
string pathExt = Path.GetExtension(dialog.FileName);
for(int index = 0; index < urls.Length; index++){
DownloadFileAuth(GetMediaLink(urls[index], quality), $"{pathBase} {index+1}{pathExt}", null, OnFailure);
DownloadFileAuth(TwitterUrls.GetMediaLink(urls[index], quality), $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
}
}
}
@@ -119,7 +93,7 @@ namespace TweetDuck.Core.Utils{
}
public static void DownloadVideo(string url, string username){
string filename = UrlUtils.GetFileNameFromUrl(url);
string filename = TwitterUrls.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename);
using(SaveFileDialog dialog = new SaveFileDialog{

View File

@@ -11,24 +11,17 @@ using Microsoft.Win32;
namespace TweetDuck.Core.Utils{
static class WindowsUtils{
private static readonly bool IsWindows8OrNewer = OSVersionEquals(major: 6, minor: 2); // windows 8/10
public static bool ShouldAvoidToolWindow { get; } = IsWindows8OrNewer;
public static bool IsAeroEnabled => IsWindows8OrNewer || (NativeMethods.DwmIsCompositionEnabled(out bool isCompositionEnabled) == 0 && isCompositionEnabled);
private static readonly Lazy<Regex> RegexStripHtmlStyles = new Lazy<Regex>(() => new Regex(@"\s?(?:style|class)="".*?"""), false);
private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false);
private static readonly bool IsWindows8OrNewer;
public static int CurrentProcessID { get; }
public static bool ShouldAvoidToolWindow { get; }
public static bool IsAeroEnabled => IsWindows8OrNewer || (NativeMethods.DwmIsCompositionEnabled(out bool isCompositionEnabled) == 0 && isCompositionEnabled);
static WindowsUtils(){
using(Process me = Process.GetCurrentProcess()){
CurrentProcessID = me.Id;
}
private static bool OSVersionEquals(int major, int minor){
Version ver = Environment.OSVersion.Version;
IsWindows8OrNewer = ver.Major == 6 && ver.Minor == 2; // windows 8/10
ShouldAvoidToolWindow = IsWindows8OrNewer;
return ver.Major == major && ver.Minor == minor;
}
public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
@@ -114,16 +107,18 @@ namespace TweetDuck.Core.Utils{
}
public static IEnumerable<Browser> FindInstalledBrowsers(){
IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive){
using(RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default))
using(RegistryKey browserList = root.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet", false)){
static IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive){
using RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default);
using RegistryKey browserList = root.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet", false);
if (browserList == null){
yield break;
}
foreach(string sub in browserList.GetSubKeyNames()){
using(RegistryKey browserKey = browserList.OpenSubKey(sub, false))
using(RegistryKey shellKey = browserKey?.OpenSubKey(@"shell\open\command")){
using RegistryKey browserKey = browserList.OpenSubKey(sub, false);
using RegistryKey shellKey = browserKey?.OpenSubKey(@"shell\open\command");
if (shellKey == null){
continue;
}
@@ -142,8 +137,6 @@ namespace TweetDuck.Core.Utils{
yield return new Browser(browserName, browserPath);
}
}
}
}
HashSet<Browser> browsers = new HashSet<Browser>();

58
Impl/LockHandler.cs Normal file
View File

@@ -0,0 +1,58 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using TweetDuck.Core.Utils;
using TweetLib.Core.Application;
namespace TweetDuck.Impl{
class LockHandler : IAppLockHandler{
private const int WaitRetryDelay = 250;
private const int RestoreFailTimeout = 2000;
private const int CloseNaturallyTimeout = 10000;
private const int CloseKillTimeout = 5000;
bool IAppLockHandler.RestoreProcess(Process process){
if (process.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)process.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)){
return true;
}
}
return false;
}
bool IAppLockHandler.CloseProcess(Process process){
try{
if (process.CloseMainWindow()){
// ReSharper disable once AccessToDisposedClosure
WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseNaturallyTimeout, WaitRetryDelay);
}
if (!process.HasExited){
process.Kill();
// ReSharper disable once AccessToDisposedClosure
WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseKillTimeout, WaitRetryDelay);
}
if (process.HasExited){
process.Dispose();
return true;
}
else{
return false;
}
}catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){
bool hasExited = CheckProcessExited(process);
process.Dispose();
return hasExited;
}
}
private static bool CheckProcessExited(Process process){
process.Refresh();
return process.HasExited;
}
}
}

16
Impl/SystemHandler.cs Normal file
View File

@@ -0,0 +1,16 @@
using System.Diagnostics;
using System.IO;
using TweetLib.Core.Application;
namespace TweetDuck.Impl{
class SystemHandler : IAppSystemHandler{
void IAppSystemHandler.OpenFileExplorer(string path){
if (File.Exists(path)){
using(Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")){}
}
else if (Directory.Exists(path)){
using(Process.Start("explorer.exe", '"' + path.Replace('/', '\\') + '"')){}
}
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using CefSharp;
using TweetDuck.Core.Adapters;
using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Plugins{
sealed class PluginDispatcher : IPluginDispatcher{
public event EventHandler<PluginDispatchEventArgs> Ready;
private readonly IWebBrowser browser;
private readonly IScriptExecutor executor;
public PluginDispatcher(IWebBrowser browser){
this.browser = browser;
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.executor = new CefScriptExecutor(browser);
}
void IPluginDispatcher.AttachBridge(string name, object bridge){
browser.RegisterAsyncJsObject(name, bridge);
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
IFrame frame = e.Frame;
if (frame.IsMain && TwitterUrls.IsTweetDeck(frame.Url)){
Ready?.Invoke(this, new PluginDispatchEventArgs(executor));
}
}
}
}

View File

@@ -1,180 +0,0 @@
using CefSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetDuck.Resources;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetDuck.Plugins{
sealed class PluginManager : IPluginManager{
private const string SetupScriptPrefix = "plugins.";
public string PathCustomPlugins => Path.Combine(pluginFolder, PluginGroup.Custom.GetSubFolder());
public IEnumerable<Plugin> Plugins => plugins;
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
public IPluginConfig Config { get; }
public event EventHandler<PluginErrorEventArgs> Reloaded;
public event EventHandler<PluginErrorEventArgs> Executed;
private readonly string pluginFolder;
private readonly string pluginDataFolder;
private readonly Control sync;
private readonly PluginBridge bridge;
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
private readonly Random rand = new Random();
private IWebBrowser mainBrowser;
public PluginManager(Control sync, IPluginConfig config, string pluginFolder, string pluginDataFolder){
this.Config = config;
this.Config.PluginChangedState += Config_PluginChangedState;
this.pluginFolder = pluginFolder;
this.pluginDataFolder = pluginDataFolder;
this.sync = sync;
this.bridge = new PluginBridge(this);
}
public void Register(IWebBrowser browser, PluginEnvironment environment, bool asMainBrowser = false){
browser.FrameLoadEnd += (sender, args) => {
IFrame frame = args.Frame;
if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){
ExecutePlugins(frame, environment);
}
};
browser.RegisterAsyncJsObject("$TDP", bridge);
if (asMainBrowser){
mainBrowser = browser;
}
}
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
mainBrowser?.ExecuteScriptAsync("TDPF_setPluginState", e.Plugin, e.IsEnabled);
}
public bool IsPluginInstalled(string identifier){
return plugins.Any(plugin => plugin.Identifier.Equals(identifier));
}
public bool HasAnyPlugin(PluginEnvironment environment){
return plugins.Any(plugin => plugin.Environments.HasFlag(environment));
}
public bool IsPluginConfigurable(Plugin plugin){
return plugin.HasConfig || bridge.WithConfigureFunction.Contains(plugin);
}
public void ConfigurePlugin(Plugin plugin){
if (bridge.WithConfigureFunction.Contains(plugin)){
mainBrowser?.ExecuteScriptAsync("TDPF_configurePlugin", plugin);
}
else if (plugin.HasConfig){
if (File.Exists(plugin.ConfigPath)){
using(Process.Start("explorer.exe", "/select,\"" + plugin.ConfigPath.Replace('/', '\\') + "\"")){}
}
else{
using(Process.Start("explorer.exe", '"' + plugin.GetPluginFolder(PluginFolder.Data).Replace('/', '\\') + '"')){}
}
}
}
public int GetTokenFromPlugin(Plugin plugin){
foreach(KeyValuePair<int, Plugin> kvp in tokens){
if (kvp.Value.Equals(plugin)){
return kvp.Key;
}
}
int token, attempts = 1000;
do{
token = rand.Next();
}while(tokens.ContainsKey(token) && --attempts >= 0);
if (attempts < 0){
token = -tokens.Count - 1;
}
tokens[token] = plugin;
return token;
}
public Plugin GetPluginFromToken(int token){
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
}
public void Reload(){
plugins.Clear();
tokens.Clear();
List<string> loadErrors = new List<string>(1);
foreach(var result in PluginGroupExtensions.Values.SelectMany(group => PluginLoader.AllInFolder(pluginFolder, pluginDataFolder, group))){
if (result.HasValue){
plugins.Add(result.Value);
}
else{
loadErrors.Add(result.Exception.Message);
}
}
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
}
private void ExecutePlugins(IFrame frame, PluginEnvironment environment){
if (!HasAnyPlugin(environment) || !ScriptLoader.ExecuteFile(frame, SetupScriptPrefix + environment.GetPluginScriptFile(), sync)){
return;
}
bool includeDisabled = environment.IncludesDisabledPlugins();
if (includeDisabled){
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GenerateConfig(Config), "gen:pluginconfig");
}
List<string> failedPlugins = new List<string>(1);
foreach(Plugin plugin in Plugins){
string path = plugin.GetScriptPath(environment);
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun){
continue;
}
string script;
try{
script = File.ReadAllText(path);
}catch(Exception e){
failedPlugins.Add($"{plugin.Identifier} ({Path.GetFileName(path)}): {e.Message}");
continue;
}
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, GetTokenFromPlugin(plugin), environment), $"plugin:{plugin}");
}
sync.InvokeAsyncSafe(() => {
Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
});
}
}
}

View File

@@ -12,7 +12,10 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other;
using TweetDuck.Core.Management;
using TweetDuck.Core.Utils;
using TweetDuck.Impl;
using TweetDuck.Resources;
using TweetLib.Core;
using TweetLib.Core.Application.Helpers;
using TweetLib.Core.Collections;
using TweetLib.Core.Utils;
@@ -50,6 +53,7 @@ namespace TweetDuck{
public static Reporter Reporter { get; }
public static ConfigManager Config { get; }
public static ScriptLoader Resources { get; }
static Program(){
Reporter = new Reporter(ErrorLogFilePath);
@@ -57,8 +61,17 @@ namespace TweetDuck{
Config = new ConfigManager();
#if DEBUG
Resources = new ScriptLoaderDebug();
#else
Resources = new ScriptLoader();
#endif
Lib.Initialize(new App.Builder{
ErrorHandler = Reporter
ErrorHandler = Reporter,
LockHandler = new LockHandler(),
SystemHandler = new SystemHandler(),
ResourceHandler = Resources
});
}
@@ -94,15 +107,17 @@ namespace TweetDuck{
LockManager.Result lockResult = LockManager.Lock();
if (lockResult == LockManager.Result.HasProcess){
if (!LockManager.RestoreLockingProcess(2000) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){
if (!LockManager.CloseLockingProcess(10000, 5000)){
if (!LockManager.RestoreLockingProcess() && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){
if (!LockManager.CloseLockingProcess()){
FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
return;
}
lockResult = LockManager.Lock();
}
else return;
else{
return;
}
}
if (lockResult != LockManager.Result.Success){
@@ -156,6 +171,7 @@ namespace TweetDuck{
Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser();
Resources.Initialize(mainForm);
Application.Run(mainForm);
if (mainForm.UpdateInstallerPath != null){

View File

@@ -1,45 +1,55 @@
using CefSharp;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other;
#if DEBUG
using System.Diagnostics;
using System.Reflection;
using TweetDuck.Core;
using TweetDuck.Plugins;
#endif
using TweetLib.Core.Application;
namespace TweetDuck.Resources{
static class ScriptLoader{
private static readonly Dictionary<string, string> CachedData = new Dictionary<string, string>(16);
class ScriptLoader : IAppResourceHandler{
private readonly Dictionary<string, string> cache = new Dictionary<string, string>(16);
private Control sync;
public static string LoadResourceSilent(string name){
return LoadResource(name, null);
public void Initialize(Control sync){
this.sync = sync;
}
public static string LoadResource(string name, Control sync){
if (CachedData.TryGetValue(name, out string resourceData)){
protected void ClearCache(){
cache.Clear();
}
public virtual void OnReloadTriggered(){
if (Control.ModifierKeys.HasFlag(Keys.Shift)){
ClearCache();
}
}
public string Load(string path) => LoadInternal(path, silent: false);
public string LoadSilent(string path) => LoadInternal(path, silent: true);
protected virtual string LocateFile(string path){
return Path.Combine(Program.ScriptPath, path);
}
private string LoadInternal(string path, bool silent){
if (sync == null){
throw new InvalidOperationException("Cannot use ScriptLoader before initialization.");
}
else if (sync.IsDisposed){
return null; // better than crashing I guess...?
}
if (cache.TryGetValue(path, out string resourceData)){
return resourceData;
}
string path = Program.ScriptPath;
#if DEBUG
if (Directory.Exists(HotSwapTargetDir)){
path = Path.Combine(HotSwapTargetDir, "scripts");
Debug.WriteLine("Hot swap active, redirecting "+name);
}
#endif
string location = LocateFile(path);
string resource;
try{
string contents = File.ReadAllText(Path.Combine(path, name), Encoding.UTF8);
string contents = File.ReadAllText(location, Encoding.UTF8);
int separator;
// first line can be either:
@@ -47,7 +57,7 @@ namespace TweetDuck.Resources{
// #<version>\n
if (contents[0] != '#'){
ShowLoadError(sync, $"File {name} appears to be corrupted, please try reinstalling the app.");
ShowLoadError(silent ? null : sync, $"File {path} appears to be corrupted, please try reinstalling the app.");
separator = 0;
}
else{
@@ -55,112 +65,21 @@ namespace TweetDuck.Resources{
string fileVersion = contents.Substring(1, separator - 1).TrimEnd();
if (fileVersion != Program.VersionTag){
ShowLoadError(sync, $"File {name} is made for a different version of TweetDuck ({fileVersion}) and may not function correctly in this version, please try reinstalling the app.");
ShowLoadError(silent ? null : sync, $"File {path} is made for a different version of TweetDuck ({fileVersion}) and may not function correctly in this version, please try reinstalling the app.");
}
}
resource = contents.Substring(separator).TrimStart();
}catch(Exception ex){
ShowLoadError(sync, $"Could not load {name}. The program will continue running with limited functionality.\n\n{ex.Message}");
ShowLoadError(silent ? null : sync, $"Could not load {path}. The program will continue running with limited functionality.\n\n{ex.Message}");
resource = null;
}
return CachedData[name] = resource;
}
public static bool ExecuteFile(IFrame frame, string file, Control sync){
string script = LoadResource(file, sync);
ExecuteScript(frame, script, "root:"+Path.GetFileNameWithoutExtension(file));
return script != null;
}
public static void ExecuteScript(IFrame frame, string script, string identifier){
if (script != null){
frame.ExecuteJavaScriptAsync(script, identifier, 1);
}
}
public static void ClearCache(){
CachedData.Clear();
return cache[path] = resource;
}
private static void ShowLoadError(Control sync, string message){
sync?.InvokeSafe(() => FormMessage.Error("Resource Error", message, FormMessage.OK));
}
#if DEBUG
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../")));
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe");
static ScriptLoader(){
if (File.Exists(HotSwapRebuildScript)){
Debug.WriteLine("Activating resource hot swap...");
ResetHotSwap();
Application.ApplicationExit += (sender, args) => ResetHotSwap();
}
}
public static void HotSwap(){
if (!File.Exists(HotSwapRebuildScript)){
Debug.WriteLine("Failed resource hot swap, missing rebuild script: "+HotSwapRebuildScript);
return;
}
ResetHotSwap();
Directory.CreateDirectory(HotSwapTargetDir);
Stopwatch sw = Stopwatch.StartNew();
using(Process process = Process.Start(new ProcessStartInfo{
FileName = HotSwapRebuildScript,
Arguments = $"\"{HotSwapTargetDir}\\\" \"{HotSwapProjectRoot}\\\" \"Debug\" \"{Program.VersionTag}\"",
WindowStyle = ProcessWindowStyle.Hidden
})){
// ReSharper disable once PossibleNullReferenceException
if (!process.WaitForExit(8000)){
Debug.WriteLine("Failed resource hot swap, script did not finish in time");
return;
}
else if (process.ExitCode != 0){
Debug.WriteLine("Failed resource hot swap, script exited with code "+process.ExitCode);
return;
}
}
sw.Stop();
Debug.WriteLine("Finished rebuild script in "+sw.ElapsedMilliseconds+" ms");
ClearCache();
// Force update plugin manager setup scripts
string newPluginRoot = Path.Combine(HotSwapTargetDir, "plugins");
const BindingFlags flagsInstance = BindingFlags.Instance | BindingFlags.NonPublic;
Type typePluginManager = typeof(PluginManager);
Type typeFormBrowser = typeof(FormBrowser);
// ReSharper disable PossibleNullReferenceException
object instPluginManager = typeFormBrowser.GetField("plugins", flagsInstance).GetValue(FormManager.TryFind<FormBrowser>());
typePluginManager.GetField("pluginFolder", flagsInstance).SetValue(instPluginManager, newPluginRoot);
Debug.WriteLine("Reloading hot swapped plugins...");
((PluginManager)instPluginManager).Reload();
// ReSharper restore PossibleNullReferenceException
}
private static void ResetHotSwap(){
try{
Directory.Delete(HotSwapTargetDir, true);
}catch(DirectoryNotFoundException){}
}
private static string FixPathSlash(string path){
return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)+'\\';
}
#endif
}
}

View File

@@ -0,0 +1,100 @@
#if DEBUG
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using TweetDuck.Core;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Resources{
sealed class ScriptLoaderDebug : ScriptLoader{
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../")));
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe");
private static string FixPathSlash(string path){
return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + '\\';
}
public ScriptLoaderDebug(){
if (File.Exists(HotSwapRebuildScript)){
Debug.WriteLine("Activating resource hot swap...");
ResetHotSwap();
Application.ApplicationExit += (sender, args) => ResetHotSwap();
}
}
public override void OnReloadTriggered(){
HotSwap();
}
protected override string LocateFile(string path){
if (Directory.Exists(HotSwapTargetDir)){
Debug.WriteLine($"Hot swap active, redirecting {path}");
return Path.Combine(HotSwapTargetDir, "scripts", path);
}
return base.LocateFile(path);
}
private void HotSwap(){
if (!File.Exists(HotSwapRebuildScript)){
Debug.WriteLine($"Failed resource hot swap, missing rebuild script: {HotSwapRebuildScript}");
return;
}
ResetHotSwap();
Directory.CreateDirectory(HotSwapTargetDir);
Stopwatch sw = Stopwatch.StartNew();
using(Process process = Process.Start(new ProcessStartInfo{
FileName = HotSwapRebuildScript,
Arguments = $"\"{HotSwapTargetDir}\\\" \"{HotSwapProjectRoot}\\\" \"Debug\" \"{Program.VersionTag}\"",
WindowStyle = ProcessWindowStyle.Hidden
})){
// ReSharper disable once PossibleNullReferenceException
if (!process.WaitForExit(8000)){
Debug.WriteLine("Failed resource hot swap, script did not finish in time");
return;
}
else if (process.ExitCode != 0){
Debug.WriteLine($"Failed resource hot swap, script exited with code {process.ExitCode}");
return;
}
}
sw.Stop();
Debug.WriteLine($"Finished rebuild script in {sw.ElapsedMilliseconds} ms");
ClearCache();
// Force update plugin manager setup scripts
string newPluginRoot = Path.Combine(HotSwapTargetDir, "plugins");
const BindingFlags flagsInstance = BindingFlags.Instance | BindingFlags.NonPublic;
Type typePluginManager = typeof(PluginManager);
Type typeFormBrowser = typeof(FormBrowser);
// ReSharper disable PossibleNullReferenceException
object instPluginManager = typeFormBrowser.GetField("plugins", flagsInstance).GetValue(FormManager.TryFind<FormBrowser>());
typePluginManager.GetField("pluginFolder", flagsInstance).SetValue(instPluginManager, newPluginRoot);
Debug.WriteLine("Reloading hot swapped plugins...");
((PluginManager)instPluginManager).Reload();
// ReSharper restore PossibleNullReferenceException
}
private void ResetHotSwap(){
try{
Directory.Delete(HotSwapTargetDir, true);
}catch(DirectoryNotFoundException){}
}
}
}
#endif

View File

@@ -1457,6 +1457,24 @@
};
}
//
// Block: Add missing languages for Bing Translator (Bengali, Icelandic, Tagalog, Tamil, Telugu, Urdu).
//
if (ensurePropertyExists(TD, "languages", "getSupportedTranslationSourceLanguages")){
const newCodes = [ "bn", "is", "tl", "ta", "te", "ur" ];
const codeSet = new Set(TD.languages.getSupportedTranslationSourceLanguages());
for(const lang of newCodes){
codeSet.add(lang);
}
const codeList = [...codeSet];
TD.languages.getSupportedTranslationSourceLanguages = function(){
return codeList;
};
}
//
// Block: Setup global function to refresh all columns.
//

View File

@@ -40,78 +40,50 @@
/* General styling */
/*******************/
* {
border-radius: 0 !important;
}
body {
/* remove scrollbar */
overflow: hidden !important;
}
.page-canvas {
/* tweak page shadow */
.page-canvas, div[tweetduck-login-wrapper], body.ResponsiveLayout > div.PageContainer > div.Section {
box-shadow: 0 0 150px rgba(255, 255, 255, 0.3) !important;
}
.topbar, .TopNav {
/* hide top bar */
display: none !important;
}
.page-canvas, .buttons, .btn, input {
/* sharpen borders */
border-radius: 0 !important;
}
input {
/* tweak input padding */
padding: 5px 8px 4px !important;
}
button[type='submit'] {
/* style buttons */
border: 1px solid rgba(0, 0, 0, 0.3) !important;
border-radius: 0 !important;
}
.tweetduck-helper {
/* custom login text */
/****************************/
/* General per-page styling */
/****************************/
html[mobile][login] div[tweetduck-login-wrapper] {
/* vertically center page & fix colors */
margin-top: calc(50vh - 200px);
padding: 26px 1.1vw;
background-color: white;
}
html[mobile][login] #tweetduck-helper:hover {
text-decoration: underline;
}
html[desktop][login] #tweetduck-helper {
margin-top: 15px !important;
font-weight: bold !important;
}
/********************************************/
/* Fix min width and margins on logout page */
/********************************************/
html[logout] .page-canvas {
width: auto !important;
max-width: 888px;
}
html[logout] .signout-wrapper {
width: auto !important;
margin: 0 auto !important;
}
html[logout] .signout {
margin: 60px 0 54px !important;
}
html[logout] .buttons {
padding-bottom: 0 !important;
}
/*******************************/
/* General logout page styling */
/*******************************/
html[logout] .aside {
/* hide elements around dialog */
display: none;
}
html[logout] .buttons button, html[logout] .buttons a {
/* style buttons */
display: inline-block;
margin: 0 4px !important;
html[mobile][logout] div[role="button"] {
border: 1px solid rgba(0, 0, 0, 0.3) !important;
border-radius: 0 !important;
}

View File

@@ -1,4 +1,8 @@
(function(){
const isLogin = location.pathname === "/login";
const isLogout = location.pathname === "/logout";
const isMobile = location.host === "mobile.twitter.com";
//
// Function: Inject custom CSS into the page.
//
@@ -14,18 +18,76 @@
document.head.appendChild(link);
if (location.pathname === "/logout"){
if (isLogin){
document.documentElement.setAttribute("login", "");
}
else if (isLogout){
document.documentElement.setAttribute("logout", "");
}
if (isMobile){
document.documentElement.setAttribute("mobile", "");
}
else{
document.documentElement.setAttribute("desktop", "");
}
};
setTimeout(injectCSS, 1);
//
// Block: Make login page links external.
// Function: Trigger once element exists.
//
if (location.pathname === "/login"){
const triggerWhenExists = function(query, callback){
let id = window.setInterval(function(){
let ele = document.querySelector(query);
if (ele && callback(ele)){
window.clearInterval(id);
}
}, 5);
};
//
// Block: Add profile import button & enable custom styling, make page links external on old login page.
//
if (isLogin){
document.addEventListener("DOMContentLoaded", function(){
if (isMobile){
triggerWhenExists("main h1", function(heading){
heading.parentNode.setAttribute("tweetduck-login-wrapper", "");
return true;
});
triggerWhenExists("a[href='/i/flow/signup']", function(texts){
texts = texts.parentNode;
let link = texts.childNodes[0];
let separator = texts.childNodes[1];
if (link && separator){
texts.classList.add("tweetduck-login-links");
link = link.cloneNode(false);
link.id = "tweetduck-helper";
link.href = "#";
link.innerText = "Import TweetDuck profile";
texts.appendChild(separator.cloneNode(true));
texts.appendChild(link);
link.addEventListener("click", function(){
$TD.openProfileImport();
});
return true;
}
else{
return false;
}
});
}
else{
const openLinkExternally = function(e){
let href = e.currentTarget.getAttribute("href");
$TD.openBrowser(href[0] === '/' ? location.origin+href : href);
@@ -41,24 +103,34 @@
let texts = document.querySelector(".page-canvas > div:last-child");
if (texts){
texts.insertAdjacentHTML("beforeend", `<p class="tweetduck-helper">Used the TweetDuck app before? <a href="#">Import your profile »</a></p>`);
texts.insertAdjacentHTML("beforeend", `<p id="tweetduck-helper">Used the TweetDuck app before? <a href="#">Import your profile »</a></p>`);
texts.querySelector(".tweetduck-helper > a").addEventListener("click", function(){
texts.querySelector("#tweetduck-helper > a").addEventListener("click", function(){
$TD.openProfileImport();
});
}
}
});
}
//
// Block: Fix broken Cancel button on logout page.
//
else if (location.pathname === "/logout"){
document.addEventListener("DOMContentLoaded", function(){
let cancel = document.querySelector(".buttons .cancel");
if (cancel && cancel.tagName === "A"){
cancel.href = "https://tweetdeck.twitter.com/";
//
// Block: Hide cookie crap.
//
if (isMobile){
document.addEventListener("DOMContentLoaded", function(){
triggerWhenExists("a[href^='https://help.twitter.com/rules-and-policies/twitter-cookies']", function(cookie){
while(!!cookie){
if (cookie.offsetHeight > 30){
cookie.remove();
return true;
}
else{
cookie = cookie.parentNode;
}
}
return false;
});
});
}
})();

View File

@@ -92,7 +92,6 @@
});
onClick(ele.querySelector(".tdu-btn-later"), function(){
$TDU.onUpdateDelayed();
exitSlide();
});

View File

@@ -55,9 +55,9 @@
<ItemGroup>
<Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\ConfigManager.cs" />
<Compile Include="Configuration\LockManager.cs" />
<Compile Include="Configuration\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Core\Adapters\CefScriptExecutor.cs" />
<Compile Include="Core\Bridge\PropertyBridge.cs" />
<Compile Include="Core\Bridge\UpdateBridge.cs" />
<Compile Include="Core\Controls\ControlExtensions.cs" />
@@ -121,7 +121,6 @@
<DependentUpon>FormNotificationTweet.cs</DependentUpon>
</Compile>
<Compile Include="Core\Notification\SoundNotification.cs" />
<Compile Include="Core\Notification\TweetNotification.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsFile.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsManager.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsReport.cs" />
@@ -238,23 +237,26 @@
<Compile Include="Core\Other\FormSettings.Designer.cs">
<DependentUpon>FormSettings.cs</DependentUpon>
</Compile>
<Compile Include="Impl\LockHandler.cs" />
<Compile Include="Impl\SystemHandler.cs" />
<Compile Include="Plugins\PluginControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Plugins\PluginControl.Designer.cs">
<DependentUpon>PluginControl.cs</DependentUpon>
</Compile>
<Compile Include="Plugins\PluginListFlowLayout.cs">
<Compile Include="Plugins\PluginDispatcher.cs" />
<Compile Include="Core\Controls\FlowLayoutPanelNoHScroll.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Plugins\PluginManager.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Reporter.cs" />
<Compile Include="Resources\ScriptLoaderDebug.cs" />
<Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType>
</Compile>
@@ -394,7 +396,7 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
<Exec Command="start &quot;&quot; /B &quot;ISCC.exe&quot; /Q &quot;$(ProjectDir)bld\gen_upd.iss&quot;" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
</Target>
<PropertyGroup>
<PreBuildEvent>powershell Get-Process TweetDuck.Browser -ErrorAction SilentlyContinue ^| Where-Object {$_.Path -eq '$(TargetDir)TweetDuck.Browser.exe'} ^| Stop-Process; Exit 0</PreBuildEvent>
<PreBuildEvent>powershell -NoProfile -Command "$ErrorActionPreference = 'SilentlyContinue'; (Get-Process TweetDuck.Browser | Where-Object {$_.Path -eq '$(TargetDir)TweetDuck.Browser.exe'}).Kill(); Exit 0"</PreBuildEvent>
</PropertyGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>

View File

@@ -12,7 +12,7 @@ namespace TweetDuck.Updates{
this.updateInfo = info;
Text = "Updating " + Program.BrandName;
labelDescription.Text = "Downloading version "+info.VersionTag+"...";
labelDescription.Text = $"Downloading version {info.VersionTag}...";
timerDownloadCheck.Start();
}

View File

@@ -49,11 +49,11 @@ namespace TweetDuck.Updates{
}
private UpdateInfo ParseFromJson(string json){
bool IsUpdaterAsset(JsonObject obj){
static bool IsUpdaterAsset(JsonObject obj){
return UpdaterAssetName == (string)obj["name"];
}
string AssetDownloadUrl(JsonObject obj){
static string AssetDownloadUrl(JsonObject obj){
return (string)obj["browser_download_url"];
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -11,6 +11,7 @@
<RootNamespace>TweetLib.Communication</RootNamespace>
<AssemblyName>TweetLib.Communication</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
@@ -23,7 +24,6 @@
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
@@ -33,7 +33,6 @@
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>7</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />

View File

@@ -4,16 +4,25 @@ using TweetLib.Core.Application;
namespace TweetLib.Core{
public sealed class App{
public static IAppErrorHandler ErrorHandler { get; private set; }
public static IAppLockHandler LockHandler { get; private set; }
public static IAppSystemHandler SystemHandler { get; private set; }
public static IAppResourceHandler ResourceHandler { get; private set; }
// Builder
public sealed class Builder{
public IAppErrorHandler? ErrorHandler { get; set; }
public IAppLockHandler? LockHandler { get; set; }
public IAppSystemHandler? SystemHandler { get; set; }
public IAppResourceHandler? ResourceHandler { get; set; }
// Validation
internal void Initialize(){
App.ErrorHandler = Validate(ErrorHandler, nameof(ErrorHandler))!;
App.LockHandler = Validate(LockHandler, nameof(LockHandler))!;
App.SystemHandler = Validate(SystemHandler, nameof(SystemHandler))!;
App.ResourceHandler = Validate(ResourceHandler, nameof(ResourceHandler))!;
}
private T Validate<T>(T obj, string name){

View File

@@ -1,12 +1,11 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using TweetDuck.Core.Utils;
namespace TweetDuck.Configuration{
sealed class LockManager{
namespace TweetLib.Core.Application.Helpers{
public sealed class LockManager{
private const int RetryDelay = 250;
public enum Result{
@@ -14,8 +13,8 @@ namespace TweetDuck.Configuration{
}
private readonly string file;
private FileStream lockStream;
private Process lockingProcess;
private FileStream? lockStream;
private Process? lockingProcess;
public LockManager(string file){
this.file = file;
@@ -37,7 +36,7 @@ namespace TweetDuck.Configuration{
private Result TryCreateLockFile(){
void CreateLockFileStream(){
lockStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read);
lockStream.Write(BitConverter.GetBytes(WindowsUtils.CurrentProcessID), 0, sizeof(int));
lockStream.Write(BitConverter.GetBytes(CurrentProcessID), 0, sizeof(int));
lockStream.Flush(true);
}
@@ -82,14 +81,12 @@ namespace TweetDuck.Configuration{
try{
Process foundProcess = Process.GetProcessById(pid);
using(Process currentProcess = Process.GetCurrentProcess()){
if (foundProcess.MainModule.FileVersionInfo.InternalName == currentProcess.MainModule.FileVersionInfo.InternalName){
if (MatchesCurrentProcess(foundProcess)){
lockingProcess = foundProcess;
}
else{
foundProcess.Close();
}
}
}catch{
// GetProcessById throws ArgumentException if the process is missing
// Process.MainModule can throw exceptions in some cases
@@ -124,7 +121,7 @@ namespace TweetDuck.Configuration{
try{
File.Delete(file);
}catch(Exception e){
Program.Reporter.LogImportant(e.ToString());
App.ErrorHandler.Log(e.ToString());
return false;
}
}
@@ -134,50 +131,32 @@ namespace TweetDuck.Configuration{
// Locking process
public bool RestoreLockingProcess(int failTimeout){
if (lockingProcess != null && lockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)lockingProcess.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckLockingProcessExited() || (lockingProcess.MainWindowHandle != IntPtr.Zero && lockingProcess.Responding), failTimeout, RetryDelay)){
return true;
}
public bool RestoreLockingProcess(){
return lockingProcess != null && App.LockHandler.RestoreProcess(lockingProcess);
}
return false;
}
public bool CloseLockingProcess(int closeTimeout, int killTimeout){
if (lockingProcess != null){
try{
if (lockingProcess.CloseMainWindow()){
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, closeTimeout, RetryDelay);
}
if (!lockingProcess.HasExited){
lockingProcess.Kill();
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, killTimeout, RetryDelay);
}
if (lockingProcess.HasExited){
lockingProcess.Dispose();
public bool CloseLockingProcess(){
if (lockingProcess != null && App.LockHandler.CloseProcess(lockingProcess)){
lockingProcess = null;
return true;
}
}catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){
if (lockingProcess != null){
bool hasExited = CheckLockingProcessExited();
lockingProcess.Dispose();
return hasExited;
}
}
}
return false;
}
private bool CheckLockingProcessExited(){
lockingProcess.Refresh();
return lockingProcess.HasExited;
// Utilities
private static int CurrentProcessID{
get{
using Process me = Process.GetCurrentProcess();
return me.Id;
}
}
[SuppressMessage("ReSharper", "PossibleNullReferenceException")]
private static bool MatchesCurrentProcess(Process process){
using Process current = Process.GetCurrentProcess();
return current.MainModule.FileVersionInfo.InternalName == process.MainModule.FileVersionInfo.InternalName;
}
}
}

View File

@@ -0,0 +1,8 @@
using System.Diagnostics;
namespace TweetLib.Core.Application{
public interface IAppLockHandler{
bool RestoreProcess(Process process);
bool CloseProcess(Process process);
}
}

View File

@@ -0,0 +1,5 @@
namespace TweetLib.Core.Application{
public interface IAppResourceHandler{
string? Load(string path);
}
}

View File

@@ -0,0 +1,5 @@
namespace TweetLib.Core.Application{
public interface IAppSystemHandler{
void OpenFileExplorer(string path);
}
}

View File

@@ -0,0 +1,7 @@
namespace TweetLib.Core.Browser{
public interface IScriptExecutor{
void RunFunction(string name, params object[] args);
void RunScript(string identifier, string script);
bool RunFile(string file);
}
}

View File

@@ -1,15 +1,9 @@
using System;
using System.Text;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Bridge;
using TweetDuck.Data;
using TweetDuck.Resources;
namespace TweetDuck.Core.Notification{
sealed class TweetNotification{
namespace TweetLib.Core.Features.Notifications{
public sealed class DesktopNotification{
private const string DefaultHeadLayout = @"<html class=""scroll-v os-windows dark txt-size--14"" lang=""en-US"" id=""tduck"" data-td-font=""medium"" data-td-theme=""dark""><head><meta charset=""utf-8""><link href=""https://ton.twimg.com/tweetdeck-web/web/dist/bundle.4b1f87e09d.css"" rel=""stylesheet""><style type='text/css'>body { background: rgb(34, 36, 38) !important }</style>";
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandler.FromByteArray(Properties.Resources.avatar, "image/png"));
public enum Position{
TopLeft, TopRight, BottomLeft, BottomRight, Custom
@@ -29,7 +23,7 @@ namespace TweetDuck.Core.Notification{
private readonly string html;
private readonly int characters;
public TweetNotification(string columnId, string chirpId, string title, string html, int characters, string tweetUrl, string quoteUrl){
public DesktopNotification(string columnId, string chirpId, string title, string html, int characters, string tweetUrl, string quoteUrl){
this.ColumnId = columnId;
this.ChirpId = chirpId;
@@ -45,18 +39,19 @@ namespace TweetDuck.Core.Notification{
return 2000 + Math.Max(1000, value * characters);
}
public string GenerateHtml(string bodyClasses, Control sync){
string headLayout = TweetDeckBridge.NotificationHeadLayout ?? DefaultHeadLayout;
string mainCSS = ScriptLoader.LoadResource("styles/notification.css", sync) ?? string.Empty;
string customCSS = Program.Config.User.CustomNotificationCSS ?? string.Empty;
public string GenerateHtml(string bodyClasses, string? headLayout, string? customStyles){ // TODO
headLayout ??= DefaultHeadLayout;
customStyles ??= string.Empty;
StringBuilder build = new StringBuilder(320 + headLayout.Length + mainCSS.Length + customCSS.Length + html.Length);
string mainCSS = App.ResourceHandler.Load("styles/notification.css") ?? string.Empty;
StringBuilder build = new StringBuilder(320 + headLayout.Length + mainCSS.Length + customStyles.Length + html.Length);
build.Append("<!DOCTYPE html>");
build.Append(headLayout);
build.Append("<style type='text/css'>").Append(mainCSS).Append("</style>");
if (!string.IsNullOrWhiteSpace(customCSS)){
build.Append("<style type='text/css'>").Append(customCSS).Append("</style>");
if (!string.IsNullOrWhiteSpace(customStyles)){
build.Append("<style type='text/css'>").Append(customStyles).Append("</style>");
}
build.Append("</head><body class='scroll-styled-v");

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
using System;
using TweetLib.Core.Browser;
namespace TweetLib.Core.Features.Plugins.Events{
public sealed class PluginDispatchEventArgs : EventArgs{
public IScriptExecutor Executor { get; }
public PluginDispatchEventArgs(IScriptExecutor executor){
this.Executor = executor;
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetLib.Core.Features.Plugins{
public interface IPluginDispatcher{
event EventHandler<PluginDispatchEventArgs> Ready;
void AttachBridge(string name, object bridge);
}
}

View File

@@ -1,14 +0,0 @@
using System;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetLib.Core.Features.Plugins{
public interface IPluginManager{
IPluginConfig Config { get; }
event EventHandler<PluginErrorEventArgs> Reloaded;
int GetTokenFromPlugin(Plugin plugin);
Plugin GetPluginFromToken(int token);
}
}

View File

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

View File

@@ -11,29 +11,56 @@ using TweetLib.Core.Utils;
namespace TweetLib.Core.Features.Plugins{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public sealed class PluginBridge{
private readonly IPluginManager manager;
internal sealed class PluginBridge{
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
private readonly Random rand = new Random();
private readonly FileCache fileCache = new FileCache();
private readonly TwoKeyDictionary<int, string, InjectedHTML> notificationInjections = new TwoKeyDictionary<int, string, InjectedHTML>(4, 1);
public IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
public ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
internal IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
internal ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
public PluginBridge(IPluginManager manager){
this.manager = manager;
this.manager.Reloaded += manager_Reloaded;
this.manager.Config.PluginChangedState += Config_PluginChangedState;
public PluginBridge(PluginManager manager){
manager.Reloaded += manager_Reloaded;
manager.Config.PluginChangedState += Config_PluginChangedState;
}
internal int GetTokenFromPlugin(Plugin plugin){
foreach(KeyValuePair<int, Plugin> kvp in tokens){
if (kvp.Value.Equals(plugin)){
return kvp.Key;
}
}
int token, attempts = 1000;
do{
token = rand.Next();
}while(tokens.ContainsKey(token) && --attempts >= 0);
if (attempts < 0){
token = -tokens.Count - 1;
}
tokens[token] = plugin;
return token;
}
private Plugin? GetPluginFromToken(int token){
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
}
// Event handlers
private void manager_Reloaded(object sender, PluginErrorEventArgs e){
tokens.Clear();
fileCache.Clear();
}
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
if (!e.IsEnabled){
int token = manager.GetTokenFromPlugin(e.Plugin);
int token = GetTokenFromPlugin(e.Plugin);
fileCache.Remove(token);
notificationInjections.Remove(token);
@@ -43,7 +70,7 @@ namespace TweetLib.Core.Features.Plugins{
// Utility methods
private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
Plugin plugin = manager.GetPluginFromToken(token);
Plugin? plugin = GetPluginFromToken(token);
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(folder, path);
if (fullPath.Length == 0){
@@ -116,7 +143,7 @@ namespace TweetLib.Core.Features.Plugins{
}
public void SetConfigurable(int token){
Plugin plugin = manager.GetPluginFromToken(token);
Plugin? plugin = GetPluginFromToken(token);
if (plugin != null){
WithConfigureFunction.Add(plugin);

View File

@@ -79,7 +79,7 @@ namespace TweetLib.Core.Features.Plugins{
}
private static PluginEnvironment EnvironmentFromFileName(string file){
return PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
return PluginEnvironments.All.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
}
private static void SetProperty(Plugin.Builder builder, string tag, string value){

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TweetLib.Core.Browser;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetLib.Core.Features.Plugins{
public sealed class PluginManager{
public string PathCustomPlugins => Path.Combine(pluginFolder, PluginGroup.Custom.GetSubFolder());
public IEnumerable<Plugin> Plugins => plugins;
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
public IPluginConfig Config { get; }
public event EventHandler<PluginErrorEventArgs> Reloaded;
public event EventHandler<PluginErrorEventArgs> Executed;
private readonly string pluginFolder;
private readonly string pluginDataFolder;
private readonly PluginBridge bridge;
private IScriptExecutor? browserExecutor;
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
public PluginManager(IPluginConfig config, string pluginFolder, string pluginDataFolder){
this.Config = config;
this.Config.PluginChangedState += Config_PluginChangedState;
this.pluginFolder = pluginFolder;
this.pluginDataFolder = pluginDataFolder;
this.bridge = new PluginBridge(this);
}
public void Register(PluginEnvironment environment, IPluginDispatcher dispatcher){
dispatcher.AttachBridge("$TDP", bridge);
dispatcher.Ready += (sender, args) => {
IScriptExecutor executor = args.Executor;
if (environment == PluginEnvironment.Browser){
browserExecutor = executor;
}
Execute(environment, executor);
};
}
public void Reload(){
plugins.Clear();
List<string> errors = new List<string>(1);
foreach(var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(pluginFolder, pluginDataFolder, group))){
if (result.HasValue){
plugins.Add(result.Value);
}
else{
errors.Add(result.Exception.Message);
}
}
Reloaded?.Invoke(this, new PluginErrorEventArgs(errors));
}
private void Execute(PluginEnvironment environment, IScriptExecutor executor){
if (!plugins.Any(plugin => plugin.HasEnvironment(environment)) || !executor.RunFile($"plugins.{environment.GetPluginScriptFile()}")){
return;
}
bool includeDisabled = environment == PluginEnvironment.Browser;
if (includeDisabled){
executor.RunScript("gen:pluginconfig", PluginScriptGenerator.GenerateConfig(Config));
}
List<string> errors = new List<string>(1);
foreach(Plugin plugin in Plugins){
string path = plugin.GetScriptPath(environment);
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun){
continue;
}
string script;
try{
script = File.ReadAllText(path);
}catch(Exception e){
errors.Add($"{plugin.Identifier} ({Path.GetFileName(path)}): {e.Message}");
continue;
}
executor.RunScript($"plugin:{plugin}", PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, bridge.GetTokenFromPlugin(plugin), environment));
}
Executed?.Invoke(this, new PluginErrorEventArgs(errors));
}
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
browserExecutor?.RunFunction("TDPF_setPluginState", e.Plugin, e.IsEnabled);
}
public bool IsPluginConfigurable(Plugin plugin){
return plugin.HasConfig || bridge.WithConfigureFunction.Contains(plugin);
}
public void ConfigurePlugin(Plugin plugin){
if (bridge.WithConfigureFunction.Contains(plugin) && browserExecutor != null){
browserExecutor.RunFunction("TDPF_configurePlugin", plugin);
}
else if (plugin.HasConfig){
App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(PluginFolder.Data));
}
}
}
}

View File

@@ -5,7 +5,7 @@ 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);
private static readonly Regex RegexImageUrlParams = new Regex(@"(format|name)=(\w+)", RegexOptions.IgnoreCase);
public static readonly string[] ValidExtensions = {
".jpg", ".jpeg", ".png", ".gif"

View File

@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace TweetLib.Core.Features.Twitter{
public static class TwitterUrls{
public const string TweetDeck = "https://tweetdeck.twitter.com";
private const string TwitterTrackingUrl = "t.co";
public static Regex RegexAccount { get; } = new Regex(@"^https?://twitter\.com/(?!signup$|tos$|privacy$|search$|search-)([^/?]+)/?$");
public static bool IsTweetDeck(string url){
return url.Contains("//tweetdeck.twitter.com/");
}
public static bool IsTwitter(string url){
return url.Contains("//twitter.com/") || url.Contains("//mobile.twitter.com/");
}
public static bool IsTwitterLogin2Factor(string url){
return url.Contains("//twitter.com/account/login_verification") || url.Contains("//mobile.twitter.com/account/login_verification");
}
public static string? GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
public static string GetMediaLink(string url, ImageQuality quality){
return ImageUrl.TryParse(url, out var obj) ? obj.WithQuality(quality) : url;
}
public static string? GetImageFileName(string url){
return GetFileNameFromUrl(ImageUrl.TryParse(url, out var obj) ? obj.WithNoQuality : url);
}
public enum UrlType{
Invalid, Tracking, Fine
}
public static UrlType Check(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? UrlType.Tracking : UrlType.Fine;
}
}
return UrlType.Invalid;
}
}
}

View File

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

View File

@@ -1,29 +0,0 @@
using System;
using System.IO;
namespace TweetLib.Core.Utils{
public static class UrlUtils{
private const string TwitterTrackingUrl = "t.co";
public enum CheckResult{
Invalid, Tracking, Fine
}
public static CheckResult Check(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? CheckResult.Tracking : CheckResult.Fine;
}
}
return CheckResult.Invalid;
}
public static string? GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
}
}

View File

@@ -0,0 +1,187 @@
namespace TweetTest.Core.TestTwitterUrls
open Xunit
open TweetLib.Core.Features.Twitter
module Check =
type Result = TwitterUrls.UrlType
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, TwitterUrls.Check("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, TwitterUrls.Check("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, TwitterUrls.Check(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, TwitterUrls.Check("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, TwitterUrls.Check("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", TwitterUrls.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", TwitterUrls.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", TwitterUrls.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", TwitterUrls.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(TwitterUrls.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(TwitterUrls.GetFileNameFromUrl("http://example.com/path/"))
module GetMediaLink_Default =
let getMediaLinkDefault url = TwitterUrls.GetMediaLink(url, ImageQuality.Default)
let domain = "https://pbs.twimg.com"
[<Fact>]
let ``does not modify URL w/o extension`` () =
Assert.Equal(domain + "/media/123", getMediaLinkDefault(domain + "/media/123"))
[<Fact>]
let ``does not modify URL w/o quality suffix`` () =
Assert.Equal(domain + "/media/123.jpg", getMediaLinkDefault(domain + "/media/123.jpg"))
[<Fact>]
let ``does not modify URL with quality suffix`` () =
Assert.Equal(domain + "/media/123.jpg:small", getMediaLinkDefault(domain + "/media/123.jpg:small"))
module GetMediaLink_Orig =
let getMediaLinkOrig url = TwitterUrls.GetMediaLink(url, ImageQuality.Best)
let domain = "https://pbs.twimg.com"
[<Fact>]
let ``appends :orig to valid URL w/o quality suffix`` () =
Assert.Equal(domain + "/media/123.jpg:orig", getMediaLinkOrig(domain + "/media/123.jpg"))
[<Fact>]
let ``rewrites :orig into valid URL with quality suffix`` () =
Assert.Equal(domain + "/media/123.jpg:orig", getMediaLinkOrig(domain + "/media/123.jpg:small"))
[<Fact>]
let ``does not modify unknown URL w/o quality suffix`` () =
Assert.Equal(domain + "/profile_images/123.jpg", getMediaLinkOrig(domain + "/profile_images/123.jpg"))
[<Fact>]
let ``rewrites :orig into unknown URL with quality suffix`` () =
Assert.Equal(domain + "/profile_images/123.jpg:orig", getMediaLinkOrig(domain + "/profile_images/123.jpg:small"))
module GetImageFileName =
[<Fact>]
let ``extracts file name from URL w/o quality suffix`` () =
Assert.Equal("test.jpg", TwitterUrls.GetImageFileName("http://example.com/test.jpg"))
[<Fact>]
let ``extracts file name from URL with quality suffix`` () =
Assert.Equal("test.jpg", TwitterUrls.GetImageFileName("http://example.com/test.jpg:orig"))
[<Fact>]
let ``extracts file name from URL with a port`` () =
Assert.Equal("test.jpg", TwitterUrls.GetImageFileName("http://example.com:80/test.jpg"))
[<Collection("RegexAccount")>]
module RegexAccount_IsMatch =
let isMatch = TwitterUrls.RegexAccount.IsMatch
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.True(isMatch("http://twitter.com/chylexmc"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.True(isMatch("https://twitter.com/chylexmc"))
[<Fact>]
let ``accepts trailing slash`` () =
Assert.True(isMatch("https://twitter.com/chylexmc/"))
[<Fact>]
let ``rejects URL with query`` () =
Assert.False(isMatch("https://twitter.com/chylexmc?query"))
[<Fact>]
let ``rejects URL with extra path`` () =
Assert.False(isMatch("https://twitter.com/chylexmc/status/123"))
[<Theory>]
[<InlineData("signup")>]
[<InlineData("tos")>]
[<InlineData("privacy")>]
[<InlineData("search")>]
[<InlineData("search?query")>]
[<InlineData("search-home")>]
[<InlineData("search-advanced")>]
let ``rejects reserved page names`` (name: string) =
Assert.False(isMatch("https://twitter.com/" + name))
[<Theory>]
[<InlineData("tosser")>]
[<InlineData("searching")>]
let ``accepts accounts starting with reserved page names`` (name: string) =
Assert.True(isMatch("https://twitter.com/" + name))
[<Collection("RegexAccount")>]
module RegexAccount_Match =
let extract str = TwitterUrls.RegexAccount.Match(str).Groups.[1].Value
[<Fact>]
let ``extracts account name from simple URL`` () =
Assert.Equal("_abc_DEF_123", extract("https://twitter.com/_abc_DEF_123"))
[<Fact>]
let ``extracts account name from URL with trailing slash`` () =
Assert.Equal("_abc_DEF_123", extract("https://twitter.com/_abc_DEF_123/"))

View File

@@ -1,113 +0,0 @@
namespace TweetTest.Core.TwitterUtils
open Xunit
open TweetDuck.Core.Utils
open TweetLib.Core.Features.Twitter
[<Collection("RegexAccount")>]
module RegexAccount_IsMatch =
let isMatch = TwitterUtils.RegexAccount.IsMatch
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.True(isMatch("http://twitter.com/chylexmc"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.True(isMatch("https://twitter.com/chylexmc"))
[<Fact>]
let ``accepts trailing slash`` () =
Assert.True(isMatch("https://twitter.com/chylexmc/"))
[<Fact>]
let ``rejects URL with query`` () =
Assert.False(isMatch("https://twitter.com/chylexmc?query"))
[<Fact>]
let ``rejects URL with extra path`` () =
Assert.False(isMatch("https://twitter.com/chylexmc/status/123"))
[<Theory>]
[<InlineData("signup")>]
[<InlineData("tos")>]
[<InlineData("privacy")>]
[<InlineData("search")>]
[<InlineData("search?query")>]
[<InlineData("search-home")>]
[<InlineData("search-advanced")>]
let ``rejects reserved page names`` (name: string) =
Assert.False(isMatch("https://twitter.com/"+name))
[<Theory>]
[<InlineData("tosser")>]
[<InlineData("searching")>]
let ``accepts accounts starting with reserved page names`` (name: string) =
Assert.True(isMatch("https://twitter.com/"+name))
[<Collection("RegexAccount")>]
module RegexAccount_Match =
let extract str = TwitterUtils.RegexAccount.Match(str).Groups.[1].Value
[<Fact>]
let ``extracts account name from simple URL`` () =
Assert.Equal("_abc_DEF_123", extract("https://twitter.com/_abc_DEF_123"))
[<Fact>]
let ``extracts account name from URL with trailing slash`` () =
Assert.Equal("_abc_DEF_123", extract("https://twitter.com/_abc_DEF_123/"))
module GetMediaLink_Default =
let getMediaLinkDefault url = TwitterUtils.GetMediaLink(url, ImageQuality.Default)
let domain = "https://pbs.twimg.com"
[<Fact>]
let ``does not modify URL w/o extension`` () =
Assert.Equal(domain+"/media/123", getMediaLinkDefault(domain+"/media/123"))
[<Fact>]
let ``does not modify URL w/o quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg", getMediaLinkDefault(domain+"/media/123.jpg"))
[<Fact>]
let ``does not modify URL with quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg:small", getMediaLinkDefault(domain+"/media/123.jpg:small"))
module GetMediaLink_Orig =
let getMediaLinkOrig url = TwitterUtils.GetMediaLink(url, ImageQuality.Best)
let domain = "https://pbs.twimg.com"
[<Fact>]
let ``appends :orig to valid URL w/o quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg:orig", getMediaLinkOrig(domain+"/media/123.jpg"))
[<Fact>]
let ``rewrites :orig into valid URL with quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg:orig", getMediaLinkOrig(domain+"/media/123.jpg:small"))
[<Fact>]
let ``does not modify unknown URL w/o quality suffix`` () =
Assert.Equal(domain+"/profile_images/123.jpg", getMediaLinkOrig(domain+"/profile_images/123.jpg"))
[<Fact>]
let ``rewrites :orig into unknown URL with quality suffix`` () =
Assert.Equal(domain+"/profile_images/123.jpg:orig", getMediaLinkOrig(domain+"/profile_images/123.jpg:small"))
module GetImageFileName =
[<Fact>]
let ``extracts file name from URL w/o quality suffix`` () =
Assert.Equal("test.jpg", TwitterUtils.GetImageFileName("http://example.com/test.jpg"))
[<Fact>]
let ``extracts file name from URL with quality suffix`` () =
Assert.Equal("test.jpg", TwitterUtils.GetImageFileName("http://example.com/test.jpg:orig"))
[<Fact>]
let ``extracts file name from URL with a port`` () =
Assert.Equal("test.jpg", TwitterUtils.GetImageFileName("http://example.com:80/test.jpg"))

View File

@@ -1,79 +0,0 @@
namespace TweetTest.Core.UrlUtils
open Xunit
open TweetLib.Core.Utils
module Check =
type Result = UrlUtils.CheckResult
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, UrlUtils.Check("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", UrlUtils.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", UrlUtils.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com/path/"))

View File

@@ -51,8 +51,7 @@
<Import Project="$(FSharpTargetsPath)" />
<ItemGroup>
<Compile Include="Core\TestStringUtils.fs" />
<Compile Include="Core\TestTwitterUtils.fs" />
<Compile Include="Core\TestUrlUtils.fs" />
<Compile Include="Core\TestTwitterUrls.fs" />
<Compile Include="Data\TestCommandLineArgs.fs" />
<Compile Include="Data\TestInjectedHTML.fs" />
<Compile Include="Data\TestResult.fs" />

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -10,6 +10,7 @@
<RootNamespace>TweetDuck.Browser</RootNamespace>
<AssemblyName>TweetDuck.Browser</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NuGetPackageImportStamp>
@@ -18,12 +19,10 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Debug\</OutputPath>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<LangVersion>7</LangVersion>
</PropertyGroup>
<PropertyGroup>
<StartupObject />

View File

@@ -98,7 +98,7 @@ namespace TweetDuck.Video{
bool needsUpdate = !timerSync.Enabled || (useCompactLayout ? tablePanelFull.Enabled : tablePanelCompactBottom.Enabled);
if (needsUpdate){
void Disable(TableLayoutPanel panel){
static void Disable(TableLayoutPanel panel){
panel.Controls.Clear();
panel.Visible = false;
panel.Enabled = false;

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -10,6 +10,7 @@
<RootNamespace>TweetDuck.Video</RootNamespace>
<AssemblyName>TweetDuck.Video</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
@@ -25,7 +26,6 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
@@ -37,7 +37,6 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
<LangVersion>7</LangVersion>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>