1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-09-14 10: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,9 +21,8 @@ namespace TweetDuck.Core.Controls{
}
public static float GetDPIScale(this Control control){
using(Graphics graphics = control.CreateGraphics()){
return graphics.DpiY/96F;
}
using Graphics graphics = control.CreateGraphics();
return graphics.DpiY / 96F;
}
public static bool IsFullyOutsideView(this Form form){
@@ -31,17 +30,17 @@ namespace TweetDuck.Core.Controls{
}
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){
if (value == bar.Maximum){
bar.Value = value;
bar.Value = value-1;
bar.Value = value - 1;
bar.Value = value;
}
else{
bar.Value = value+1;
bar.Value = value + 1;
bar.Value = value;
}
}
@@ -60,10 +59,11 @@ namespace TweetDuck.Core.Controls{
public static bool AlignValueToTick(this TrackBar trackBar){
if (trackBar.Value % trackBar.SmallChange != 0){
trackBar.Value = trackBar.SmallChange*(int)Math.Floor(((double)trackBar.Value/trackBar.SmallChange)+0.5);
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

@@ -23,7 +23,7 @@ namespace TweetDuck.Core.Controls{
}
Rectangle rect = e.ClipRectangle;
rect.Width = (int)(rect.Width*((double)Value/Maximum));
rect.Width = (int)(rect.Width * ((double)Value / Maximum));
e.Graphics.FillRectangle(brush, rect);
}

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

@@ -7,17 +7,16 @@ namespace TweetDuck.Core.Controls{
public int LineHeight { get; set; }
protected override void OnPaint(PaintEventArgs e){
int y = (int)Math.Floor((ClientRectangle.Height-Text.Length*LineHeight)/2F)-1;
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;
float x = (ClientRectangle.Width - e.Graphics.MeasureString(str, Font).Width) / 2F;
e.Graphics.DrawString(str, Font, brush, x, y);
y += LineHeight;
}
}
}
}
}

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){
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
this.InvokeAsyncSafe(() => { // TODO not needed but makes code consistent...
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
});
}
if (isLoaded){
@@ -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){
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
this.InvokeAsyncSafe(() => {
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
});
}
}
@@ -286,7 +289,7 @@ namespace TweetDuck.Core{
UpdateInstallerPath = update.InstallerPath;
ForceClose();
}
else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: "+(update.DownloadError?.Message ?? "unknown error")+"\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: " + (update.DownloadError?.Message ?? "unknown error") + "\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(Program.Website);
ForceClose();
}
@@ -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){
@@ -206,7 +206,7 @@ namespace TweetDuck.Core.Handling{
}
protected static void InsertSelectionSearchItem(IMenuModel model, CefMenuCommand insertCommand, string insertLabel){
model.InsertItemAt(model.GetIndexOf(MenuSearchInBrowser)+1, insertCommand, insertLabel);
model.InsertItemAt(model.GetIndexOf(MenuSearchInBrowser) + 1, insertCommand, insertLabel);
}
protected static void AddDebugMenuItems(IMenuModel model){
@@ -217,13 +217,13 @@ namespace TweetDuck.Core.Handling{
}
protected static void RemoveSeparatorIfLast(IMenuModel model){
if (model.Count > 0 && model.GetTypeAt(model.Count-1) == MenuItemType.Separator){
model.RemoveAt(model.Count-1);
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator){
model.RemoveAt(model.Count - 1);
}
}
protected static void AddSeparator(IMenuModel model){
if (model.Count > 0 && model.GetTypeAt(model.Count-1) != MenuItemType.Separator){ // do not add separators if there is nothing to separate
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator){ // do not add separators if there is nothing to separate
model.AddSeparator();
}
}

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{
@@ -24,7 +24,7 @@ namespace TweetDuck.Core.Handling{
private const string TitleMuteNotifications = "Mute notifications";
private const string TitleSettings = "Options";
private const string TitlePlugins = "Plugins";
private const string TitleAboutProgram = "About "+Program.BrandName;
private const string TitleAboutProgram = "About " + Program.BrandName;
private readonly FormBrowser form;
@@ -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

@@ -25,7 +25,7 @@ namespace TweetDuck.Core.Handling.Filters{
int responseLength = responseData.Length;
if (state == State.Reading){
int bytesToRead = Math.Min(responseLength-offset, (int)Math.Min(dataIn?.Length ?? 0, int.MaxValue));
int bytesToRead = Math.Min(responseLength - offset, (int)Math.Min(dataIn?.Length ?? 0, int.MaxValue));
dataIn?.Read(responseData, offset, bytesToRead);
offset += bytesToRead;
@@ -42,7 +42,7 @@ namespace TweetDuck.Core.Handling.Filters{
return FilterStatus.NeedMoreData;
}
else if (state == State.Writing){
int bytesToWrite = Math.Min(responseLength-offset, (int)Math.Min(dataOut.Length, int.MaxValue));
int bytesToWrite = Math.Min(responseLength - offset, (int)Math.Min(dataOut.Length, int.MaxValue));
if (bytesToWrite > 0){
dataOut.Write(responseData, offset, bytesToWrite);

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

@@ -53,13 +53,13 @@ namespace TweetDuck.Core.Handling.General{
input = new TextBox{
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
Font = SystemFonts.MessageBoxFont,
Location = new Point(BrowserUtils.Scale(22+inputPad, dpiScale), form.ActionPanelY-BrowserUtils.Scale(46, dpiScale)),
Size = new Size(form.ClientSize.Width-BrowserUtils.Scale(44+inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale))
Location = new Point(BrowserUtils.Scale(22 + inputPad, dpiScale), form.ActionPanelY - BrowserUtils.Scale(46, dpiScale)),
Size = new Size(form.ClientSize.Width - BrowserUtils.Scale(44 + inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale))
};
form.Controls.Add(input);
form.ActiveControl = input;
form.Height += input.Size.Height+input.Margin.Vertical;
form.Height += input.Size.Height + input.Margin.Vertical;
}
else{
callback.Continue(false);

View File

@@ -22,7 +22,7 @@ namespace TweetDuck.Core.Handling{
extraMessage = "Please download the installer, and tick 'Install dev tools' during the installation process. The installer will automatically find and update your current installation of TweetDuck.";
}
FormMessage.Information("Dev Tools", "You do not have dev tools installed. "+extraMessage, FormMessage.OK);
FormMessage.Information("Dev Tools", "You do not have dev tools installed. " + extraMessage, FormMessage.OK);
});
}

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

@@ -38,7 +38,7 @@ namespace TweetDuck.Core.Management{
AutoClearTimer = new Timer(state => {
if (AutoClearTimer != null){
try{
if (CalculateCacheSize() >= Program.Config.System.ClearCacheThreshold*1024L*1024L){
if (CalculateCacheSize() >= Program.Config.System.ClearCacheThreshold * 1024L * 1024L){
SetClearOnExit();
}
}catch(Exception){

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;
@@ -50,7 +49,7 @@ namespace TweetDuck.Core.Management{
try{
stream.WriteFile(new string[]{ "plugin.data", plugin.Identifier, path.Relative }, path.Full);
}catch(ArgumentOutOfRangeException e){
FormMessage.Warning("Export Profile", "Could not include a plugin file in the export. "+e.Message, FormMessage.OK);
FormMessage.Warning("Export Profile", "Could not include a plugin file in the export. " + e.Message, FormMessage.OK);
}
}
}
@@ -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]);
}
}
@@ -159,7 +157,7 @@ namespace TweetDuck.Core.Management{
}
if (missingPlugins.Count > 0){
FormMessage.Information("Profile Import", "Detected missing plugins when importing plugin data:\n"+string.Join("\n", missingPlugins), FormMessage.OK);
FormMessage.Information("Profile Import", "Detected missing plugins when importing plugin data:\n" + string.Join("\n", missingPlugins), FormMessage.OK);
}
return true;

View File

@@ -40,7 +40,7 @@ namespace TweetDuck.Core.Management{
if ((process = Process.Start(new ProcessStartInfo{
FileName = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe"),
Arguments = $"{owner.Handle} {(int)Math.Floor(100F*owner.GetDPIScale())} {Config.VideoPlayerVolume} \"{url}\" \"{pipe.GenerateToken()}\"",
Arguments = $"{owner.Handle} {(int)Math.Floor(100F * owner.GetDPIScale())} {Config.VideoPlayerVolume} \"{url}\" \"{pipe.GenerateToken()}\"",
UseShellExecute = false,
RedirectStandardOutput = true
})) != null){
@@ -135,7 +135,7 @@ namespace TweetDuck.Core.Management{
private void process_OutputDataReceived(object sender, DataReceivedEventArgs e){
if (!string.IsNullOrEmpty(e.Data)){
Program.Reporter.LogVerbose("[VideoPlayer] "+e.Data);
Program.Reporter.LogVerbose("[VideoPlayer] " + e.Data);
}
}

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;
@@ -23,22 +23,22 @@ namespace TweetDuck.Core.Notification.Example{
}
}
protected override string BodyClasses => base.BodyClasses+" td-example";
protected override string BodyClasses => base.BodyClasses + " td-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,
@@ -28,7 +36,7 @@ namespace TweetDuck.Core.Notification{
Screen screen;
if (Config.NotificationDisplay > 0 && Config.NotificationDisplay <= Screen.AllScreens.Length){
screen = Screen.AllScreens[Config.NotificationDisplay-1];
screen = Screen.AllScreens[Config.NotificationDisplay - 1];
}
else{
screen = Screen.FromControl(owner);
@@ -37,21 +45,21 @@ namespace TweetDuck.Core.Notification{
int edgeDist = Config.NotificationEdgeDistance;
switch(Config.NotificationPosition){
case TweetNotification.Position.TopLeft:
return new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+edgeDist);
case DesktopNotification.Position.TopLeft:
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + edgeDist);
case TweetNotification.Position.TopRight:
return new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
case DesktopNotification.Position.TopRight:
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
case TweetNotification.Position.BottomLeft:
return new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
case DesktopNotification.Position.BottomLeft:
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
case TweetNotification.Position.BottomRight:
return new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
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.CustomNotificationPosition = new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
Config.Save();
}
@@ -92,14 +100,14 @@ namespace TweetDuck.Core.Notification{
protected override bool ShowWithoutActivation => true;
protected float DpiScale { get; }
protected double SizeScale => DpiScale*Config.ZoomLevel/100.0;
protected double SizeScale => DpiScale * Config.ZoomLevel / 100.0;
protected readonly FormBrowser owner;
protected readonly ChromiumWebBrowser browser;
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,21 +47,21 @@ 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))
};
}
protected virtual string BodyClasses => IsCursorOverBrowser ? "td-notification td-hover" : "td-notification";
public Size BrowserSize => Config.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height-timerBarHeight) : ClientSize;
public Size BrowserSize => Config.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height - timerBarHeight) : ClientSize;
protected FormNotificationMain(FormBrowser owner, PluginManager pluginManager, bool enableContextMenu) : base(owner, enableContextMenu){
InitializeComponent();
@@ -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);
@@ -100,7 +102,7 @@ namespace TweetDuck.Core.Notification{
int eventType = wParam.ToInt32();
if (eventType == NativeMethods.WM_MOUSEWHEEL && IsCursorOverBrowser){
browser.SendMouseWheelEvent(0, 0, 0, BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed*0.01), CefEventFlags.None);
browser.SendMouseWheelEvent(0, 0, 0, BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed * 0.01), CefEventFlags.None);
return NativeMethods.HOOK_HANDLED;
}
else if (eventType == NativeMethods.WM_XBUTTONDOWN && DesktopBounds.Contains(Cursor.Position)){
@@ -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,14 +166,23 @@ 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;
}
timeLeft -= timerProgress.Interval;
int value = BrowserUtils.Scale(progressBarTimer.Maximum+25, (totalTime-timeLeft)/(double)totalTime);
progressBarTimer.SetValueInstant(Config.NotificationTimerCountDown ? progressBarTimer.Maximum-value : value);
int value = BrowserUtils.Scale(progressBarTimer.Maximum + 25, (totalTime - timeLeft) / (double)totalTime);
progressBarTimer.SetValueInstant(Config.NotificationTimerCountDown ? progressBarTimer.Maximum - value : value);
if (timeLeft <= 0){
FinishCurrentNotification();
@@ -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;
@@ -237,7 +248,7 @@ namespace TweetDuck.Core.Notification{
protected override void SetNotificationSize(int width, int height){
if (Config.DisplayNotificationTimer){
ClientSize = new Size(width, height+timerBarHeight);
ClientSize = new Size(width, height + timerBarHeight);
progressBarTimer.Visible = true;
}
else{
@@ -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){
@@ -153,7 +154,7 @@ namespace TweetDuck.Core.Notification{
base.UpdateTitle();
if (tweetQueue.Count > 0){
Text = Text+" ("+tweetQueue.Count+" more left)";
Text = Text + " (" + tweetQueue.Count + " more left)";
}
}

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;
@@ -134,9 +134,9 @@ namespace TweetDuck.Core.Notification.Screenshot{
private void debugger_Tick(object sender, EventArgs e){
if (frameCounter < 63 && screenshot.TakeScreenshot(true)){
try{
Clipboard.GetImage()?.Save(Path.Combine(DebugScreenshotPath, "frame_"+(++frameCounter)+".png"), ImageFormat.Png);
Clipboard.GetImage()?.Save(Path.Combine(DebugScreenshotPath, "frame_" + (++frameCounter) + ".png"), ImageFormat.Png);
}catch{
System.Diagnostics.Debug.WriteLine("Failed generating frame "+frameCounter);
System.Diagnostics.Debug.WriteLine("Failed generating frame " + frameCounter);
}
}
else{

View File

@@ -28,12 +28,12 @@ namespace TweetDuck.Core.Notification{
FormBrowser browser = FormManager.TryFind<FormBrowser>();
browser?.InvokeAsyncSafe(() => {
using(FormMessage form = new FormMessage("Sound Notification Error", "Could not find custom notification sound file:\n"+path, MessageBoxIcon.Error)){
using(FormMessage form = new FormMessage("Sound Notification Error", "Could not find custom notification sound file:\n" + path, MessageBoxIcon.Error)){
form.AddButton(FormMessage.Ignore, ControlType.Cancel | ControlType.Focused);
Button btnViewOptions = form.AddButton("View Options");
btnViewOptions.Width += 16;
btnViewOptions.Location = new Point(btnViewOptions.Location.X-16, btnViewOptions.Location.Y);
btnViewOptions.Location = new Point(btnViewOptions.Location.X - 16, btnViewOptions.Location.Y);
if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnViewOptions){
browser.OpenSettings(typeof(TabSettingsSounds));

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{
@@ -90,9 +90,9 @@ namespace TweetDuck.Core.Other.Analytics{
private void RestartTimer(){
TimeSpan diff = DateTime.Now.Subtract(File.LastDataCollection);
int minutesTillNext = (int)(CollectionInterval.TotalMinutes-Math.Floor(diff.TotalMinutes));
int minutesTillNext = (int)(CollectionInterval.TotalMinutes - Math.Floor(diff.TotalMinutes));
currentTimer.Interval = Math.Max(minutesTillNext, 2)*60000;
currentTimer.Interval = Math.Max(minutesTillNext, 2) * 60000;
currentTimer.Start();
}
@@ -139,7 +139,7 @@ namespace TweetDuck.Core.Other.Analytics{
case WebExceptionStatus.ProtocolError:
HttpWebResponse response = e.Response as HttpWebResponse;
message = "HTTP Error "+(response != null ? $"{(int)response.StatusCode} ({response.StatusDescription})" : "(unknown code)");
message = "HTTP Error " + (response != null ? $"{(int)response.StatusCode} ({response.StatusDescription})" : "(unknown code)");
break;
}
@@ -152,7 +152,7 @@ namespace TweetDuck.Core.Other.Analytics{
#endif
}
ScheduleReportIn(TimeSpan.FromHours(4), message ?? "Error: "+(task.Exception.InnerException?.Message ?? task.Exception.Message));
ScheduleReportIn(TimeSpan.FromHours(4), message ?? "Error: " + (task.Exception.InnerException?.Message ?? task.Exception.Message));
}
}));
}

View File

@@ -47,7 +47,7 @@ namespace TweetDuck.Core.Other.Analytics{
build.AppendLine();
}
else{
build.AppendLine(entry.Key+": "+entry.Value);
build.AppendLine(entry.Key + ": " + entry.Value);
}
}

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;
@@ -127,9 +126,9 @@ namespace TweetDuck.Core.Other.Analytics{
private static string Bool(bool value) => value ? "on" : "off";
private static string Exact(int value) => value.ToString();
private static string RoundUp(int value, int multiple) => (multiple*(int)Math.Ceiling((double)value/multiple)).ToString();
private static string RoundUp(int value, int multiple) => (multiple * (int)Math.Ceiling((double)value / multiple)).ToString();
private static string LogRound(int value, int logBase) => (value <= 0 ? 0 : (int)Math.Pow(logBase, Math.Floor(Math.Log(value, logBase)))).ToString();
private static string Plugin(Plugin plugin) => plugin.Group.GetIdentifierPrefixShort()+plugin.Identifier.Substring(plugin.Group.GetIdentifierPrefix().Length);
private static string Plugin(Plugin plugin) => plugin.Group.GetIdentifierPrefixShort() + plugin.Identifier.Substring(plugin.Group.GetIdentifierPrefix().Length);
private static string Dict(Dictionary<string, string> dict, string key, string def = "(unknown)") => dict.TryGetValue(key, out string value) ? value : def;
private static string List(IEnumerable<string> list) => string.Join("|", list.DefaultIfEmpty("(none)"));
@@ -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,10 +167,10 @@ 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));
}
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)
};
}
@@ -286,7 +285,7 @@ namespace TweetDuck.Core.Other.Analytics{
}
string accType = matchType.Groups[1].Value == "#" ? matchType.Groups[2].Value : "account";
return matchAdvanced.Success && !matchAdvanced.Value.Contains("false") ? "advanced/"+accType : accType;
return matchAdvanced.Success && !matchAdvanced.Value.Contains("false") ? "advanced/" + accType : accType;
}catch{
return "(unknown)";
}
@@ -307,7 +306,7 @@ namespace TweetDuck.Core.Other.Analytics{
}
return new ExternalInfo{
Resolution = screen.Bounds.Width+"x"+screen.Bounds.Height,
Resolution = screen.Bounds.Width + "x" + screen.Bounds.Height,
DPI = dpi
};
}

View File

@@ -12,7 +12,7 @@ namespace TweetDuck.Core.Other{
public FormAbout(){
InitializeComponent();
Text = "About "+Program.BrandName+" "+Program.VersionTag;
Text = "About " + Program.BrandName + " " + Program.VersionTag;
labelDescription.Text = "TweetDuck was created by chylex as a replacement to the discontinued official TweetDeck client for Windows.\n\nThe program is available for free under the open source MIT license.";

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{
@@ -37,7 +37,7 @@ namespace TweetDuck.Core.Other{
}
public static void Show(string hash = null){
string url = GuideUrl+(hash ?? string.Empty);
string url = GuideUrl + (hash ?? string.Empty);
FormGuide guide = FormManager.TryFind<FormGuide>();
if (guide == null){
@@ -60,8 +60,8 @@ namespace TweetDuck.Core.Other{
private FormGuide(string url, FormBrowser owner){
InitializeComponent();
Text = Program.BrandName+" Guide";
Size = new Size(owner.Size.Width*3/4, owner.Size.Height*3/4);
Text = Program.BrandName + " Guide";
Size = new Size(owner.Size.Width * 3 / 4, owner.Size.Height * 3 / 4);
VisibleChanged += (sender, args) => this.MoveToCenter(owner);
ResourceHandlerFactory resourceHandlerFactory = new ResourceHandlerFactory();
@@ -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

@@ -133,7 +133,7 @@ namespace TweetDuck.Core.Other{
Font = SystemFonts.MessageBoxFont,
Location = new Point(0, 12),
Size = new Size(BrowserUtils.Scale(88, dpiScale), BrowserUtils.Scale(26, dpiScale)),
TabIndex = 256-buttonCount,
TabIndex = 256 - buttonCount,
Text = title,
UseVisualStyleBackColor = true
};
@@ -171,17 +171,17 @@ namespace TweetDuck.Core.Other{
control.Size = new Size(BrowserUtils.Scale(control.Width, dpiScale), BrowserUtils.Scale(control.Height, dpiScale));
minFormWidth += control.Width+control.Margin.Horizontal;
minFormWidth += control.Width + control.Margin.Horizontal;
ClientWidth = Math.Max(realFormWidth, minFormWidth);
}
private void RecalculateButtonLocation(){
int dist = ButtonDistance;
int start = ClientWidth-dist;
int start = ClientWidth - dist;
for(int index = 0; index < buttonCount; index++){
Control control = panelActions.Controls[index];
control.Location = new Point(start-index*dist, control.Location.Y);
control.Location = new Point(start - index * dist, control.Location.Y);
}
}
@@ -194,17 +194,17 @@ namespace TweetDuck.Core.Other{
int labelOffset = BrowserUtils.Scale(8, dpiScale);
if (isMultiline && !wasLabelMultiline){
labelMessage.Location = new Point(labelMessage.Location.X, labelMessage.Location.Y-labelOffset);
labelMessage.Location = new Point(labelMessage.Location.X, labelMessage.Location.Y - labelOffset);
prevLabelHeight += labelOffset;
}
else if (!isMultiline && wasLabelMultiline){
labelMessage.Location = new Point(labelMessage.Location.X, labelMessage.Location.Y+labelOffset);
labelMessage.Location = new Point(labelMessage.Location.X, labelMessage.Location.Y + labelOffset);
prevLabelHeight -= labelOffset;
}
realFormWidth = ClientWidth-(icon == null ? BrowserUtils.Scale(50, dpiScale) : 0)+labelMessage.Width-prevLabelWidth;
realFormWidth = ClientWidth - (icon == null ? BrowserUtils.Scale(50, dpiScale) : 0) + labelMessage.Width - prevLabelWidth;
ClientWidth = Math.Max(realFormWidth, minFormWidth);
Height += labelMessage.Height-prevLabelHeight;
Height += labelMessage.Height - prevLabelHeight;
prevLabelWidth = labelMessage.Width;
prevLabelHeight = labelMessage.Height;
@@ -213,7 +213,7 @@ namespace TweetDuck.Core.Other{
protected override void OnPaint(PaintEventArgs e){
if (icon != null){
e.Graphics.DrawIcon(icon, BrowserUtils.Scale(25, dpiScale), 1+BrowserUtils.Scale(25, dpiScale));
e.Graphics.DrawIcon(icon, BrowserUtils.Scale(25, dpiScale), 1 + BrowserUtils.Scale(25, dpiScale));
}
base.OnPaint(e);

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{
@@ -16,7 +16,7 @@ namespace TweetDuck.Core.Other{
public FormPlugins(){
InitializeComponent();
Text = Program.BrandName+" Plugins";
Text = Program.BrandName + " Plugins";
}
public FormPlugins(PluginManager pluginManager) : this(){
@@ -69,8 +69,8 @@ namespace TweetDuck.Core.Other{
timerLayout.Stop();
// stupid WinForms scrollbars and panels
Padding = new Padding(Padding.Left, Padding.Top, Padding.Right+1, Padding.Bottom+1);
Padding = new Padding(Padding.Left, Padding.Top, Padding.Right-1, Padding.Bottom-1);
Padding = new Padding(Padding.Left, Padding.Top, Padding.Right + 1, Padding.Bottom + 1);
Padding = new Padding(Padding.Left, Padding.Top, Padding.Right - 1, Padding.Bottom - 1);
}
public void flowLayoutPlugins_Resize(object sender, EventArgs e){
@@ -80,22 +80,22 @@ namespace TweetDuck.Core.Other{
return;
}
bool showScrollBar = lastPlugin.Location.Y+lastPlugin.Height+1 >= flowLayoutPlugins.Height;
bool showScrollBar = lastPlugin.Location.Y + lastPlugin.Height + 1 >= flowLayoutPlugins.Height;
int horizontalOffset = showScrollBar ? SystemInformation.VerticalScrollBarWidth : 0;
flowLayoutPlugins.AutoScroll = showScrollBar;
flowLayoutPlugins.VerticalScroll.Visible = showScrollBar;
foreach(Control control in flowLayoutPlugins.Controls){
control.Width = flowLayoutPlugins.Width-control.Margin.Horizontal-horizontalOffset;
control.Width = flowLayoutPlugins.Width - control.Margin.Horizontal - horizontalOffset;
}
flowLayoutPlugins.Controls[flowLayoutPlugins.Controls.Count-1].Visible = !showScrollBar;
flowLayoutPlugins.Controls[flowLayoutPlugins.Controls.Count - 1].Visible = !showScrollBar;
flowLayoutPlugins.Focus();
}
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{
@@ -27,7 +27,7 @@ namespace TweetDuck.Core.Other{
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, AnalyticsManager analytics, Type startTab){
InitializeComponent();
Text = Program.BrandName+" Options";
Text = Program.BrandName + " Options";
this.browser = browser;
this.browser.PauseNotification();
@@ -110,7 +110,7 @@ namespace TweetDuck.Core.Other{
BackColor = SystemColors.Control,
FlatStyle = FlatStyle.Flat,
Font = SystemFonts.MessageBoxFont,
Location = new Point(0, (buttonHeight+1)*(panelButtons.Controls.Count/2)),
Location = new Point(0, (buttonHeight + 1) * (panelButtons.Controls.Count / 2)),
Margin = new Padding(0),
Size = new Size(panelButtons.Width, buttonHeight),
Text = title,
@@ -125,7 +125,7 @@ namespace TweetDuck.Core.Other{
panelButtons.Controls.Add(new Panel{
BackColor = Color.DimGray,
Location = new Point(0, panelButtons.Controls[panelButtons.Controls.Count-1].Location.Y+buttonHeight),
Location = new Point(0, panelButtons.Controls[panelButtons.Controls.Count - 1].Location.Y + buttonHeight),
Margin = new Padding(0),
Size = new Size(panelButtons.Width, 1)
});
@@ -157,8 +157,8 @@ namespace TweetDuck.Core.Other{
}
}
if (tab.Control.Height < panelContents.Height-2){
tab.Control.Height = panelContents.Height-2; // fixes off-by-pixel error on high DPI
if (tab.Control.Height < panelContents.Height - 2){
tab.Control.Height = panelContents.Height - 2; // fixes off-by-pixel error on high DPI
}
tab.Control.OnReady();

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

@@ -8,7 +8,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
public DialogSettingsAnalytics(AnalyticsReport report){
InitializeComponent();
Text = Program.BrandName+" Options - Analytics Report";
Text = Program.BrandName + " Options - Analytics Report";
textBoxReport.EnableMultilineShortcuts();
textBoxReport.Text = report.ToString().TrimEnd();

View File

@@ -16,7 +16,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
public DialogSettingsCSS(string browserCSS, string notificationCSS, Action<string> reinjectBrowserCSS, Action openDevTools){
InitializeComponent();
Text = Program.BrandName+" Options - CSS";
Text = Program.BrandName + " Options - CSS";
this.reinjectBrowserCSS = reinjectBrowserCSS;
this.openDevTools = openDevTools;
@@ -64,21 +64,21 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
}
}
if (!(deleteTo < text.Length-1 && text[deleteTo] == '\r' && text[deleteTo+1] == '\n')){
if (!(deleteTo < text.Length - 1 && text[deleteTo] == '\r' && text[deleteTo + 1] == '\n')){
++deleteTo;
}
tb.Select(deleteTo, tb.SelectionLength+tb.SelectionStart-deleteTo);
tb.Select(deleteTo, tb.SelectionLength + tb.SelectionStart - deleteTo);
tb.SelectedText = string.Empty;
}
}
else if (e.KeyCode == Keys.Back && e.Modifiers == Keys.None){
int deleteTo = tb.SelectionStart;
if (deleteTo > 1 && text[deleteTo-1] == ' ' && text[deleteTo-2] == ' '){
if (deleteTo > 1 && text[deleteTo - 1] == ' ' && text[deleteTo - 2] == ' '){
e.SuppressKeyPress = true;
tb.Select(deleteTo-2, 2);
tb.Select(deleteTo - 2, 2);
tb.SelectedText = string.Empty;
}
}
@@ -89,28 +89,28 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
if (insertAt == 0){
return;
}
else if (text[insertAt-1] == '{'){
insertText = Environment.NewLine+" ";
else if (text[insertAt - 1] == '{'){
insertText = Environment.NewLine + " ";
int nextBracket = insertAt < text.Length ? text.IndexOfAny(new char[]{ '{', '}' }, insertAt+1) : -1;
int nextBracket = insertAt < text.Length ? text.IndexOfAny(new char[]{ '{', '}' }, insertAt + 1) : -1;
if (nextBracket == -1 || text[nextBracket] == '{'){
string insertExtra = Environment.NewLine+"}";
string insertExtra = Environment.NewLine + "}";
insertText += insertExtra;
cursorOffset -= insertExtra.Length;
}
}
else{
int lineStart = text.LastIndexOf('\n', tb.SelectionStart-1);
int lineStart = text.LastIndexOf('\n', tb.SelectionStart - 1);
Match match = Regex.Match(text.Substring(lineStart == -1 ? 0 : lineStart+1), "^([ \t]+)");
insertText = match.Success ? Environment.NewLine+match.Groups[1].Value : null;
Match match = Regex.Match(text.Substring(lineStart == -1 ? 0 : lineStart + 1), "^([ \t]+)");
insertText = match.Success ? Environment.NewLine + match.Groups[1].Value : null;
}
if (!string.IsNullOrEmpty(insertText)){
e.SuppressKeyPress = true;
tb.Text = text.Insert(insertAt, insertText);
tb.SelectionStart = insertAt+cursorOffset+insertText.Length;
tb.SelectionStart = insertAt + cursorOffset + insertText.Length;
}
}
}

View File

@@ -13,7 +13,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
public DialogSettingsCefArgs(string args){
InitializeComponent();
Text = Program.BrandName+" Options - CEF Arguments";
Text = Program.BrandName + " Options - CEF Arguments";
textBoxArgs.EnableMultilineShortcuts();
textBoxArgs.Text = initialArgs = args ?? "";
@@ -32,7 +32,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
}
int count = CommandLineArgs.ReadCefArguments(CefArgs).Count;
string prompt = count == 0 && !string.IsNullOrWhiteSpace(initialArgs) ? "All current arguments will be removed. Continue?" : count+(count == 1 ? " argument was" : " arguments were")+" detected. Continue?";
string prompt = count == 0 && !string.IsNullOrWhiteSpace(initialArgs) ? "All current arguments will be removed. Continue?" : count + (count == 1 ? " argument was" : " arguments were") + " detected. Continue?";
if (FormMessage.Question("Confirm CEF Arguments", prompt, FormMessage.OK, FormMessage.Cancel)){
DialogResult = DialogResult.OK;

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{
@@ -124,7 +124,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
// Continue...
panelDecision.Visible = false;
panelSelection.Visible = true;
Height += panelSelection.Height-panelDecision.Height;
Height += panelSelection.Height - panelDecision.Height;
break;
case State.Reset:

View File

@@ -24,7 +24,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
control_Change(this, EventArgs.Empty);
Text = Program.BrandName+" Arguments";
Text = Program.BrandName + " Arguments";
}
private void control_Change(object sender, EventArgs e){

View File

@@ -8,7 +8,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
public DialogSettingsSearchEngine(){
InitializeComponent();
Text = Program.BrandName+" Options - Custom Search Engine";
Text = Program.BrandName + " Options - Custom Search Engine";
textBoxUrl.Text = Program.Config.User.SearchEngineUrl ?? "";
textBoxUrl.Select(textBoxUrl.Text.Length, 0);

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{
@@ -36,7 +36,7 @@ namespace TweetDuck.Core.Other.Settings{
numClearCacheThreshold.SetValueSafe(SysConfig.ClearCacheThreshold);
BrowserCache.GetCacheSize(task => {
string text = task.Status == TaskStatus.RanToCompletion ? (int)Math.Ceiling(task.Result/(1024.0*1024.0))+" MB" : "unknown";
string text = task.Status == TaskStatus.RanToCompletion ? (int)Math.Ceiling(task.Result / (1024.0 * 1024.0)) + " MB" : "unknown";
this.InvokeSafe(() => btnClearCache.Text = $"Clear Cache ({text})");
});
@@ -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{
@@ -24,7 +24,7 @@ namespace TweetDuck.Core.Other.Settings{
if (analytics != null){
string collectionTime = analyticsFile.LastCollectionMessage;
labelDataCollectionMessage.Text = string.IsNullOrEmpty(collectionTime) ? "No collection yet" : "Last collection: "+collectionTime;
labelDataCollectionMessage.Text = string.IsNullOrEmpty(collectionTime) ? "No collection yet" : "Last collection: " + collectionTime;
}
}

View File

@@ -51,7 +51,7 @@ namespace TweetDuck.Core.Other.Settings{
checkAnimatedAvatars.Checked = Config.EnableAnimatedImages;
trackBarZoom.SetValueSafe(Config.ZoomLevel);
labelZoomValue.Text = trackBarZoom.Value+"%";
labelZoomValue.Text = trackBarZoom.Value + "%";
// system tray
@@ -63,7 +63,7 @@ namespace TweetDuck.Core.Other.Settings{
comboBoxTrayType.Items.Add("Minimize to Tray");
comboBoxTrayType.Items.Add("Close to Tray");
comboBoxTrayType.Items.Add("Combined");
comboBoxTrayType.SelectedIndex = Math.Min(Math.Max((int)Config.TrayBehavior, 0), comboBoxTrayType.Items.Count-1);
comboBoxTrayType.SelectedIndex = Math.Min(Math.Max((int)Config.TrayBehavior, 0), comboBoxTrayType.Items.Count - 1);
checkTrayHighlight.Enabled = Config.TrayBehavior.ShouldDisplayIcon();
checkTrayHighlight.Checked = Config.EnableTrayHighlight;
@@ -189,7 +189,7 @@ namespace TweetDuck.Core.Other.Settings{
if (trackBarZoom.AlignValueToTick()){
zoomUpdateTimer.Stop();
zoomUpdateTimer.Start();
labelZoomValue.Text = trackBarZoom.Value+"%";
labelZoomValue.Text = trackBarZoom.Value + "%";
}
}

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{
@@ -59,31 +59,31 @@ namespace TweetDuck.Core.Other.Settings{
checkTimerCountDown.Checked = Config.NotificationTimerCountDown;
trackBarDuration.SetValueSafe(Config.NotificationDurationValue);
labelDurationValue.Text = Config.NotificationDurationValue+" ms/c";
labelDurationValue.Text = Config.NotificationDurationValue + " ms/c";
// location
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);
comboBoxDisplay.SelectedIndex = Math.Min(comboBoxDisplay.Items.Count - 1, Config.NotificationDisplay);
trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance);
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value+" px";
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value + " px";
// size
@@ -91,12 +91,12 @@ 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);
labelScrollSpeedValue.Text = trackBarScrollSpeed.Value+"%";
labelScrollSpeedValue.Text = trackBarScrollSpeed.Value + "%";
}
public override void OnReady(){
@@ -195,7 +195,7 @@ namespace TweetDuck.Core.Other.Settings{
durationUpdateTimer.Start();
Config.NotificationDurationValue = trackBarDuration.Value;
labelDurationValue.Text = Config.NotificationDurationValue+" ms/c";
labelDurationValue.Text = Config.NotificationDurationValue + " ms/c";
}
private void btnDurationShort_Click(object sender, EventArgs e){
@@ -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();
}
}
@@ -255,7 +255,7 @@ namespace TweetDuck.Core.Other.Settings{
}
private void trackBarEdgeDistance_ValueChanged(object sender, EventArgs e){
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value+" px";
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value + " px";
Config.NotificationEdgeDistance = trackBarEdgeDistance.Value;
notification.ShowExampleNotification(false);
}
@@ -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,13 +276,13 @@ namespace TweetDuck.Core.Other.Settings{
Config.CustomNotificationSize = notification.BrowserSize;
}
Config.NotificationSize = TweetNotification.Size.Custom;
Config.NotificationSize = DesktopNotification.Size.Custom;
notification.ShowExampleNotification(false);
}
private void trackBarScrollSpeed_ValueChanged(object sender, EventArgs e){
if (trackBarScrollSpeed.AlignValueToTick()){
labelScrollSpeedValue.Text = trackBarScrollSpeed.Value+"%";
labelScrollSpeedValue.Text = trackBarScrollSpeed.Value + "%";
Config.NotificationScrollSpeed = trackBarScrollSpeed.Value;
}
}

View File

@@ -20,7 +20,7 @@ namespace TweetDuck.Core.Other.Settings{
toolTip.SetToolTip(tbCustomSound, "When empty, the default TweetDeck sound notification is used.");
trackBarVolume.SetValueSafe(Config.NotificationSoundVolume);
labelVolumeValue.Text = trackBarVolume.Value+"%";
labelVolumeValue.Text = trackBarVolume.Value + "%";
tbCustomSound.Text = Config.NotificationSoundPath;
tbCustomSound_TextChanged(tbCustomSound, EventArgs.Empty);
@@ -83,7 +83,7 @@ namespace TweetDuck.Core.Other.Settings{
private void trackBarVolume_ValueChanged(object sender, EventArgs e){
volumeUpdateTimer.Stop();
volumeUpdateTimer.Start();
labelVolumeValue.Text = trackBarVolume.Value+"%";
labelVolumeValue.Text = trackBarVolume.Value + "%";
}
private void volumeUpdateTimer_Tick(object sender, EventArgs e){

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{
@@ -29,7 +29,7 @@ namespace TweetDuck.Core.Utils{
args["disable-threaded-scrolling"] = "1";
if (args.TryGetValue("disable-features", out string disabledFeatures)){
args["disable-features"] = "TouchpadAndWheelScrollLatching,"+disabledFeatures;
args["disable-features"] = "TouchpadAndWheelScrollLatching," + disabledFeatures;
}
else{
args["disable-features"] = "TouchpadAndWheelScrollLatching";
@@ -48,7 +48,7 @@ namespace TweetDuck.Core.Utils{
args["enable-system-flash"] = "0";
if (args.TryGetValue("js-flags", out string jsFlags)){
args["js-flags"] = "--expose-gc "+jsFlags;
args["js-flags"] = "--expose-gc " + jsFlags;
}
else{
args["js-flags"] = "--expose-gc";
@@ -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,12 +103,12 @@ 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)){
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)){
form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel | ControlType.Focused);
form.AddButton(FormMessage.Yes, DialogResult.Yes, ControlType.Accept);
form.AddButton("Always Visit", DialogResult.Ignore);
@@ -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:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
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;
@@ -152,20 +158,12 @@ namespace TweetDuck.Core.Utils{
}
}
else{
OpenExternalBrowser(searchUrl+Uri.EscapeUriString(query));
OpenExternalBrowser(searchUrl + Uri.EscapeUriString(query));
}
}
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));
return (int)Math.Round(baseValue * scaleFactor);
}
}
}

View File

@@ -133,7 +133,7 @@ namespace TweetDuck.Core.Utils{
ticks = (uint)Environment.TickCount;
}
int seconds = (int)Math.Floor(TimeSpan.FromMilliseconds(ticks-info.dwTime).TotalSeconds);
int seconds = (int)Math.Floor(TimeSpan.FromMilliseconds(ticks - info.dwTime).TotalSeconds);
return Math.Max(0, seconds); // ignore rollover after several weeks of uptime
}

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,62 +15,37 @@ 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)){
WindowsUtils.OpenAssociatedProgram(path);
}
else{
FormMessage.Error("Image Download", "Invalid file extension "+ext, FormMessage.OK);
FormMessage.Error("Image Download", "Invalid file extension " + ext, FormMessage.OK);
}
}
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);
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
});
}
}
@@ -85,22 +59,22 @@ 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{
AutoUpgradeEnabled = true,
OverwritePrompt = urls.Length == 1,
Title = "Save Image",
FileName = qualityIndex == -1 ? filename : $"{username} {Path.ChangeExtension(filename, null)} {firstImageLink.Substring(qualityIndex+1)}".Trim()+ext,
Filter = (urls.Length == 1 ? "Image" : "Images")+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
FileName = qualityIndex == -1 ? filename : $"{username} {Path.ChangeExtension(filename, null)} {firstImageLink.Substring(qualityIndex + 1)}".Trim() + ext,
Filter = (urls.Length == 1 ? "Image" : "Images") + (string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){
if (dialog.ShowDialog() == DialogResult.OK){
void OnFailure(Exception ex){
FormMessage.Error("Image Download", "An error occurred while downloading the image: "+ex.Message, FormMessage.OK);
static void OnFailure(Exception ex){
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
}
if (urls.Length == 1){
@@ -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{
@@ -127,11 +101,11 @@ namespace TweetDuck.Core.Utils{
OverwritePrompt = true,
Title = "Save Video",
FileName = string.IsNullOrEmpty(username) ? filename : $"{username} {filename}".TrimStart(),
Filter = "Video"+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
Filter = "Video" + (string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){
if (dialog.ShowDialog() == DialogResult.OK){
DownloadFileAuth(url, dialog.FileName, null, ex => {
FormMessage.Error("Video Download", "An error occurred while downloading the video: "+ex.Message, FormMessage.OK);
FormMessage.Error("Video Download", "An error occurred while downloading the video: " + ex.Message, FormMessage.OK);
});
}
}

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){
@@ -44,7 +37,7 @@ namespace TweetDuck.Core.Utils{
}catch(Win32Exception e) when (e.NativeErrorCode == 0x000004C7){ // operation canceled by the user
return false;
}catch(Exception e){
Program.Reporter.HandleException("Error Opening Program", "Could not open the associated program for "+file, true, e);
Program.Reporter.HandleException("Error Opening Program", "Could not open the associated program for " + file, true, e);
return false;
}
}
@@ -86,8 +79,8 @@ namespace TweetDuck.Core.Utils{
string updatedHtml = RegexStripHtmlStyles.Value.Replace(originalHtml, string.Empty);
int removed = originalHtml.Length-updatedHtml.Length;
updatedHtml = RegexOffsetClipboardHtml.Value.Replace(updatedHtml, match => (int.Parse(match.Value)-removed).ToString().PadLeft(match.Value.Length, '0'));
int removed = originalHtml.Length - updatedHtml.Length;
updatedHtml = RegexOffsetClipboardHtml.Value.Replace(updatedHtml, match => (int.Parse(match.Value) - removed).ToString().PadLeft(match.Value.Length, '0'));
DataObject obj = new DataObject();
obj.SetText(originalText, TextDataFormat.UnicodeText);
@@ -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;
}
@@ -135,15 +130,13 @@ namespace TweetDuck.Core.Utils{
continue;
}
if (browserPath[0] == '"' && browserPath[browserPath.Length-1] == '"'){
browserPath = browserPath.Substring(1, browserPath.Length-2);
if (browserPath[0] == '"' && browserPath[browserPath.Length - 1] == '"'){
browserPath = browserPath.Substring(1, browserPath.Length - 2);
}
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

@@ -27,7 +27,7 @@ namespace TweetDuck.Plugins{
float dpiScale = this.GetDPIScale();
if (dpiScale > 1F){
Size = MaximumSize = new Size(MaximumSize.Width, MaximumSize.Height+3);
Size = MaximumSize = new Size(MaximumSize.Width, MaximumSize.Height + 3);
}
this.labelName.Text = plugin.Name;

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
});
}
@@ -71,7 +84,7 @@ namespace TweetDuck{
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
if (!FileUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: " + StoragePath, FormMessage.OK);
return;
}
@@ -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){
@@ -128,7 +143,7 @@ namespace TweetDuck{
try{
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
}catch(Exception e){
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: "+e.Message, FormMessage.OK);
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK);
return;
}
@@ -139,7 +154,7 @@ namespace TweetDuck{
CefSettings settings = new CefSettings{
UserAgent = BrowserUtils.UserAgentChrome,
BrowserSubprocessPath = BrandName+".Browser.exe",
BrowserSubprocessPath = BrandName + ".Browser.exe",
CachePath = StoragePath,
UserDataPath = CefDataPath,
LogFile = ConsoleLogFilePath,
@@ -156,13 +171,14 @@ namespace TweetDuck{
Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser();
Resources.Initialize(mainForm);
Application.Run(mainForm);
if (mainForm.UpdateInstallerPath != null){
ExitCleanup();
// ProgramPath has a trailing backslash
string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\"" + ProgramPath + "\" /RUNARGS=\"" + Arguments.GetCurrentForInstallerCmd() + "\"" + (IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !FileUtils.CheckFolderWritePermission(ProgramPath);
if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
@@ -179,10 +195,10 @@ namespace TweetDuck{
if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))){
if (Path.GetInvalidPathChars().Any(custom.Contains)){
Reporter.HandleEarlyFailure("Data Folder Invalid", "The data folder contains invalid characters:\n"+custom);
Reporter.HandleEarlyFailure("Data Folder Invalid", "The data folder contains invalid characters:\n" + custom);
}
else if (!Path.IsPathRooted(custom)){
Reporter.HandleEarlyFailure("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n"+custom);
Reporter.HandleEarlyFailure("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n" + custom);
}
return Environment.ExpandEnvironmentVariables(custom);

View File

@@ -19,7 +19,7 @@ namespace TweetDuck{
public void SetupUnhandledExceptionHandler(string caption){
AppDomain.CurrentDomain.UnhandledException += (sender, args) => {
if (args.ExceptionObject is Exception ex) {
if (args.ExceptionObject is Exception ex){
HandleException(caption, "An unhandled exception has occurred.", false, ex);
}
};
@@ -58,8 +58,8 @@ namespace TweetDuck{
public void HandleException(string caption, string message, bool canIgnore, Exception e){
bool loggedSuccessfully = LogImportant(e.ToString());
string exceptionText = e is ExpandedLogException ? e.Message+"\n\nDetails with potentially sensitive information are in the Error Log." : e.Message;
FormMessage form = new FormMessage(caption, message+"\nError: "+exceptionText, canIgnore ? MessageBoxIcon.Warning : MessageBoxIcon.Error);
string exceptionText = e is ExpandedLogException ? e.Message + "\n\nDetails with potentially sensitive information are in the Error Log." : e.Message;
FormMessage form = new FormMessage(caption, message + "\nError: " + exceptionText, canIgnore ? MessageBoxIcon.Warning : MessageBoxIcon.Error);
Button btnExit = form.AddButton(FormMessage.Exit);
Button btnIgnore = form.AddButton(FormMessage.Ignore, DialogResult.Ignore, ControlType.Cancel);
@@ -115,7 +115,7 @@ namespace TweetDuck{
this.details = details;
}
public override string ToString() => base.ToString()+"\r\n"+details;
public override string ToString() => base.ToString() + "\r\n" + details;
}
}
}

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,120 +57,29 @@ 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{
separator = contents.IndexOf('\n');
string fileVersion = contents.Substring(1, separator-1).TrimEnd();
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

@@ -11,8 +11,8 @@ namespace TweetDuck.Updates{
this.updateInfo = info;
Text = "Updating "+Program.BrandName;
labelDescription.Text = "Downloading version "+info.VersionTag+"...";
Text = "Updating " + Program.BrandName;
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

@@ -13,7 +13,7 @@ namespace TweetLib.Communication{
public static Client CreateClient(string token){
int space = token.IndexOf(' ');
return new Client(token.Substring(0, space), token.Substring(space+1));
return new Client(token.Substring(0, space), token.Substring(space + 1));
}
protected readonly PipeStream pipeIn;
@@ -74,7 +74,7 @@ namespace TweetLib.Communication{
internal Server() : base(new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable), new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)){}
public string GenerateToken(){
return ServerPipeIn.GetClientHandleAsString()+" "+ServerPipeOut.GetClientHandleAsString();
return ServerPipeIn.GetClientHandleAsString() + " " + ServerPipeOut.GetClientHandleAsString();
}
public void DisposeToken(){
@@ -100,7 +100,7 @@ namespace TweetLib.Communication{
}
else{
Key = line.Substring(0, separatorIndex);
Data = line.Substring(separatorIndex+1);
Data = line.Substring(separatorIndex + 1);
}
}
}

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

@@ -16,7 +16,7 @@ namespace TweetLib.Core.Collections{
if (entry.Length > 0 && entry[0] == entryChar){
if (index < array.Length - 1){
string potentialValue = array[index+1];
string potentialValue = array[index + 1];
if (potentialValue.Length > 0 && potentialValue[0] == entryChar){
targetArgs.AddFlag(entry);

View File

@@ -21,7 +21,7 @@ namespace TweetLib.Core.Data{
byte[] name = Encoding.UTF8.GetBytes(identifier);
if (name.Length > 255){
throw new ArgumentOutOfRangeException("Identifier cannot be 256 or more characters long: "+identifier);
throw new ArgumentOutOfRangeException("Identifier cannot be 256 or more characters long: " + identifier);
}
byte[] contents;
@@ -103,7 +103,7 @@ namespace TweetLib.Core.Data{
public string[] KeyValue{
get{
int index = Identifier.IndexOf(KeySeparator);
return index == -1 ? StringUtils.EmptyArray : Identifier.Substring(index+1).Split(KeySeparator);
return index == -1 ? StringUtils.EmptyArray : Identifier.Substring(index + 1).Split(KeySeparator);
}
}

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;
@@ -42,21 +36,22 @@ namespace TweetDuck.Core.Notification{
}
public int GetDisplayDuration(int value){
return 2000+Math.Max(1000, value*characters);
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

@@ -24,25 +24,25 @@ namespace TweetLib.Core.Serialization{
while(true){
int nextIndex = data.IndexOf('\\', index);
if (nextIndex == -1 || nextIndex+1 >= data.Length){
if (nextIndex == -1 || nextIndex + 1 >= data.Length){
break;
}
else{
build.Append(data.Substring(index, nextIndex-index));
build.Append(data.Substring(index, nextIndex - index));
char next = data[nextIndex+1];
char next = data[nextIndex + 1];
if (next == '\\'){ // convert double backslash to single backslash
build.Append('\\');
index = nextIndex+2;
index = nextIndex + 2;
}
else if (next == '\r' && nextIndex+2 < data.Length && data[nextIndex+2] == '\n'){ // convert backslash followed by CRLF to custom new line
else if (next == '\r' && nextIndex + 2 < data.Length && data[nextIndex + 2] == '\n'){ // convert backslash followed by CRLF to custom new line
build.Append(NewLineCustom);
index = nextIndex+3;
index = nextIndex + 3;
}
else{ // single backslash
build.Append('\\');
index = nextIndex+1;
index = nextIndex + 1;
}
}
}
@@ -125,8 +125,8 @@ namespace TweetLib.Core.Serialization{
}
}
else{
line = contents.Substring(currentPos, nextPos-currentPos);
currentPos = nextPos+NewLineReal.Length;
line = contents.Substring(currentPos, nextPos - currentPos);
currentPos = nextPos + NewLineReal.Length;
}
int space = line.IndexOf(' ');
@@ -137,7 +137,7 @@ namespace TweetLib.Core.Serialization{
}
string property = line.Substring(0, space);
string value = UnescapeLine(line.Substring(space+1));
string value = UnescapeLine(line.Substring(space + 1));
if (props.TryGetValue(property, out PropertyInfo info)){
if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)){

View File

@@ -7,7 +7,7 @@ namespace TweetLib.Core.Utils{
string dir = Path.GetDirectoryName(file);
if (dir == null){
throw new ArgumentException("Invalid file path: "+file);
throw new ArgumentException("Invalid file path: " + file);
}
else if (dir.Length > 0){
Directory.CreateDirectory(dir);

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

@@ -68,7 +68,7 @@ namespace TweetTest.Data{
[TestMethod]
public void TestTextFilesAndComplexKeys(){
File.WriteAllText("input_text_1", "Hello World!"+Environment.NewLine);
File.WriteAllText("input_text_1", "Hello World!" + Environment.NewLine);
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("text_files"))){
cfs.WriteFile(new string[]{ "key1", "a", "bb", "ccc", "dddd" }, "input_text_1");
@@ -93,7 +93,7 @@ namespace TweetTest.Data{
}
Assert.IsTrue(File.Exists("text_file_1"));
Assert.AreEqual("Hello World!"+Environment.NewLine, File.ReadAllText("text_file_1"));
Assert.AreEqual("Hello World!" + Environment.NewLine, File.ReadAllText("text_file_1"));
}
[TestMethod]

View File

@@ -30,7 +30,7 @@ namespace TweetTest.Data{
TestBool = true,
TestInt = -100,
TestStringBasic = "hello123",
TestStringNewLine = "abc"+Environment.NewLine+"def"+Environment.NewLine,
TestStringNewLine = "abc" + Environment.NewLine + "def" + Environment.NewLine,
TestStringBackslash = @"C:\Test\\\Abc\",
TestStringNull = null,
TestEnum = TestEnum.D
@@ -45,7 +45,7 @@ namespace TweetTest.Data{
Assert.IsTrue(read.TestBool);
Assert.AreEqual(-100, read.TestInt);
Assert.AreEqual("hello123", read.TestStringBasic);
Assert.AreEqual("abc"+Environment.NewLine+"def"+Environment.NewLine, read.TestStringNewLine);
Assert.AreEqual("abc" + Environment.NewLine + "def" + Environment.NewLine, read.TestStringNewLine);
Assert.AreEqual(@"C:\Test\\\Abc\", read.TestStringBackslash);
Assert.IsNull(read.TestStringNull);
Assert.AreEqual(TestEnum.D, read.TestEnum);

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"))

Some files were not shown because too many files have changed in this diff Show More