mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 10:32:10 +02:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
a369c65451 | |||
318f65f187 | |||
1cd60e831c | |||
b988959eaa | |||
1eb1f9848a | |||
7f6cc0da01 | |||
19fcb69525 | |||
22cef0a44c | |||
2459d31bff | |||
19f104239a | |||
bd0be65038 | |||
bbb7907e54 | |||
a6963a18d4 | |||
92716ea3e0 | |||
aec4c1feea | |||
d505b3305b | |||
a34a02e14d | |||
26d2d7a51e | |||
c2f7e52d13 | |||
de68d8934d | |||
4fdf7fc958 | |||
42a5e72f19 | |||
f7359ebc8a | |||
f395ac53dc | |||
1113e0b559 | |||
5e3bd31862 | |||
11d978dad1 | |||
f7961024d7 | |||
72973a8707 | |||
68254f48d5 | |||
eac4f30c50 | |||
25680fa980 | |||
ff5e1da14d | |||
95afff7879 | |||
50bd526025 | |||
108a0fefc3 | |||
dd8c5d27be | |||
b2937bc776 |
@@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Notification;
|
||||
using TweetDuck.Core.Other;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Data;
|
||||
using TweetLib.Core.Features.Configuration;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Configuration{
|
||||
sealed class UserConfig : BaseConfig{
|
||||
@@ -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;
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace TweetDuck.Configuration{
|
||||
public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
|
||||
public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty;
|
||||
|
||||
public TwitterUtils.ImageQuality TwitterImageQuality => BestImageQuality ? TwitterUtils.ImageQuality.Orig : TwitterUtils.ImageQuality.Default;
|
||||
public ImageQuality TwitterImageQuality => BestImageQuality ? ImageQuality.Best : ImageQuality.Default;
|
||||
|
||||
public string NotificationSoundPath{
|
||||
get => _notificationSoundPath ?? string.Empty;
|
||||
|
41
Core/Adapters/CefScriptExecutor.cs
Normal file
41
Core/Adapters/CefScriptExecutor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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){");
|
||||
|
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,14 +117,12 @@ namespace TweetDuck.Core.Bridge{
|
||||
}
|
||||
|
||||
public void Alert(string type, string contents){
|
||||
MessageBoxIcon icon;
|
||||
|
||||
switch(type){
|
||||
case "error": icon = MessageBoxIcon.Error; break;
|
||||
case "warning": icon = MessageBoxIcon.Warning; break;
|
||||
case "info": icon = MessageBoxIcon.Information; break;
|
||||
default: icon = MessageBoxIcon.None; break;
|
||||
}
|
||||
MessageBoxIcon icon = type switch{
|
||||
"error" => MessageBoxIcon.Error,
|
||||
"warning" => MessageBoxIcon.Warning,
|
||||
"info" => MessageBoxIcon.Information,
|
||||
_ => MessageBoxIcon.None
|
||||
};
|
||||
|
||||
FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -21,17 +21,16 @@ namespace TweetDuck.Core.Controls{
|
||||
}
|
||||
|
||||
public static float GetDPIScale(this Control control){
|
||||
using(Graphics graphics = control.CreateGraphics()){
|
||||
using Graphics graphics = control.CreateGraphics();
|
||||
return graphics.DpiY / 96F;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsFullyOutsideView(this Form form){
|
||||
return !Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(form.Bounds));
|
||||
}
|
||||
|
||||
public static void MoveToCenter(this Form targetForm, Form parentForm){
|
||||
targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2);
|
||||
targetForm.Location = new Point(parentForm.Location.X + (parentForm.Width / 2) - (targetForm.Width / 2), parentForm.Location.Y + (parentForm.Height / 2) - (targetForm.Height / 2));
|
||||
}
|
||||
|
||||
public static void SetValueInstant(this ProgressBar bar, int value){
|
||||
@@ -63,7 +62,8 @@ namespace TweetDuck.Core.Controls{
|
||||
trackBar.Value = trackBar.SmallChange * (int)Math.Floor(((double)trackBar.Value / trackBar.SmallChange) + 0.5);
|
||||
return false;
|
||||
}
|
||||
else return true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void EnableMultilineShortcuts(this TextBox textBox){
|
||||
|
@@ -1,13 +1,8 @@
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Plugins.Controls{
|
||||
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
|
@@ -8,8 +8,8 @@ namespace TweetDuck.Core.Controls{
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e){
|
||||
int y = (int)Math.Floor((ClientRectangle.Height - Text.Length * LineHeight) / 2F) - 1;
|
||||
using Brush brush = new SolidBrush(ForeColor);
|
||||
|
||||
using(Brush brush = new SolidBrush(ForeColor)){
|
||||
foreach(char chr in Text){
|
||||
string str = chr.ToString();
|
||||
float x = (ClientRectangle.Width - e.Graphics.MeasureString(str, Font).Width) / 2F;
|
||||
@@ -20,4 +20,3 @@ namespace TweetDuck.Core.Controls{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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(Program.Config.Plugins, Program.PluginPath);
|
||||
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
|
||||
this.plugins.Reloaded += plugins_Reloaded;
|
||||
this.plugins.Executed += plugins_Executed;
|
||||
this.plugins.Reload();
|
||||
@@ -78,7 +78,6 @@ namespace TweetDuck.Core{
|
||||
|
||||
this.updateBridge = new UpdateBridge(updates, this);
|
||||
this.updateBridge.UpdateAccepted += updateBridge_UpdateAccepted;
|
||||
this.updateBridge.UpdateDelayed += updateBridge_UpdateDelayed;
|
||||
this.updateBridge.UpdateDismissed += updateBridge_UpdateDismissed;
|
||||
|
||||
this.browser = new TweetDeckBrowser(this, plugins, new TweetDeckBridge.Browser(this, notification), updateBridge);
|
||||
@@ -236,7 +235,9 @@ namespace TweetDuck.Core{
|
||||
|
||||
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
|
||||
if (e.HasErrors){
|
||||
this.InvokeAsyncSafe(() => { // TODO not needed but makes code consistent...
|
||||
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoaded){
|
||||
@@ -244,9 +245,11 @@ namespace TweetDuck.Core{
|
||||
}
|
||||
}
|
||||
|
||||
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
|
||||
private void plugins_Executed(object sender, PluginErrorEventArgs e){
|
||||
if (e.HasErrors){
|
||||
this.InvokeAsyncSafe(() => {
|
||||
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,10 +322,6 @@ namespace TweetDuck.Core{
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBridge_UpdateDelayed(object sender, UpdateInfo update){
|
||||
// stops the timer
|
||||
}
|
||||
|
||||
private void updateBridge_UpdateDismissed(object sender, UpdateInfo update){
|
||||
Config.DismissedUpdate = update.VersionTag;
|
||||
Config.Save();
|
||||
@@ -330,7 +329,9 @@ namespace TweetDuck.Core{
|
||||
|
||||
protected override void WndProc(ref Message m){
|
||||
if (isLoaded && m.Msg == Program.WindowRestoreMessage){
|
||||
if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
|
||||
using Process me = Process.GetCurrentProcess();
|
||||
|
||||
if (me.Id == m.WParam.ToInt32()){
|
||||
trayIcon_ClickRestore(trayIcon, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -367,14 +368,7 @@ namespace TweetDuck.Core{
|
||||
}
|
||||
|
||||
public void ReloadToTweetDeck(){
|
||||
#if DEBUG
|
||||
ScriptLoader.HotSwap();
|
||||
#else
|
||||
if (ModifierKeys.HasFlag(Keys.Shift)){
|
||||
ScriptLoader.ClearCache();
|
||||
}
|
||||
#endif
|
||||
|
||||
Program.Resources.OnReloadTriggered();
|
||||
ignoreUpdateCheckError = false;
|
||||
browser.ReloadToTweetDeck();
|
||||
AnalyticsFile.BrowserReloads.Trigger();
|
||||
|
@@ -6,19 +6,20 @@ 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();
|
||||
|
||||
private static TwitterUtils.ImageQuality ImageQuality => Config.TwitterImageQuality;
|
||||
protected static UserConfig Config => Program.Config.User;
|
||||
private static ImageQuality ImageQuality => Config.TwitterImageQuality;
|
||||
|
||||
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
|
||||
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501;
|
||||
@@ -41,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)){
|
||||
@@ -55,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");
|
||||
@@ -79,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"));
|
||||
@@ -107,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);
|
||||
@@ -115,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: {
|
||||
@@ -185,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){
|
||||
@@ -193,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){
|
||||
|
@@ -2,7 +2,7 @@
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Management;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Core.Handling{
|
||||
sealed class ContextMenuBrowser : ContextMenuBase{
|
||||
@@ -53,7 +53,7 @@ namespace TweetDuck.Core.Handling{
|
||||
|
||||
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
|
||||
|
||||
if (isSelecting && !isEditing && TwitterUtils.IsTweetDeckWebsite(frame)){
|
||||
if (isSelecting && !isEditing && TwitterUrls.IsTweetDeck(frame.Url)){
|
||||
InsertSelectionSearchItem(model, MenuSearchInColumn, "Search in a column");
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
@@ -9,7 +8,7 @@ namespace TweetDuck.Core.Handling.General{
|
||||
sealed class FileDialogHandler : IDialogHandler{
|
||||
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback){
|
||||
if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple){
|
||||
string allFilters = string.Join(";", acceptFilters.Select(filter => "*"+filter));
|
||||
string allFilters = string.Join(";", acceptFilters.SelectMany(ParseFileType).Where(filter => !string.IsNullOrEmpty(filter)).Select(filter => "*" + filter));
|
||||
|
||||
using(OpenFileDialog dialog = new OpenFileDialog{
|
||||
AutoUpgradeEnabled = true,
|
||||
@@ -19,8 +18,8 @@ namespace TweetDuck.Core.Handling.General{
|
||||
Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*"
|
||||
}){
|
||||
if (dialog.ShowDialog() == DialogResult.OK){
|
||||
string ext = Path.GetExtension(dialog.FileName);
|
||||
callback.Continue(acceptFilters.FindIndex(filter => filter.Equals(ext, StringComparison.OrdinalIgnoreCase)), dialog.FileNames.ToList());
|
||||
string ext = Path.GetExtension(dialog.FileName)?.ToLower();
|
||||
callback.Continue(acceptFilters.FindIndex(filter => ParseFileType(filter).Contains(ext)), dialog.FileNames.ToList());
|
||||
}
|
||||
else{
|
||||
callback.Cancel();
|
||||
@@ -36,5 +35,27 @@ namespace TweetDuck.Core.Handling.General{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ParseFileType(string type){
|
||||
if (string.IsNullOrEmpty(type)){
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
if (type[0] == '.'){
|
||||
return new string[]{ type };
|
||||
}
|
||||
|
||||
switch(type){
|
||||
case "image/jpeg": return new string[]{ ".jpg", ".jpeg" };
|
||||
case "image/png": return new string[]{ ".png" };
|
||||
case "image/gif": return new string[]{ ".gif" };
|
||||
case "image/webp": return new string[]{ ".webp" };
|
||||
case "video/mp4": return new string[]{ ".mp4" };
|
||||
case "video/quicktime": return new string[]{ ".mov", ".qt" };
|
||||
}
|
||||
|
||||
System.Diagnostics.Debugger.Break();
|
||||
return new string[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,16 +12,18 @@ namespace TweetDuck.Core.Handling.General{
|
||||
int pipe = text.IndexOf('|');
|
||||
|
||||
if (pipe != -1){
|
||||
switch(text.Substring(0, pipe)){
|
||||
case "error": icon = MessageBoxIcon.Error; break;
|
||||
case "warning": icon = MessageBoxIcon.Warning; break;
|
||||
case "info": icon = MessageBoxIcon.Information; break;
|
||||
case "question": icon = MessageBoxIcon.Question; break;
|
||||
default: return new FormMessage(caption, text, icon);
|
||||
}
|
||||
icon = text.Substring(0, pipe) switch{
|
||||
"error" => MessageBoxIcon.Error,
|
||||
"warning" => MessageBoxIcon.Warning,
|
||||
"info" => MessageBoxIcon.Information,
|
||||
"question" => MessageBoxIcon.Question,
|
||||
_ => MessageBoxIcon.None
|
||||
};
|
||||
|
||||
if (icon != MessageBoxIcon.None){
|
||||
text = text.Substring(pipe + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return new FormMessage(caption, text, icon);
|
||||
}
|
||||
|
@@ -4,11 +4,15 @@ using TweetDuck.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Core.Handling.General{
|
||||
sealed class LifeSpanHandler : ILifeSpanHandler{
|
||||
private static bool IsPopupAllowed(string url){
|
||||
return url.StartsWith("https://twitter.com/teams/authorize?");
|
||||
}
|
||||
|
||||
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl){
|
||||
switch(targetDisposition){
|
||||
case WindowOpenDisposition.NewBackgroundTab:
|
||||
case WindowOpenDisposition.NewForegroundTab:
|
||||
case WindowOpenDisposition.NewPopup:
|
||||
case WindowOpenDisposition.NewPopup when !IsPopupAllowed(targetUrl):
|
||||
case WindowOpenDisposition.NewWindow:
|
||||
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
|
||||
return true;
|
||||
|
@@ -7,10 +7,11 @@ using CefSharp;
|
||||
using CefSharp.Handler;
|
||||
using TweetDuck.Core.Handling.General;
|
||||
using TweetDuck.Core.Utils;
|
||||
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){
|
||||
@@ -21,11 +22,7 @@ namespace TweetDuck.Core.Handling{
|
||||
TweetDeckHashes.Clear();
|
||||
|
||||
foreach(string rule in rules.Replace(" ", "").ToLower().Split(',')){
|
||||
string[] split = rule.Split('=');
|
||||
|
||||
if (split.Length == 2){
|
||||
string key = split[0];
|
||||
string hash = split[1];
|
||||
var (key, hash) = StringUtils.SplitInTwo(rule, '=') ?? throw new ArgumentException("A rule must have one '=' character: " + rule);
|
||||
|
||||
if (hash.All(chr => char.IsDigit(chr) || (chr >= 'a' && chr <= 'f'))){
|
||||
TweetDeckHashes.Add(key, hash);
|
||||
@@ -34,10 +31,6 @@ namespace TweetDuck.Core.Handling{
|
||||
throw new ArgumentException("Invalid hash characters: " + rule);
|
||||
}
|
||||
}
|
||||
else{
|
||||
throw new ArgumentException("A rule must have exactly one '=' character: "+rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly bool autoReload;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TweetDuck.Core.Other;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
@@ -74,7 +73,7 @@ namespace TweetDuck.Core.Management{
|
||||
Items items = Items.None;
|
||||
|
||||
try{
|
||||
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))){
|
||||
using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None));
|
||||
string key;
|
||||
|
||||
while((key = stream.SkipFile()) != null){
|
||||
@@ -97,7 +96,6 @@ namespace TweetDuck.Core.Management{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(Exception){
|
||||
items = Items.None;
|
||||
}
|
||||
@@ -141,7 +139,7 @@ namespace TweetDuck.Core.Management{
|
||||
|
||||
entry.WriteToFile(Path.Combine(Program.PluginDataPath, value[0], value[1]), true);
|
||||
|
||||
if (!plugins.IsPluginInstalled(value[0])){
|
||||
if (!plugins.Plugins.Any(plugin => plugin.Identifier.Equals(value[0]))){
|
||||
missingPlugins.Add(value[0]);
|
||||
}
|
||||
}
|
||||
|
@@ -2,17 +2,17 @@
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Core.Notification.Example{
|
||||
sealed class FormNotificationExample : FormNotificationMain{
|
||||
public override bool RequiresResize => true;
|
||||
protected override bool CanDragWindow => Config.NotificationPosition == TweetNotification.Position.Custom;
|
||||
protected override bool CanDragWindow => Config.NotificationPosition == DesktopNotification.Position.Custom;
|
||||
|
||||
protected override FormBorderStyle NotificationBorderStyle{
|
||||
get{
|
||||
if (Config.NotificationSize == TweetNotification.Size.Custom){
|
||||
if (Config.NotificationSize == DesktopNotification.Size.Custom){
|
||||
switch(base.NotificationBorderStyle){
|
||||
case FormBorderStyle.FixedSingle: return FormBorderStyle.Sizable;
|
||||
case FormBorderStyle.FixedToolWindow: return FormBorderStyle.SizableToolWindow;
|
||||
@@ -27,18 +27,18 @@ namespace TweetDuck.Core.Notification.Example{
|
||||
|
||||
public event EventHandler Ready;
|
||||
|
||||
private readonly TweetNotification exampleNotification;
|
||||
private readonly DesktopNotification exampleNotification;
|
||||
|
||||
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false){
|
||||
browser.LoadingStateChanged += browser_LoadingStateChanged;
|
||||
|
||||
string exampleTweetHTML = ScriptLoader.LoadResourceSilent("pages/example.html")?.Replace("{avatar}", TweetNotification.AppLogo.Url) ?? string.Empty;
|
||||
string exampleTweetHTML = Program.Resources.LoadSilent("pages/example.html")?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
|
||||
|
||||
#if DEBUG
|
||||
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
|
||||
#endif
|
||||
|
||||
exampleNotification = new TweetNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
|
||||
exampleNotification = new DesktopNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
|
||||
|
@@ -1,28 +1,34 @@
|
||||
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{
|
||||
switch(TweetDeckBridge.FontSize){
|
||||
case "largest": return 4;
|
||||
case "large": return 3;
|
||||
case "small": return 1;
|
||||
case "smallest": return 0;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
get => FontSize switch{
|
||||
"largest" => 4,
|
||||
"large" => 3,
|
||||
"small" => 1,
|
||||
"smallest" => 0,
|
||||
_ => 2
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual Point PrimaryLocation{
|
||||
@@ -39,19 +45,19 @@ namespace TweetDuck.Core.Notification{
|
||||
int edgeDist = Config.NotificationEdgeDistance;
|
||||
|
||||
switch(Config.NotificationPosition){
|
||||
case TweetNotification.Position.TopLeft:
|
||||
case DesktopNotification.Position.TopLeft:
|
||||
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + edgeDist);
|
||||
|
||||
case TweetNotification.Position.TopRight:
|
||||
case DesktopNotification.Position.TopRight:
|
||||
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
|
||||
|
||||
case TweetNotification.Position.BottomLeft:
|
||||
case DesktopNotification.Position.BottomLeft:
|
||||
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
|
||||
|
||||
case TweetNotification.Position.BottomRight:
|
||||
case DesktopNotification.Position.BottomRight:
|
||||
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
|
||||
|
||||
case TweetNotification.Position.Custom:
|
||||
case DesktopNotification.Position.Custom:
|
||||
if (!Config.IsCustomNotificationPositionSet){
|
||||
Config.CustomNotificationPosition = new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
|
||||
Config.Save();
|
||||
@@ -101,7 +107,7 @@ namespace TweetDuck.Core.Notification{
|
||||
|
||||
private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification();
|
||||
|
||||
private TweetNotification currentNotification;
|
||||
private DesktopNotification currentNotification;
|
||||
private int pauseCounter;
|
||||
|
||||
public string CurrentTweetUrl => currentNotification?.TweetUrl;
|
||||
@@ -122,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),
|
||||
@@ -188,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);
|
||||
}
|
||||
|
||||
|
@@ -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{
|
||||
@@ -44,27 +46,17 @@ namespace TweetDuck.Core.Notification{
|
||||
}
|
||||
|
||||
private int BaseClientWidth{
|
||||
get{
|
||||
switch(Config.NotificationSize){
|
||||
default:
|
||||
return BrowserUtils.Scale(284, SizeScale*(1.0+0.05*FontSizeLevel));
|
||||
|
||||
case TweetNotification.Size.Custom:
|
||||
return Config.CustomNotificationSize.Width;
|
||||
}
|
||||
}
|
||||
get => Config.NotificationSize switch{
|
||||
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Width,
|
||||
_ => BrowserUtils.Scale(284, SizeScale * (1.0 + 0.05 * FontSizeLevel))
|
||||
};
|
||||
}
|
||||
|
||||
private int BaseClientHeight{
|
||||
get{
|
||||
switch(Config.NotificationSize){
|
||||
default:
|
||||
return BrowserUtils.Scale(122, SizeScale*(1.0+0.08*FontSizeLevel));
|
||||
|
||||
case TweetNotification.Size.Custom:
|
||||
return Config.CustomNotificationSize.Height;
|
||||
}
|
||||
}
|
||||
get => Config.NotificationSize switch{
|
||||
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";
|
||||
@@ -83,7 +75,7 @@ namespace TweetDuck.Core.Notification{
|
||||
browser.LoadingStateChanged += Browser_LoadingStateChanged;
|
||||
browser.FrameLoadEnd += Browser_FrameLoadEnd;
|
||||
|
||||
plugins.Register(browser, PluginEnvironment.Notification, this);
|
||||
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser));
|
||||
|
||||
mouseHookDelegate = MouseHookProc;
|
||||
Disposed += (sender, args) => StopMouseHook(true);
|
||||
@@ -164,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +166,16 @@ namespace TweetDuck.Core.Notification{
|
||||
}
|
||||
|
||||
private void timerHideProgress_Tick(object sender, EventArgs e){
|
||||
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen){
|
||||
bool isCursorInside = Bounds.Contains(Cursor.Position);
|
||||
|
||||
if (isCursorInside){
|
||||
StartMouseHook();
|
||||
}
|
||||
else{
|
||||
StopMouseHook(false);
|
||||
}
|
||||
|
||||
if (isCursorInside || FreezeTimer || ContextMenuOpen){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ namespace TweetDuck.Core.Notification{
|
||||
|
||||
// notification methods
|
||||
|
||||
public virtual void ShowNotification(TweetNotification notification){
|
||||
public virtual void ShowNotification(DesktopNotification notification){
|
||||
LoadTweet(notification);
|
||||
}
|
||||
|
||||
@@ -227,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);
|
||||
@@ -237,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;
|
||||
@@ -265,7 +266,6 @@ namespace TweetDuck.Core.Notification{
|
||||
}
|
||||
|
||||
MoveToVisibleLocation();
|
||||
StartMouseHook();
|
||||
}
|
||||
|
||||
protected virtual void OnNotificationReady(){
|
||||
|
@@ -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){
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -11,18 +11,16 @@ namespace TweetDuck.Core.Notification{
|
||||
public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
|
||||
|
||||
public static IResourceHandler CreateFileHandler(string path){
|
||||
string mimeType;
|
||||
|
||||
switch(Path.GetExtension(path)){
|
||||
case ".weba":
|
||||
case ".webm": mimeType = "audio/webm"; break;
|
||||
case ".wav": mimeType = "audio/wav"; break;
|
||||
case ".ogg": mimeType = "audio/ogg"; break;
|
||||
case ".mp3": mimeType = "audio/mp3"; break;
|
||||
case ".flac": mimeType = "audio/flac"; break;
|
||||
case ".opus": mimeType = "audio/ogg; codecs=opus"; break;
|
||||
default: mimeType = null; break;
|
||||
}
|
||||
string mimeType = Path.GetExtension(path) switch{
|
||||
".weba" => "audio/webm",
|
||||
".webm" => "audio/webm",
|
||||
".wav" => "audio/wav",
|
||||
".ogg" => "audio/ogg",
|
||||
".mp3" => "audio/mp3",
|
||||
".flac" => "audio/flac",
|
||||
".opus" => "audio/ogg; codecs=opus",
|
||||
_ => null
|
||||
};
|
||||
|
||||
try{
|
||||
return ResourceHandler.FromFilePath(path, mimeType);
|
||||
|
@@ -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{
|
||||
|
@@ -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;
|
||||
@@ -82,7 +81,7 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
{ "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
|
||||
0,
|
||||
{ "Plugins All" , List(plugins.Plugins.Select(Plugin)) },
|
||||
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugin => plugins.Config.IsEnabled(plugin)).Select(Plugin)) },
|
||||
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugins.Config.IsEnabled).Select(Plugin)) },
|
||||
0,
|
||||
{ "Theme" , Dict(editLayoutDesign, "_theme", "light/def") },
|
||||
{ "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") },
|
||||
@@ -144,7 +143,8 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
string osName, osEdition, osBuild;
|
||||
|
||||
try{
|
||||
using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false)){
|
||||
using RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false);
|
||||
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
osName = key.GetValue("ProductName") as string;
|
||||
osBuild = key.GetValue("CurrentBuild") as string;
|
||||
@@ -158,7 +158,6 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
osEdition = match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch{
|
||||
osName = osEdition = osBuild = null;
|
||||
}
|
||||
@@ -168,11 +167,11 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
SystemBuild = osBuild ?? "(unknown)";
|
||||
|
||||
try{
|
||||
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory")){
|
||||
using ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory");
|
||||
|
||||
foreach(ManagementBaseObject obj in searcher.Get()){
|
||||
RamSize += (int)((ulong)obj["Capacity"] / (1024L * 1024L));
|
||||
}
|
||||
}
|
||||
}catch{
|
||||
RamSize = 0;
|
||||
}
|
||||
@@ -180,7 +179,8 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
string gpu = null;
|
||||
|
||||
try{
|
||||
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController")){
|
||||
using ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController");
|
||||
|
||||
foreach(ManagementBaseObject obj in searcher.Get()){
|
||||
string vendor = obj["Caption"] as string;
|
||||
|
||||
@@ -188,7 +188,6 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
gpu = vendor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch{
|
||||
// rip
|
||||
}
|
||||
@@ -207,36 +206,30 @@ namespace TweetDuck.Core.Other.Analytics{
|
||||
}
|
||||
|
||||
private static string TrayMode{
|
||||
get{
|
||||
switch(UserConfig.TrayBehavior){
|
||||
case TrayIcon.Behavior.DisplayOnly: return "icon";
|
||||
case TrayIcon.Behavior.MinimizeToTray: return "minimize";
|
||||
case TrayIcon.Behavior.CloseToTray: return "close";
|
||||
case TrayIcon.Behavior.Combined: return "combined";
|
||||
default: return "off";
|
||||
}
|
||||
}
|
||||
get => UserConfig.TrayBehavior switch{
|
||||
TrayIcon.Behavior.DisplayOnly => "icon",
|
||||
TrayIcon.Behavior.MinimizeToTray => "minimize",
|
||||
TrayIcon.Behavior.CloseToTray => "close",
|
||||
TrayIcon.Behavior.Combined => "combined",
|
||||
_ => "off"
|
||||
};
|
||||
}
|
||||
|
||||
private static string NotificationPosition{
|
||||
get{
|
||||
switch(UserConfig.NotificationPosition){
|
||||
case TweetNotification.Position.TopLeft: return "top left";
|
||||
case TweetNotification.Position.TopRight: return "top right";
|
||||
case TweetNotification.Position.BottomLeft: return "bottom left";
|
||||
case TweetNotification.Position.BottomRight: return "bottom right";
|
||||
default: return "custom";
|
||||
}
|
||||
}
|
||||
get => UserConfig.NotificationPosition switch{
|
||||
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{
|
||||
switch(UserConfig.NotificationSize){
|
||||
case TweetNotification.Size.Auto: return "auto";
|
||||
default: return RoundUp(UserConfig.CustomNotificationSize.Width, 20)+"x"+RoundUp(UserConfig.CustomNotificationSize.Height, 20);
|
||||
}
|
||||
}
|
||||
get => UserConfig.NotificationSize switch{
|
||||
DesktopNotification.Size.Auto => "auto",
|
||||
_ => RoundUp(UserConfig.CustomNotificationSize.Width, 20) + "x" + RoundUp(UserConfig.CustomNotificationSize.Height, 20)
|
||||
};
|
||||
}
|
||||
|
||||
private static string NotificationTimer{
|
||||
|
@@ -7,8 +7,8 @@ using TweetDuck.Core.Handling;
|
||||
using TweetDuck.Core.Handling.General;
|
||||
using TweetDuck.Core.Utils;
|
||||
using System.Text.RegularExpressions;
|
||||
using TweetDuck.Core.Adapters;
|
||||
using TweetDuck.Data;
|
||||
using TweetDuck.Resources;
|
||||
|
||||
namespace TweetDuck.Core.Other{
|
||||
sealed partial class FormGuide : Form, FormManager.IAppDialog{
|
||||
@@ -116,7 +116,7 @@ namespace TweetDuck.Core.Other{
|
||||
}
|
||||
|
||||
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
|
||||
ScriptLoader.ExecuteScript(e.Frame, "Array.prototype.forEach.call(document.getElementsByTagName('A'), ele => ele.addEventListener('click', e => { e.preventDefault(); window.open(ele.getAttribute('href')); }))", "gen:links");
|
||||
CefScriptExecutor.RunScript(e.Frame, "Array.prototype.forEach.call(document.getElementsByTagName('A'), ele => ele.addEventListener('click', e => { e.preventDefault(); window.open(ele.getAttribute('href')); }))", "gen:links");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
Core/Other/FormPlugins.Designer.cs
generated
8
Core/Other/FormPlugins.Designer.cs
generated
@@ -1,4 +1,6 @@
|
||||
namespace TweetDuck.Core.Other {
|
||||
using TweetDuck.Core.Controls;
|
||||
|
||||
namespace TweetDuck.Core.Other {
|
||||
partial class FormPlugins {
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -27,7 +29,7 @@
|
||||
this.btnClose = new System.Windows.Forms.Button();
|
||||
this.btnReload = new System.Windows.Forms.Button();
|
||||
this.btnOpenFolder = new System.Windows.Forms.Button();
|
||||
this.flowLayoutPlugins = new TweetDuck.Plugins.Controls.PluginListFlowLayout();
|
||||
this.flowLayoutPlugins = new FlowLayoutPanelNoHScroll();
|
||||
this.timerLayout = new System.Windows.Forms.Timer(this.components);
|
||||
this.SuspendLayout();
|
||||
//
|
||||
@@ -117,7 +119,7 @@
|
||||
private System.Windows.Forms.Button btnClose;
|
||||
private System.Windows.Forms.Button btnReload;
|
||||
private System.Windows.Forms.Button btnOpenFolder;
|
||||
private Plugins.Controls.PluginListFlowLayout flowLayoutPlugins;
|
||||
private FlowLayoutPanelNoHScroll flowLayoutPlugins;
|
||||
private System.Windows.Forms.Timer timerLayout;
|
||||
}
|
||||
}
|
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Plugins.Controls;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Core.Other{
|
||||
@@ -96,7 +95,7 @@ namespace TweetDuck.Core.Other{
|
||||
}
|
||||
|
||||
private void btnOpenFolder_Click(object sender, EventArgs e){
|
||||
using(Process.Start("explorer.exe", '"'+pluginManager.PathCustomPlugins+'"')){}
|
||||
App.SystemHandler.OpenFileExplorer(pluginManager.PathCustomPlugins);
|
||||
}
|
||||
|
||||
private void btnReload_Click(object sender, EventArgs e){
|
||||
|
@@ -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{
|
||||
|
@@ -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)){
|
||||
|
@@ -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{
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Configuration;
|
||||
@@ -7,6 +6,7 @@ using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Management;
|
||||
using TweetDuck.Core.Other.Settings.Dialogs;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetLib.Core;
|
||||
|
||||
namespace TweetDuck.Core.Other.Settings{
|
||||
sealed partial class TabSettingsAdvanced : BaseTabSettings{
|
||||
@@ -67,11 +67,11 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
#region Application
|
||||
|
||||
private void btnOpenAppFolder_Click(object sender, EventArgs e){
|
||||
using(Process.Start("explorer.exe", "\""+Program.ProgramPath+"\"")){}
|
||||
App.SystemHandler.OpenFileExplorer(Program.ProgramPath);
|
||||
}
|
||||
|
||||
private void btnOpenDataFolder_Click(object sender, EventArgs e){
|
||||
using(Process.Start("explorer.exe", "\""+Program.StoragePath+"\"")){}
|
||||
App.SystemHandler.OpenFileExplorer(Program.StoragePath);
|
||||
}
|
||||
|
||||
private void btnRestart_Click(object sender, EventArgs e){
|
||||
|
@@ -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{
|
||||
|
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Notification;
|
||||
using TweetDuck.Core.Notification.Example;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
|
||||
namespace TweetDuck.Core.Other.Settings{
|
||||
sealed partial class TabSettingsNotifications : BaseTabSettings{
|
||||
@@ -66,18 +66,18 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
toolTip.SetToolTip(radioLocCustom, "Drag the example notification window to the desired location.");
|
||||
|
||||
switch(Config.NotificationPosition){
|
||||
case TweetNotification.Position.TopLeft: radioLocTL.Checked = true; break;
|
||||
case TweetNotification.Position.TopRight: radioLocTR.Checked = true; break;
|
||||
case TweetNotification.Position.BottomLeft: radioLocBL.Checked = true; break;
|
||||
case TweetNotification.Position.BottomRight: radioLocBR.Checked = true; break;
|
||||
case TweetNotification.Position.Custom: radioLocCustom.Checked = true; break;
|
||||
case DesktopNotification.Position.TopLeft: radioLocTL.Checked = true; break;
|
||||
case DesktopNotification.Position.TopRight: radioLocTR.Checked = true; break;
|
||||
case DesktopNotification.Position.BottomLeft: radioLocBL.Checked = true; break;
|
||||
case DesktopNotification.Position.BottomRight: radioLocBR.Checked = true; break;
|
||||
case DesktopNotification.Position.Custom: radioLocCustom.Checked = true; break;
|
||||
}
|
||||
|
||||
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = !radioLocCustom.Checked;
|
||||
comboBoxDisplay.Items.Add("(Same as TweetDuck)");
|
||||
|
||||
foreach(Screen screen in Screen.AllScreens){
|
||||
comboBoxDisplay.Items.Add(screen.DeviceName.TrimStart('\\', '.')+" ("+screen.Bounds.Width+"x"+screen.Bounds.Height+")");
|
||||
comboBoxDisplay.Items.Add($"{screen.DeviceName.TrimStart('\\', '.')} ({screen.Bounds.Width}x{screen.Bounds.Height})");
|
||||
}
|
||||
|
||||
comboBoxDisplay.SelectedIndex = Math.Min(comboBoxDisplay.Items.Count - 1, Config.NotificationDisplay);
|
||||
@@ -91,8 +91,8 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
toolTip.SetToolTip(radioSizeCustom, "Resize the example notification window to the desired size.");
|
||||
|
||||
switch(Config.NotificationSize){
|
||||
case TweetNotification.Size.Auto: radioSizeAuto.Checked = true; break;
|
||||
case TweetNotification.Size.Custom: radioSizeCustom.Checked = true; break;
|
||||
case DesktopNotification.Size.Auto: radioSizeAuto.Checked = true; break;
|
||||
case DesktopNotification.Size.Custom: radioSizeCustom.Checked = true; break;
|
||||
}
|
||||
|
||||
trackBarScrollSpeed.SetValueSafe(Config.NotificationScrollSpeed);
|
||||
@@ -219,10 +219,10 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
#region Location
|
||||
|
||||
private void radioLoc_CheckedChanged(object sender, EventArgs e){
|
||||
if (radioLocTL.Checked)Config.NotificationPosition = TweetNotification.Position.TopLeft;
|
||||
else if (radioLocTR.Checked)Config.NotificationPosition = TweetNotification.Position.TopRight;
|
||||
else if (radioLocBL.Checked)Config.NotificationPosition = TweetNotification.Position.BottomLeft;
|
||||
else if (radioLocBR.Checked)Config.NotificationPosition = TweetNotification.Position.BottomRight;
|
||||
if (radioLocTL.Checked)Config.NotificationPosition = DesktopNotification.Position.TopLeft;
|
||||
else if (radioLocTR.Checked)Config.NotificationPosition = DesktopNotification.Position.TopRight;
|
||||
else if (radioLocBL.Checked)Config.NotificationPosition = DesktopNotification.Position.BottomLeft;
|
||||
else if (radioLocBR.Checked)Config.NotificationPosition = DesktopNotification.Position.BottomRight;
|
||||
|
||||
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = true;
|
||||
notification.ShowExampleNotification(false);
|
||||
@@ -233,18 +233,18 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
Config.CustomNotificationPosition = notification.Location;
|
||||
}
|
||||
|
||||
Config.NotificationPosition = TweetNotification.Position.Custom;
|
||||
Config.NotificationPosition = DesktopNotification.Position.Custom;
|
||||
|
||||
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = false;
|
||||
notification.ShowExampleNotification(false);
|
||||
|
||||
if (notification.IsFullyOutsideView() && FormMessage.Question("Notification is Outside View", "The notification seems to be outside of view, would you like to reset its position?", FormMessage.Yes, FormMessage.No)){
|
||||
Config.NotificationPosition = TweetNotification.Position.TopRight;
|
||||
Config.NotificationPosition = DesktopNotification.Position.TopRight;
|
||||
notification.MoveToVisibleLocation();
|
||||
|
||||
Config.CustomNotificationPosition = notification.Location;
|
||||
|
||||
Config.NotificationPosition = TweetNotification.Position.Custom;
|
||||
Config.NotificationPosition = DesktopNotification.Position.Custom;
|
||||
notification.MoveToVisibleLocation();
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,7 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
|
||||
private void radioSize_CheckedChanged(object sender, EventArgs e){
|
||||
if (radioSizeAuto.Checked){
|
||||
Config.NotificationSize = TweetNotification.Size.Auto;
|
||||
Config.NotificationSize = DesktopNotification.Size.Auto;
|
||||
}
|
||||
|
||||
notification.ShowExampleNotification(false);
|
||||
@@ -276,7 +276,7 @@ namespace TweetDuck.Core.Other.Settings{
|
||||
Config.CustomNotificationSize = notification.BrowserSize;
|
||||
}
|
||||
|
||||
Config.NotificationSize = TweetNotification.Size.Custom;
|
||||
Config.NotificationSize = DesktopNotification.Size.Custom;
|
||||
notification.ShowExampleNotification(false);
|
||||
}
|
||||
|
||||
|
@@ -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, owner, 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){
|
||||
|
@@ -7,7 +7,7 @@ using System.Windows.Forms;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Core.Other;
|
||||
using TweetLib.Core.Utils;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Core.Utils{
|
||||
static class BrowserUtils{
|
||||
@@ -60,8 +60,12 @@ namespace TweetDuck.Core.Utils{
|
||||
}
|
||||
|
||||
public static void SetupZoomEvents(this ChromiumWebBrowser browser){
|
||||
static void SetZoomLevel(IBrowserHost host, int percentage){
|
||||
host.SetZoomLevel(Math.Log(percentage / 100.0, 1.2));
|
||||
}
|
||||
|
||||
void UpdateZoomLevel(object sender, EventArgs args){
|
||||
SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
|
||||
SetZoomLevel(browser.GetBrowserHost(), Config.ZoomLevel);
|
||||
}
|
||||
|
||||
Config.ZoomLevelChanged += UpdateZoomLevel;
|
||||
@@ -69,7 +73,7 @@ namespace TweetDuck.Core.Utils{
|
||||
|
||||
browser.FrameLoadStart += (sender, args) => {
|
||||
if (args.Frame.IsMain && Config.ZoomLevel != 100){
|
||||
SetZoomLevel(args.Browser, Config.ZoomLevel);
|
||||
SetZoomLevel(args.Browser.GetHost(), Config.ZoomLevel);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -77,8 +81,8 @@ namespace TweetDuck.Core.Utils{
|
||||
public static void OpenExternalBrowser(string url){
|
||||
if (string.IsNullOrWhiteSpace(url))return;
|
||||
|
||||
switch(UrlUtils.Check(url)){
|
||||
case UrlUtils.CheckResult.Fine:
|
||||
switch(TwitterUrls.Check(url)){
|
||||
case TwitterUrls.UrlType.Fine:
|
||||
if (FormGuide.CheckGuideUrl(url, out string hash)){
|
||||
FormGuide.Show(hash);
|
||||
}
|
||||
@@ -99,9 +103,9 @@ namespace TweetDuck.Core.Utils{
|
||||
|
||||
break;
|
||||
|
||||
case UrlUtils.CheckResult.Tracking:
|
||||
case TwitterUrls.UrlType.Tracking:
|
||||
if (Config.IgnoreTrackingUrlWarning){
|
||||
goto case UrlUtils.CheckResult.Fine;
|
||||
goto case TwitterUrls.UrlType.Fine;
|
||||
}
|
||||
|
||||
using(FormMessage form = new FormMessage("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n" + url, MessageBoxIcon.Warning)){
|
||||
@@ -117,20 +121,22 @@ namespace TweetDuck.Core.Utils{
|
||||
}
|
||||
|
||||
if (result == DialogResult.Ignore || result == DialogResult.Yes){
|
||||
goto case UrlUtils.CheckResult.Fine;
|
||||
goto case TwitterUrls.UrlType.Fine;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case UrlUtils.CheckResult.Invalid:
|
||||
case TwitterUrls.UrlType.Invalid:
|
||||
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n" + url, FormMessage.OK);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenExternalSearch(string query){
|
||||
if (string.IsNullOrWhiteSpace(query))return;
|
||||
if (string.IsNullOrWhiteSpace(query)){
|
||||
return;
|
||||
}
|
||||
|
||||
string searchUrl = Config.SearchEngineUrl;
|
||||
|
||||
@@ -156,16 +162,8 @@ namespace TweetDuck.Core.Utils{
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetErrorName(CefErrorCode code){
|
||||
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
|
||||
}
|
||||
|
||||
public static int Scale(int baseValue, double scaleFactor){
|
||||
return (int)Math.Round(baseValue * scaleFactor);
|
||||
}
|
||||
|
||||
public static void SetZoomLevel(IBrowser browser, int percentage){
|
||||
browser.GetHost().SetZoomLevel(Math.Log(percentage/100.0, 1.2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -10,77 +9,26 @@ using TweetDuck.Data;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using TweetLib.Core.Utils;
|
||||
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 readonly string[] ValidImageExtensions = {
|
||||
".jpg", ".jpeg", ".png", ".gif"
|
||||
};
|
||||
|
||||
public enum ImageQuality{
|
||||
Default, Orig
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
private static string ExtractMediaBaseLink(string url){
|
||||
int slash = url.LastIndexOf('/');
|
||||
return slash == -1 ? url : StringUtils.ExtractBefore(url, ':', slash);
|
||||
}
|
||||
|
||||
public static string GetMediaLink(string url, ImageQuality quality){
|
||||
if (quality == ImageQuality.Orig){
|
||||
string result = ExtractMediaBaseLink(url);
|
||||
|
||||
if (url.Contains("//ton.twitter.com/") && url.Contains("/ton/data/dm/")){
|
||||
result += ":large";
|
||||
}
|
||||
else if (result != url || url.Contains("//pbs.twimg.com/media/")){
|
||||
result += ":orig";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else{
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetImageFileName(string url){
|
||||
return UrlUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
|
||||
}
|
||||
|
||||
public static void ViewImage(string url, ImageQuality quality){
|
||||
void ViewImageInternal(string path){
|
||||
static void ViewImageInternal(string path){
|
||||
string ext = Path.GetExtension(path);
|
||||
|
||||
if (ValidImageExtensions.Contains(ext)){
|
||||
if (ImageUrl.ValidExtensions.Contains(ext)){
|
||||
WindowsUtils.OpenAssociatedProgram(path);
|
||||
}
|
||||
else{
|
||||
@@ -88,13 +36,13 @@ namespace TweetDuck.Core.Utils{
|
||||
}
|
||||
}
|
||||
|
||||
string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
|
||||
string file = Path.Combine(BrowserCache.CacheFolder, TwitterUrls.GetImageFileName(url) ?? Path.GetRandomFileName());
|
||||
|
||||
if (FileUtils.FileExistsAndNotEmpty(file)){
|
||||
ViewImageInternal(file);
|
||||
}
|
||||
else{
|
||||
DownloadFileAuth(GetMediaLink(url, quality), file, () => {
|
||||
DownloadFileAuth(TwitterUrls.GetMediaLink(url, quality), file, () => {
|
||||
ViewImageInternal(file);
|
||||
}, ex => {
|
||||
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
|
||||
@@ -111,10 +59,10 @@ namespace TweetDuck.Core.Utils{
|
||||
return;
|
||||
}
|
||||
|
||||
string firstImageLink = GetMediaLink(urls[0], quality);
|
||||
string firstImageLink = TwitterUrls.GetMediaLink(urls[0], quality);
|
||||
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
|
||||
|
||||
string filename = GetImageFileName(firstImageLink);
|
||||
string filename = TwitterUrls.GetImageFileName(firstImageLink);
|
||||
string ext = Path.GetExtension(filename); // includes dot
|
||||
|
||||
using(SaveFileDialog dialog = new SaveFileDialog{
|
||||
@@ -125,7 +73,7 @@ namespace TweetDuck.Core.Utils{
|
||||
Filter = (urls.Length == 1 ? "Image" : "Images") + (string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
|
||||
}){
|
||||
if (dialog.ShowDialog() == DialogResult.OK){
|
||||
void OnFailure(Exception ex){
|
||||
static void OnFailure(Exception ex){
|
||||
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
|
||||
}
|
||||
|
||||
@@ -137,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,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{
|
||||
|
@@ -11,24 +11,17 @@ using Microsoft.Win32;
|
||||
|
||||
namespace TweetDuck.Core.Utils{
|
||||
static class WindowsUtils{
|
||||
private static readonly bool IsWindows8OrNewer = OSVersionEquals(major: 6, minor: 2); // windows 8/10
|
||||
|
||||
public static bool ShouldAvoidToolWindow { get; } = IsWindows8OrNewer;
|
||||
public static bool IsAeroEnabled => IsWindows8OrNewer || (NativeMethods.DwmIsCompositionEnabled(out bool isCompositionEnabled) == 0 && isCompositionEnabled);
|
||||
|
||||
private static readonly Lazy<Regex> RegexStripHtmlStyles = new Lazy<Regex>(() => new Regex(@"\s?(?:style|class)="".*?"""), false);
|
||||
private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false);
|
||||
|
||||
private static readonly bool IsWindows8OrNewer;
|
||||
|
||||
public static int CurrentProcessID { get; }
|
||||
public static bool ShouldAvoidToolWindow { get; }
|
||||
public static bool IsAeroEnabled => IsWindows8OrNewer || (NativeMethods.DwmIsCompositionEnabled(out bool isCompositionEnabled) == 0 && isCompositionEnabled);
|
||||
|
||||
static WindowsUtils(){
|
||||
using(Process me = Process.GetCurrentProcess()){
|
||||
CurrentProcessID = me.Id;
|
||||
}
|
||||
|
||||
private static bool OSVersionEquals(int major, int minor){
|
||||
Version ver = Environment.OSVersion.Version;
|
||||
IsWindows8OrNewer = ver.Major == 6 && ver.Minor == 2; // windows 8/10
|
||||
|
||||
ShouldAvoidToolWindow = IsWindows8OrNewer;
|
||||
return ver.Major == major && ver.Minor == minor;
|
||||
}
|
||||
|
||||
public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
|
||||
@@ -114,16 +107,18 @@ namespace TweetDuck.Core.Utils{
|
||||
}
|
||||
|
||||
public static IEnumerable<Browser> FindInstalledBrowsers(){
|
||||
IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive){
|
||||
using(RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default))
|
||||
using(RegistryKey browserList = root.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet", false)){
|
||||
static IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive){
|
||||
using RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default);
|
||||
using RegistryKey browserList = root.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet", false);
|
||||
|
||||
if (browserList == null){
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach(string sub in browserList.GetSubKeyNames()){
|
||||
using(RegistryKey browserKey = browserList.OpenSubKey(sub, false))
|
||||
using(RegistryKey shellKey = browserKey?.OpenSubKey(@"shell\open\command")){
|
||||
using RegistryKey browserKey = browserList.OpenSubKey(sub, false);
|
||||
using RegistryKey shellKey = browserKey?.OpenSubKey(@"shell\open\command");
|
||||
|
||||
if (shellKey == null){
|
||||
continue;
|
||||
}
|
||||
@@ -142,8 +137,6 @@ namespace TweetDuck.Core.Utils{
|
||||
yield return new Browser(browserName, browserPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Browser> browsers = new HashSet<Browser>();
|
||||
|
||||
|
58
Impl/LockHandler.cs
Normal file
58
Impl/LockHandler.cs
Normal 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
16
Impl/SystemHandler.cs
Normal 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('/', '\\') + '"')){}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
namespace TweetDuck.Plugins.Controls {
|
||||
namespace TweetDuck.Plugins {
|
||||
partial class PluginControl {
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
@@ -6,7 +6,7 @@ using TweetDuck.Core.Utils;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
||||
namespace TweetDuck.Plugins.Controls{
|
||||
namespace TweetDuck.Plugins{
|
||||
sealed partial class PluginControl : UserControl{
|
||||
private readonly PluginManager pluginManager;
|
||||
private readonly Plugin plugin;
|
||||
@@ -64,11 +64,11 @@ namespace TweetDuck.Plugins.Controls{
|
||||
|
||||
int requiredLines = Math.Max(descriptionLines, 1 + (string.IsNullOrEmpty(labelVersion.Text) ? 0 : 1) + (isConfigurable ? 1 : 0));
|
||||
|
||||
switch(requiredLines){
|
||||
case 1: nextHeight = MaximumSize.Height-2*(font.Height-1); break;
|
||||
case 2: nextHeight = MaximumSize.Height-(font.Height-1); break;
|
||||
default: nextHeight = MaximumSize.Height; break;
|
||||
}
|
||||
nextHeight = requiredLines switch{
|
||||
1 => MaximumSize.Height - 2 * (font.Height - 1),
|
||||
2 => MaximumSize.Height - 1 * (font.Height - 1),
|
||||
_ => MaximumSize.Height
|
||||
};
|
||||
|
||||
if (nextHeight != Height){
|
||||
timerLayout.Start();
|
34
Plugins/PluginDispatcher.cs
Normal file
34
Plugins/PluginDispatcher.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,192 +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.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{
|
||||
private static readonly IReadOnlyDictionary<PluginEnvironment, string> PluginSetupScriptNames = PluginEnvironmentExtensions.Map(null, "plugins.browser.js", "plugins.notification.js");
|
||||
|
||||
public string PathOfficialPlugins => Path.Combine(rootPath, "official");
|
||||
public string PathCustomPlugins => Path.Combine(rootPath, "user");
|
||||
|
||||
public IEnumerable<Plugin> Plugins => plugins;
|
||||
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
|
||||
|
||||
public IPluginConfig Config { get; }
|
||||
|
||||
public event EventHandler<PluginErrorEventArgs> Reloaded;
|
||||
public event EventHandler<PluginErrorEventArgs> Executed;
|
||||
|
||||
private readonly string rootPath;
|
||||
private readonly 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(IPluginConfig config, string rootPath){
|
||||
this.Config = config;
|
||||
this.Config.PluginChangedState += Config_PluginChangedState;
|
||||
|
||||
this.rootPath = rootPath;
|
||||
this.bridge = new PluginBridge(this);
|
||||
}
|
||||
|
||||
public void Register(IWebBrowser browser, PluginEnvironment environment, Control sync, bool asMainBrowser = false){
|
||||
browser.FrameLoadEnd += (sender, args) => {
|
||||
IFrame frame = args.Frame;
|
||||
|
||||
if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){
|
||||
ExecutePlugins(frame, environment, sync);
|
||||
}
|
||||
};
|
||||
|
||||
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>(2);
|
||||
|
||||
IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
|
||||
if (!Directory.Exists(path)){
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
|
||||
string name = Path.GetFileName(fullDir);
|
||||
|
||||
if (string.IsNullOrEmpty(name)){
|
||||
loadErrors.Add($"{group.GetIdentifierPrefix()}(?): Could not extract directory name from path: {fullDir}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Plugin plugin;
|
||||
|
||||
try{
|
||||
plugin = PluginLoader.FromFolder(name, fullDir, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name), group);
|
||||
}catch(Exception e){
|
||||
loadErrors.Add($"{group.GetIdentifierPrefix()}{name}: {e.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
plugins.UnionWith(LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official));
|
||||
plugins.UnionWith(LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom));
|
||||
|
||||
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
|
||||
}
|
||||
|
||||
private void ExecutePlugins(IFrame frame, PluginEnvironment environment, Control sync){
|
||||
if (!HasAnyPlugin(environment) || !ScriptLoader.ExecuteFile(frame, PluginSetupScriptNames[environment], 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);
|
||||
}
|
||||
|
||||
Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
|
||||
}
|
||||
}
|
||||
}
|
24
Program.cs
24
Program.cs
@@ -12,7 +12,10 @@ using TweetDuck.Core.Handling.General;
|
||||
using TweetDuck.Core.Other;
|
||||
using TweetDuck.Core.Management;
|
||||
using TweetDuck.Core.Utils;
|
||||
using TweetDuck.Impl;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Application.Helpers;
|
||||
using TweetLib.Core.Collections;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
@@ -50,6 +53,7 @@ namespace TweetDuck{
|
||||
|
||||
public static Reporter Reporter { get; }
|
||||
public static ConfigManager Config { get; }
|
||||
public static ScriptLoader Resources { get; }
|
||||
|
||||
static Program(){
|
||||
Reporter = new Reporter(ErrorLogFilePath);
|
||||
@@ -57,8 +61,17 @@ namespace TweetDuck{
|
||||
|
||||
Config = new ConfigManager();
|
||||
|
||||
#if DEBUG
|
||||
Resources = new ScriptLoaderDebug();
|
||||
#else
|
||||
Resources = new ScriptLoader();
|
||||
#endif
|
||||
|
||||
Lib.Initialize(new App.Builder{
|
||||
ErrorHandler = Reporter
|
||||
ErrorHandler = Reporter,
|
||||
LockHandler = new LockHandler(),
|
||||
SystemHandler = new SystemHandler(),
|
||||
ResourceHandler = Resources
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,15 +107,17 @@ namespace TweetDuck{
|
||||
LockManager.Result lockResult = LockManager.Lock();
|
||||
|
||||
if (lockResult == LockManager.Result.HasProcess){
|
||||
if (!LockManager.RestoreLockingProcess(2000) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){
|
||||
if (!LockManager.CloseLockingProcess(10000, 5000)){
|
||||
if (!LockManager.RestoreLockingProcess() && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){
|
||||
if (!LockManager.CloseLockingProcess()){
|
||||
FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
|
||||
return;
|
||||
}
|
||||
|
||||
lockResult = LockManager.Lock();
|
||||
}
|
||||
else return;
|
||||
else{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (lockResult != LockManager.Result.Success){
|
||||
@@ -156,6 +171,7 @@ namespace TweetDuck{
|
||||
Application.ApplicationExit += (sender, args) => ExitCleanup();
|
||||
|
||||
FormBrowser mainForm = new FormBrowser();
|
||||
Resources.Initialize(mainForm);
|
||||
Application.Run(mainForm);
|
||||
|
||||
if (mainForm.UpdateInstallerPath != null){
|
||||
|
@@ -22,7 +22,7 @@ PM> Install-Package CefSharp.WinForms -Version 67.0.0
|
||||
|
||||
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
|
||||
|
||||
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads. An F# compiler must be present when building the project to enable this feature: `C:\Program Files (x86)\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe`
|
||||
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads.
|
||||
|
||||
### Release
|
||||
|
||||
@@ -44,11 +44,11 @@ If you decide to publicly release a custom version, please make it clear that it
|
||||
|
||||
### Installers
|
||||
|
||||
TweetDuck uses **Inno Setup** for installers and updates. First, download and install [InnoSetup QuickStart Pack](http://www.jrsoftware.org/isdl.php) (non-unicode; editor and encryption support not required) and the [Inno Download Plugin](https://code.google.com/archive/p/inno-download-plugin).
|
||||
TweetDuck uses **Inno Setup** for installers and updates. First, download and install [InnoSetup 5.6.1](http://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) (with Preprocessor support) and the [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list).
|
||||
|
||||
Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer for the change to take place.
|
||||
Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer and Visual Studio for the change to take place.
|
||||
|
||||
Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio!
|
||||
Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio first!
|
||||
|
||||
After the window closes, three installers will be generated inside the `bld/Output` folder:
|
||||
* **TweetDuck.exe**
|
||||
|
@@ -578,7 +578,7 @@ ${iconData.map(entry => `#tduck .icon-${entry[0]}:before{content:\"\\f0${entry[1
|
||||
let cols = this.config.columnWidth.slice(1);
|
||||
|
||||
this.css.insert(".column { width: calc((100vw - 205px) / "+cols+" - 6px) !important; min-width: 160px }");
|
||||
this.css.insert(".is-condensed .column { width: calc((100vw - 55px) / "+cols+" - 6px) !important }");
|
||||
this.css.insert(".is-condensed .column { width: calc((100vw - 65px) / "+cols+" - 6px) !important }");
|
||||
}
|
||||
else{
|
||||
this.css.insert(".column { width: "+this.config.columnWidth+" !important }");
|
||||
|
@@ -506,18 +506,6 @@ html.dark .lst-group .selected a:hover{background:#55acee}
|
||||
html.dark .lst-group .selected .fullname,html.dark .lst-group .selected .inner strong,html.dark .lst-group .selected .list-link,html.dark .lst-group .selected .list-twitter-list,html.dark .lst-group .selected .list-subtitle,html.dark .lst-group .selected .list-account,html.dark .lst-group .selected .list-listmember,html.dark .lst-group .selected .txt-ellipsis{color:#F5F8FA}
|
||||
html.dark .lst-group .selected .username,html.dark .lst-group .selected .bytext,html.dark .lst-group .selected .subtitle,html.dark .lst-group .selected .icon-protected{color:#eef3f7}
|
||||
html.dark .itm-remove{border-top:1px solid #ddd}
|
||||
html.dark .caret-outer{border-bottom:7px solid rgba(17,17,17,0.1)}
|
||||
html.dark .caret-inner{border-bottom:6px solid #fff}
|
||||
html.dark .drp-h-divider{border-bottom:1px solid #ddd}
|
||||
html.dark .dropdown-menu .typeahead-item,html.dark .dropdown-menu [data-action]{color:#292F33}
|
||||
html.dark .dropdown-menu .is-selected{background:#55acee;color:#fff}
|
||||
html.dark .dropdown-menu .is-selected [data-action]{color:#fff}
|
||||
html.dark .dropdown-menu .is-selected a:not(:hover):not(:focus){color:#fff}
|
||||
html.dark .dropdown-menu a:not(:hover):not(:focus){color:#292F33}
|
||||
html.dark .dropdown-menu-old li:hover{background:#55acee}
|
||||
html.dark .dropdown-menu-old li:hover a{color:#fff}
|
||||
html.dark .dropdown-menu-old li:hover .attribution{color:#fff}
|
||||
html.dark .non-selectable-item{color:#292F33}
|
||||
html.dark .update-available-item:before{background-color:#FFAD1F}
|
||||
html.dark .is-selected .update-available-item:before{background-color:rgba(41,47,51,0.2)}
|
||||
html.dark .popover{background-color:#fff;box-shadow:0 0 10px rgba(17,17,17,0.7)}
|
||||
|
@@ -1,45 +1,55 @@
|
||||
using CefSharp;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Core.Controls;
|
||||
using TweetDuck.Core.Other;
|
||||
|
||||
#if DEBUG
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using TweetDuck.Core;
|
||||
using TweetDuck.Plugins;
|
||||
#endif
|
||||
using TweetLib.Core.Application;
|
||||
|
||||
namespace TweetDuck.Resources{
|
||||
static class ScriptLoader{
|
||||
private static readonly Dictionary<string, string> CachedData = new Dictionary<string, string>(16);
|
||||
class ScriptLoader : IAppResourceHandler{
|
||||
private readonly Dictionary<string, string> cache = new Dictionary<string, string>(16);
|
||||
private Control sync;
|
||||
|
||||
public static string LoadResourceSilent(string name){
|
||||
return LoadResource(name, null);
|
||||
public void Initialize(Control sync){
|
||||
this.sync = sync;
|
||||
}
|
||||
|
||||
public static string LoadResource(string name, Control sync){
|
||||
if (CachedData.TryGetValue(name, out string resourceData)){
|
||||
protected void ClearCache(){
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
public virtual void OnReloadTriggered(){
|
||||
if (Control.ModifierKeys.HasFlag(Keys.Shift)){
|
||||
ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
public string Load(string path) => LoadInternal(path, silent: false);
|
||||
public string LoadSilent(string path) => LoadInternal(path, silent: true);
|
||||
|
||||
protected virtual string LocateFile(string path){
|
||||
return Path.Combine(Program.ScriptPath, path);
|
||||
}
|
||||
|
||||
private string LoadInternal(string path, bool silent){
|
||||
if (sync == null){
|
||||
throw new InvalidOperationException("Cannot use ScriptLoader before initialization.");
|
||||
}
|
||||
else if (sync.IsDisposed){
|
||||
return null; // better than crashing I guess...?
|
||||
}
|
||||
|
||||
if (cache.TryGetValue(path, out string resourceData)){
|
||||
return resourceData;
|
||||
}
|
||||
|
||||
string path = Program.ScriptPath;
|
||||
|
||||
#if DEBUG
|
||||
if (Directory.Exists(HotSwapTargetDir)){
|
||||
path = Path.Combine(HotSwapTargetDir, "scripts");
|
||||
Debug.WriteLine("Hot swap active, redirecting "+name);
|
||||
}
|
||||
#endif
|
||||
|
||||
string location = LocateFile(path);
|
||||
string resource;
|
||||
|
||||
try{
|
||||
string contents = File.ReadAllText(Path.Combine(path, name), Encoding.UTF8);
|
||||
string contents = File.ReadAllText(location, Encoding.UTF8);
|
||||
int separator;
|
||||
|
||||
// first line can be either:
|
||||
@@ -47,7 +57,7 @@ namespace TweetDuck.Resources{
|
||||
// #<version>\n
|
||||
|
||||
if (contents[0] != '#'){
|
||||
ShowLoadError(sync, $"File {name} appears to be corrupted, please try reinstalling the app.");
|
||||
ShowLoadError(silent ? null : sync, $"File {path} appears to be corrupted, please try reinstalling the app.");
|
||||
separator = 0;
|
||||
}
|
||||
else{
|
||||
@@ -55,112 +65,21 @@ namespace TweetDuck.Resources{
|
||||
string fileVersion = contents.Substring(1, separator - 1).TrimEnd();
|
||||
|
||||
if (fileVersion != Program.VersionTag){
|
||||
ShowLoadError(sync, $"File {name} is made for a different version of TweetDuck ({fileVersion}) and may not function correctly in this version, please try reinstalling the app.");
|
||||
ShowLoadError(silent ? null : sync, $"File {path} is made for a different version of TweetDuck ({fileVersion}) and may not function correctly in this version, please try reinstalling the app.");
|
||||
}
|
||||
}
|
||||
|
||||
resource = contents.Substring(separator).TrimStart();
|
||||
}catch(Exception ex){
|
||||
ShowLoadError(sync, $"Could not load {name}. The program will continue running with limited functionality.\n\n{ex.Message}");
|
||||
ShowLoadError(silent ? null : sync, $"Could not load {path}. The program will continue running with limited functionality.\n\n{ex.Message}");
|
||||
resource = null;
|
||||
}
|
||||
|
||||
return CachedData[name] = resource;
|
||||
}
|
||||
|
||||
public static bool ExecuteFile(IFrame frame, string file, Control sync){
|
||||
string script = LoadResource(file, sync);
|
||||
ExecuteScript(frame, script, "root:"+Path.GetFileNameWithoutExtension(file));
|
||||
return script != null;
|
||||
}
|
||||
|
||||
public static void ExecuteScript(IFrame frame, string script, string identifier){
|
||||
if (script != null){
|
||||
frame.ExecuteJavaScriptAsync(script, identifier, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearCache(){
|
||||
CachedData.Clear();
|
||||
return cache[path] = resource;
|
||||
}
|
||||
|
||||
private static void ShowLoadError(Control sync, string message){
|
||||
sync?.InvokeSafe(() => FormMessage.Error("Resource Error", message, FormMessage.OK));
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../")));
|
||||
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
|
||||
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe");
|
||||
|
||||
static ScriptLoader(){
|
||||
if (File.Exists(HotSwapRebuildScript)){
|
||||
Debug.WriteLine("Activating resource hot swap...");
|
||||
|
||||
ResetHotSwap();
|
||||
Application.ApplicationExit += (sender, args) => ResetHotSwap();
|
||||
}
|
||||
}
|
||||
|
||||
public static void HotSwap(){
|
||||
if (!File.Exists(HotSwapRebuildScript)){
|
||||
Debug.WriteLine("Failed resource hot swap, missing rebuild script: "+HotSwapRebuildScript);
|
||||
return;
|
||||
}
|
||||
|
||||
ResetHotSwap();
|
||||
Directory.CreateDirectory(HotSwapTargetDir);
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
using(Process process = Process.Start(new ProcessStartInfo{
|
||||
FileName = HotSwapRebuildScript,
|
||||
Arguments = $"\"{HotSwapTargetDir}\\\" \"{HotSwapProjectRoot}\\\" \"Debug\" \"{Program.VersionTag}\"",
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
})){
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
if (!process.WaitForExit(8000)){
|
||||
Debug.WriteLine("Failed resource hot swap, script did not finish in time");
|
||||
return;
|
||||
}
|
||||
else if (process.ExitCode != 0){
|
||||
Debug.WriteLine("Failed resource hot swap, script exited with code "+process.ExitCode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Debug.WriteLine("Finished rebuild script in "+sw.ElapsedMilliseconds+" ms");
|
||||
|
||||
ClearCache();
|
||||
|
||||
// Force update plugin manager setup scripts
|
||||
|
||||
string newPluginRoot = Path.Combine(HotSwapTargetDir, "plugins");
|
||||
|
||||
const BindingFlags flagsInstance = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
|
||||
Type typePluginManager = typeof(PluginManager);
|
||||
Type typeFormBrowser = typeof(FormBrowser);
|
||||
|
||||
// ReSharper disable PossibleNullReferenceException
|
||||
object instPluginManager = typeFormBrowser.GetField("plugins", flagsInstance).GetValue(FormManager.TryFind<FormBrowser>());
|
||||
typePluginManager.GetField("rootPath", 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
|
||||
}
|
||||
}
|
||||
|
100
Resources/ScriptLoaderDebug.cs
Normal file
100
Resources/ScriptLoaderDebug.cs
Normal 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
|
@@ -1298,6 +1298,31 @@
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Fix DM image previews and GIF thumbnails not loading due to new URLs.
|
||||
//
|
||||
if (ensurePropertyExists(TD, "services", "TwitterMedia", "prototype", "getTwitterPreviewUrl")){
|
||||
const prevFunc = TD.services.TwitterMedia.prototype.getTwitterPreviewUrl;
|
||||
|
||||
TD.services.TwitterMedia.prototype.getTwitterPreviewUrl = function(){
|
||||
const url = prevFunc.apply(this, arguments);
|
||||
|
||||
if (url.startsWith("https://ton.twitter.com/1.1/ton/data/dm/") || url.startsWith("https://pbs.twimg.com/tweet_video_thumb/")){
|
||||
const format = url.match(/\?.*format=(\w+)/);
|
||||
|
||||
if (format && format.length === 2){
|
||||
const fix = `.${format[1]}?`;
|
||||
|
||||
if (!url.includes(fix)){
|
||||
return url.replace("?", fix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Block: Fix youtu.be previews not showing up for https links.
|
||||
//
|
||||
@@ -1432,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.
|
||||
//
|
||||
@@ -1592,7 +1635,7 @@
|
||||
}
|
||||
|
||||
//
|
||||
// Block: Fix broken horizontal scrolling of column container when holding Shift. TODO Fix broken smooth scrolling.
|
||||
// Block: Fix broken horizontal scrolling of column container when holding Shift.
|
||||
//
|
||||
if (ensurePropertyExists(TD, "ui", "columns", "setupColumnScrollListeners")){
|
||||
TD.ui.columns.setupColumnScrollListeners = appendToFunction(TD.ui.columns.setupColumnScrollListeners, function(column){
|
||||
@@ -1600,9 +1643,7 @@
|
||||
return if !ele.length;
|
||||
|
||||
ele.off("onmousewheel").on("mousewheel", ".scroll-v", function(e){
|
||||
if (e.shiftKey){
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
|
||||
window.TDGF_prioritizeNewestEvent(ele[0], "mousewheel");
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
@@ -92,7 +92,6 @@
|
||||
});
|
||||
|
||||
onClick(ele.querySelector(".tdu-btn-later"), function(){
|
||||
$TDU.onUpdateDelayed();
|
||||
exitSlide();
|
||||
});
|
||||
|
||||
|
@@ -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,24 +237,26 @@
|
||||
<Compile Include="Core\Other\FormSettings.Designer.cs">
|
||||
<DependentUpon>FormSettings.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Plugins\Controls\PluginControl.cs">
|
||||
<Compile Include="Impl\LockHandler.cs" />
|
||||
<Compile Include="Impl\SystemHandler.cs" />
|
||||
<Compile Include="Plugins\PluginControl.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Plugins\Controls\PluginControl.Designer.cs">
|
||||
<Compile Include="Plugins\PluginControl.Designer.cs">
|
||||
<DependentUpon>PluginControl.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Plugins\Controls\PluginListFlowLayout.cs">
|
||||
<Compile Include="Plugins\PluginDispatcher.cs" />
|
||||
<Compile Include="Core\Controls\FlowLayoutPanelNoHScroll.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Plugins\PluginBridge.cs" />
|
||||
<Compile Include="Configuration\PluginConfig.cs" />
|
||||
<Compile Include="Plugins\PluginManager.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<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>
|
||||
@@ -385,7 +386,7 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
|
||||
</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<Target Name="BeforeBuild" Condition="(!$([System.IO.File]::Exists("$(ProjectDir)\bld\post_build.exe")) OR ($([System.IO.File]::GetLastWriteTime("$(ProjectDir)\Resources\PostBuild.fsx").Ticks) > $([System.IO.File]::GetLastWriteTime("$(ProjectDir)\bld\post_build.exe").Ticks)))">
|
||||
<Exec Command=""$(ProjectDir)bld\POST BUILD.bat"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
|
||||
<Exec Command=""$(ProjectDir)bld\POST BUILD.bat" "$(DevEnvDir)CommonExtensions\Microsoft\FSharp\fsc.exe"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
|
||||
</Target>
|
||||
<Target Name="AfterBuild" Condition="$(ConfigurationName) == Release">
|
||||
<Exec Command="del "$(TargetDir)*.pdb"" />
|
||||
@@ -395,7 +396,7 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
|
||||
<Exec Command="start "" /B "ISCC.exe" /Q "$(ProjectDir)bld\gen_upd.iss"" 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>
|
||||
|
@@ -12,7 +12,7 @@ namespace TweetDuck.Updates{
|
||||
this.updateInfo = info;
|
||||
|
||||
Text = "Updating " + Program.BrandName;
|
||||
labelDescription.Text = "Downloading version "+info.VersionTag+"...";
|
||||
labelDescription.Text = $"Downloading version {info.VersionTag}...";
|
||||
timerDownloadCheck.Start();
|
||||
}
|
||||
|
||||
|
@@ -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"];
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,12 @@
|
||||
@ECHO OFF
|
||||
|
||||
IF EXIST "post_build.exe" (
|
||||
DEL "post_build.exe"
|
||||
|
||||
SET fsc="%PROGRAMFILES(x86)%\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe"
|
||||
|
||||
IF NOT EXIST %fsc% (
|
||||
SET fsc="%PROGRAMFILES%\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe"
|
||||
)
|
||||
|
||||
IF NOT EXIST %fsc% (
|
||||
IF NOT EXIST %1 (
|
||||
ECHO fsc.exe not found
|
||||
EXIT 1
|
||||
)
|
||||
|
||||
%fsc% --standalone --deterministic --preferreduilang:en-US --platform:x86 --target:exe --out:post_build.exe "%~dp0..\Resources\PostBuild.fsx"
|
||||
%1 --standalone --deterministic --preferreduilang:en-US --platform:x86 --target:exe --out:post_build.exe "%~dp0..\Resources\PostBuild.fsx"
|
||||
|
@@ -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" />
|
||||
|
@@ -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){
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
8
lib/TweetLib.Core/Application/IAppLockHandler.cs
Normal file
8
lib/TweetLib.Core/Application/IAppLockHandler.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TweetLib.Core.Application{
|
||||
public interface IAppLockHandler{
|
||||
bool RestoreProcess(Process process);
|
||||
bool CloseProcess(Process process);
|
||||
}
|
||||
}
|
5
lib/TweetLib.Core/Application/IAppResourceHandler.cs
Normal file
5
lib/TweetLib.Core/Application/IAppResourceHandler.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace TweetLib.Core.Application{
|
||||
public interface IAppResourceHandler{
|
||||
string? Load(string path);
|
||||
}
|
||||
}
|
5
lib/TweetLib.Core/Application/IAppSystemHandler.cs
Normal file
5
lib/TweetLib.Core/Application/IAppSystemHandler.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace TweetLib.Core.Application{
|
||||
public interface IAppSystemHandler{
|
||||
void OpenFileExplorer(string path);
|
||||
}
|
||||
}
|
7
lib/TweetLib.Core/Browser/IScriptExecutor.cs
Normal file
7
lib/TweetLib.Core/Browser/IScriptExecutor.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -1,15 +1,9 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Core.Bridge;
|
||||
using TweetDuck.Data;
|
||||
using TweetDuck.Resources;
|
||||
|
||||
namespace TweetDuck.Core.Notification{
|
||||
sealed class TweetNotification{
|
||||
namespace TweetLib.Core.Features.Notifications{
|
||||
public sealed class DesktopNotification{
|
||||
private const string DefaultHeadLayout = @"<html class=""scroll-v os-windows dark txt-size--14"" lang=""en-US"" id=""tduck"" data-td-font=""medium"" data-td-theme=""dark""><head><meta charset=""utf-8""><link href=""https://ton.twimg.com/tweetdeck-web/web/dist/bundle.4b1f87e09d.css"" rel=""stylesheet""><style type='text/css'>body { background: rgb(34, 36, 38) !important }</style>";
|
||||
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandler.FromByteArray(Properties.Resources.avatar, "image/png"));
|
||||
|
||||
public enum Position{
|
||||
TopLeft, TopRight, BottomLeft, BottomRight, Custom
|
||||
@@ -29,7 +23,7 @@ namespace TweetDuck.Core.Notification{
|
||||
private readonly string html;
|
||||
private readonly int characters;
|
||||
|
||||
public TweetNotification(string columnId, string chirpId, string title, string html, int characters, string tweetUrl, string quoteUrl){
|
||||
public DesktopNotification(string columnId, string chirpId, string title, string html, int characters, string tweetUrl, string quoteUrl){
|
||||
this.ColumnId = columnId;
|
||||
this.ChirpId = chirpId;
|
||||
|
||||
@@ -45,18 +39,19 @@ namespace TweetDuck.Core.Notification{
|
||||
return 2000 + Math.Max(1000, value * characters);
|
||||
}
|
||||
|
||||
public string GenerateHtml(string bodyClasses, Control sync){
|
||||
string headLayout = TweetDeckBridge.NotificationHeadLayout ?? DefaultHeadLayout;
|
||||
string mainCSS = ScriptLoader.LoadResource("styles/notification.css", sync) ?? string.Empty;
|
||||
string customCSS = Program.Config.User.CustomNotificationCSS ?? string.Empty;
|
||||
public string GenerateHtml(string bodyClasses, string? headLayout, string? customStyles){ // TODO
|
||||
headLayout ??= DefaultHeadLayout;
|
||||
customStyles ??= string.Empty;
|
||||
|
||||
StringBuilder build = new StringBuilder(320 + headLayout.Length + mainCSS.Length + customCSS.Length + html.Length);
|
||||
string mainCSS = App.ResourceHandler.Load("styles/notification.css") ?? string.Empty;
|
||||
|
||||
StringBuilder build = new StringBuilder(320 + headLayout.Length + mainCSS.Length + customStyles.Length + html.Length);
|
||||
build.Append("<!DOCTYPE html>");
|
||||
build.Append(headLayout);
|
||||
build.Append("<style type='text/css'>").Append(mainCSS).Append("</style>");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customCSS)){
|
||||
build.Append("<style type='text/css'>").Append(customCSS).Append("</style>");
|
||||
if (!string.IsNullOrWhiteSpace(customStyles)){
|
||||
build.Append("<style type='text/css'>").Append(customStyles).Append("</style>");
|
||||
}
|
||||
|
||||
build.Append("</head><body class='scroll-styled-v");
|
@@ -1,88 +1,32 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
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{
|
||||
yield return PluginEnvironment.Browser;
|
||||
yield return PluginEnvironment.Notification;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
|
||||
return environment == PluginEnvironment.Browser;
|
||||
}
|
||||
public static class PluginEnvironments{
|
||||
public static IEnumerable<PluginEnvironment> All { get; } = new PluginEnvironment[]{
|
||||
PluginEnvironment.Browser,
|
||||
PluginEnvironment.Notification
|
||||
};
|
||||
|
||||
public static string? GetPluginScriptFile(this PluginEnvironment environment){
|
||||
switch(environment){
|
||||
case PluginEnvironment.Browser: return "browser.js";
|
||||
case PluginEnvironment.Notification: return "notification.js";
|
||||
default: return null;
|
||||
}
|
||||
return environment switch{
|
||||
PluginEnvironment.Browser => "browser.js",
|
||||
PluginEnvironment.Notification => "notification.js",
|
||||
_ => throw new InvalidOperationException($"Invalid plugin environment: {environment}")
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetPluginScriptVariables(this PluginEnvironment environment){
|
||||
switch(environment){
|
||||
case PluginEnvironment.Browser: return "$,$TD,$TDP,TD";
|
||||
case PluginEnvironment.Notification: return "$TD,$TDP";
|
||||
default: return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<PluginEnvironment, T> Map<T>(T forNone, T forBrowser, T forNotification){
|
||||
return new PluginEnvironmentDictionary<T>(forNone, forBrowser, forNotification);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")]
|
||||
private sealed class PluginEnvironmentDictionary<T> : IReadOnlyDictionary<PluginEnvironment, T>{
|
||||
private const int TotalKeys = 3;
|
||||
|
||||
public IEnumerable<PluginEnvironment> Keys => Enum.GetValues(typeof(PluginEnvironment)).Cast<PluginEnvironment>();
|
||||
public IEnumerable<T> Values => data;
|
||||
public int Count => TotalKeys;
|
||||
|
||||
public T this[PluginEnvironment key] => data[(int)key];
|
||||
|
||||
private readonly T[] data;
|
||||
|
||||
public PluginEnvironmentDictionary(T forNone, T forBrowser, T forNotification){
|
||||
this.data = new T[TotalKeys];
|
||||
this.data[(int)PluginEnvironment.None] = forNone;
|
||||
this.data[(int)PluginEnvironment.Browser] = forBrowser;
|
||||
this.data[(int)PluginEnvironment.Notification] = forNotification;
|
||||
}
|
||||
|
||||
public bool ContainsKey(PluginEnvironment key){
|
||||
return key >= 0 && (int)key < TotalKeys;
|
||||
}
|
||||
|
||||
public bool TryGetValue(PluginEnvironment key, out T value){
|
||||
if (ContainsKey(key)){
|
||||
value = this[key];
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<PluginEnvironment, T>> GetEnumerator(){
|
||||
return Keys.Select(key => new KeyValuePair<PluginEnvironment, T>(key, this[key])).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
return environment switch{
|
||||
PluginEnvironment.Browser => "$,$TD,$TDP,TD",
|
||||
PluginEnvironment.Notification => "$TD,$TDP",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,23 +1,39 @@
|
||||
namespace TweetLib.Core.Features.Plugins.Enums{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins.Enums{
|
||||
public enum PluginGroup{
|
||||
Official, Custom
|
||||
}
|
||||
|
||||
public static class PluginGroupExtensions{
|
||||
public static string GetIdentifierPrefix(this PluginGroup group){
|
||||
switch(group){
|
||||
case PluginGroup.Official: return "official/";
|
||||
case PluginGroup.Custom: return "custom/";
|
||||
default: return "unknown/";
|
||||
public static class PluginGroups{
|
||||
public static IEnumerable<PluginGroup> All { get; } = new PluginGroup[]{
|
||||
PluginGroup.Official,
|
||||
PluginGroup.Custom
|
||||
};
|
||||
|
||||
public static string GetSubFolder(this PluginGroup group){
|
||||
return group switch{
|
||||
PluginGroup.Official => "official",
|
||||
PluginGroup.Custom => "user",
|
||||
_ => throw new InvalidOperationException($"Invalid plugin group: {group}")
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetIdentifierPrefix(this PluginGroup group){
|
||||
return group switch{
|
||||
PluginGroup.Official => "official/",
|
||||
PluginGroup.Custom => "custom/",
|
||||
_ => "unknown/"
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetIdentifierPrefixShort(this PluginGroup group){
|
||||
switch(group){
|
||||
case PluginGroup.Official: return "o/";
|
||||
case PluginGroup.Custom: return "c/";
|
||||
default: return "?/";
|
||||
}
|
||||
return group switch{
|
||||
PluginGroup.Official => "o/",
|
||||
PluginGroup.Custom => "c/",
|
||||
_ => "?/"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
9
lib/TweetLib.Core/Features/Plugins/IPluginDispatcher.cs
Normal file
9
lib/TweetLib.Core/Features/Plugins/IPluginDispatcher.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
@@ -71,11 +77,11 @@ namespace TweetLib.Core.Features.Plugins{
|
||||
}
|
||||
|
||||
public string GetPluginFolder(PluginFolder folder){
|
||||
switch(folder){
|
||||
case PluginFolder.Root: return pathRoot;
|
||||
case PluginFolder.Data: return pathData;
|
||||
default: return string.Empty;
|
||||
}
|
||||
return folder switch{
|
||||
PluginFolder.Root => pathRoot,
|
||||
PluginFolder.Data => pathData,
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public string GetFullPathIfSafe(PluginFolder folder, string relativePath){
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
@@ -5,40 +5,62 @@ using System.IO;
|
||||
using System.Text;
|
||||
using TweetLib.Core.Collections;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Plugins{
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
sealed class PluginBridge{
|
||||
private static string SanitizeCacheKey(string key){
|
||||
return key.Replace('\\', '/').Trim();
|
||||
}
|
||||
internal sealed class PluginBridge{
|
||||
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
|
||||
private readonly Random rand = new Random();
|
||||
|
||||
private readonly PluginManager manager;
|
||||
private readonly TwoKeyDictionary<int, string, string> fileCache = new TwoKeyDictionary<int, string, string>(4, 2);
|
||||
private readonly FileCache fileCache = new FileCache();
|
||||
private readonly TwoKeyDictionary<int, string, InjectedHTML> notificationInjections = new TwoKeyDictionary<int, string, InjectedHTML>(4, 1);
|
||||
|
||||
public IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
|
||||
public HashSet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
|
||||
internal IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
|
||||
internal ISet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
|
||||
|
||||
public PluginBridge(PluginManager manager){
|
||||
this.manager = manager;
|
||||
this.manager.Reloaded += manager_Reloaded;
|
||||
this.manager.Config.PluginChangedState += Config_PluginChangedState;
|
||||
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);
|
||||
@@ -48,14 +70,14 @@ namespace TweetDuck.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){
|
||||
switch(folder){
|
||||
case PluginFolder.Data: throw new ArgumentException("File path has to be relative to the plugin data folder.");
|
||||
case PluginFolder.Root: throw new ArgumentException("File path has to be relative to the plugin root folder.");
|
||||
default: throw new ArgumentException("Invalid folder type "+folder+", this is a TweetDuck error.");
|
||||
default: throw new ArgumentException($"Invalid folder type {folder}, this is a TweetDuck error.");
|
||||
}
|
||||
}
|
||||
else{
|
||||
@@ -63,15 +85,15 @@ namespace TweetDuck.Plugins{
|
||||
}
|
||||
}
|
||||
|
||||
private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool readCached){
|
||||
cacheKey = SanitizeCacheKey(cacheKey);
|
||||
private string ReadFileUnsafe(int token, PluginFolder folder, string path, bool readCached){
|
||||
string fullPath = GetFullPathOrThrow(token, folder, path);
|
||||
|
||||
if (readCached && fileCache.TryGetValue(token, cacheKey, out string cachedContents)){
|
||||
if (readCached && fileCache.TryGetValue(token, folder, path, out string cachedContents)){
|
||||
return cachedContents;
|
||||
}
|
||||
|
||||
try{
|
||||
return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
|
||||
return fileCache[token, folder, path] = File.ReadAllText(fullPath, Encoding.UTF8);
|
||||
}catch(FileNotFoundException){
|
||||
throw new FileNotFoundException("File not found.");
|
||||
}catch(DirectoryNotFoundException){
|
||||
@@ -86,17 +108,17 @@ namespace TweetDuck.Plugins{
|
||||
|
||||
FileUtils.CreateDirectoryForFile(fullPath);
|
||||
File.WriteAllText(fullPath, contents, Encoding.UTF8);
|
||||
fileCache[token, SanitizeCacheKey(path)] = contents;
|
||||
fileCache[token, PluginFolder.Data, path] = contents;
|
||||
}
|
||||
|
||||
public string ReadFile(int token, string path, bool cache){
|
||||
return ReadFileUnsafe(token, path, GetFullPathOrThrow(token, PluginFolder.Data, path), cache);
|
||||
return ReadFileUnsafe(token, PluginFolder.Data, path, cache);
|
||||
}
|
||||
|
||||
public void DeleteFile(int token, string path){
|
||||
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
|
||||
|
||||
fileCache.Remove(token, SanitizeCacheKey(path));
|
||||
fileCache.Remove(token, PluginFolder.Data, path);
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
|
||||
@@ -105,7 +127,7 @@ namespace TweetDuck.Plugins{
|
||||
}
|
||||
|
||||
public string ReadFileRoot(int token, string path){
|
||||
return ReadFileUnsafe(token, "root*"+path, GetFullPathOrThrow(token, PluginFolder.Root, path), true);
|
||||
return ReadFileUnsafe(token, PluginFolder.Root, path, true);
|
||||
}
|
||||
|
||||
public bool CheckFileExistsRoot(int token, string path){
|
||||
@@ -121,11 +143,45 @@ namespace TweetDuck.Plugins{
|
||||
}
|
||||
|
||||
public void SetConfigurable(int token){
|
||||
Plugin plugin = manager.GetPluginFromToken(token);
|
||||
Plugin? plugin = GetPluginFromToken(token);
|
||||
|
||||
if (plugin != null){
|
||||
WithConfigureFunction.Add(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FileCache{
|
||||
private readonly TwoKeyDictionary<int, string, string> cache = new TwoKeyDictionary<int, string, string>(4, 2);
|
||||
|
||||
public string this[int token, PluginFolder folder, string path]{
|
||||
set => cache[token, Key(folder, path)] = value;
|
||||
}
|
||||
|
||||
public void Clear(){
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
public bool TryGetValue(int token, PluginFolder folder, string path, out string contents){
|
||||
return cache.TryGetValue(token, Key(folder, path), out contents);
|
||||
}
|
||||
|
||||
public void Remove(int token){
|
||||
cache.Remove(token);
|
||||
}
|
||||
|
||||
public void Remove(int token, PluginFolder folder, string path){
|
||||
cache.Remove(token, Key(folder, path));
|
||||
}
|
||||
|
||||
private static string Key(PluginFolder folder, string path){
|
||||
string prefix = folder switch{
|
||||
PluginFolder.Root => "root/",
|
||||
PluginFolder.Data => "data/",
|
||||
_ => throw new InvalidOperationException($"Invalid folder type {folder}, this is a TweetDuck error.")
|
||||
};
|
||||
|
||||
return prefix + path.Replace('\\', '/').Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TweetLib.Core.Data;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins{
|
||||
public static class PluginLoader{
|
||||
private static readonly string[] EndTag = { "[END]" };
|
||||
|
||||
public static IEnumerable<Result<Plugin>> AllInFolder(string pluginFolder, string pluginDataFolder, PluginGroup group){
|
||||
string path = Path.Combine(pluginFolder, group.GetSubFolder());
|
||||
|
||||
if (!Directory.Exists(path)){
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
|
||||
string name = Path.GetFileName(fullDir);
|
||||
string prefix = group.GetIdentifierPrefix();
|
||||
|
||||
if (string.IsNullOrEmpty(name)){
|
||||
yield return new Result<Plugin>(new DirectoryNotFoundException($"{prefix}(?): Could not extract directory name from path: {fullDir}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
Result<Plugin> result;
|
||||
|
||||
try{
|
||||
result = new Result<Plugin>(FromFolder(name, fullDir, Path.Combine(pluginDataFolder, prefix, name), group));
|
||||
}catch(Exception e){
|
||||
result = new Result<Plugin>(new Exception($"{prefix}{name}: {e.Message}", e));
|
||||
}
|
||||
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group){
|
||||
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
||||
|
||||
@@ -49,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){
|
||||
|
123
lib/TweetLib.Core/Features/Plugins/PluginManager.cs
Normal file
123
lib/TweetLib.Core/Features/Plugins/PluginManager.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
lib/TweetLib.Core/Features/Twitter/ImageQuality.cs
Normal file
5
lib/TweetLib.Core/Features/Twitter/ImageQuality.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace TweetLib.Core.Features.Twitter{
|
||||
public enum ImageQuality{
|
||||
Default, Best
|
||||
}
|
||||
}
|
88
lib/TweetLib.Core/Features/Twitter/ImageUrl.cs
Normal file
88
lib/TweetLib.Core/Features/Twitter/ImageUrl.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetLib.Core.Features.Twitter{
|
||||
public class ImageUrl{
|
||||
private static readonly Regex RegexImageUrlParams = new Regex(@"(format|name)=(\w+)", RegexOptions.IgnoreCase);
|
||||
|
||||
public static readonly string[] ValidExtensions = {
|
||||
".jpg", ".jpeg", ".png", ".gif"
|
||||
};
|
||||
|
||||
public static bool TryParse(string url, out ImageUrl obj){
|
||||
obj = default!;
|
||||
|
||||
int slash = url.LastIndexOf('/');
|
||||
|
||||
if (slash == -1){
|
||||
return false;
|
||||
}
|
||||
|
||||
int question = url.IndexOf('?', slash);
|
||||
|
||||
if (question == -1){
|
||||
var oldStyleUrl = StringUtils.SplitInTwo(url, ':', slash);
|
||||
|
||||
if (oldStyleUrl.HasValue){
|
||||
var (baseUrl, quality) = oldStyleUrl.Value;
|
||||
|
||||
obj = new ImageUrl(baseUrl, quality);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? imageExtension = null;
|
||||
string? imageQuality = null;
|
||||
|
||||
foreach(Match match in RegexImageUrlParams.Matches(url, question)){
|
||||
string value = match.Groups[2].Value;
|
||||
|
||||
switch(match.Groups[1].Value){
|
||||
case "format":
|
||||
imageExtension = '.' + value;
|
||||
break;
|
||||
|
||||
case "name":
|
||||
imageQuality = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ValidExtensions.Contains(imageExtension) || imageQuality == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
string originalUrl = url.Substring(0, question);
|
||||
|
||||
obj = new ImageUrl(Path.HasExtension(originalUrl) ? originalUrl : originalUrl + imageExtension, imageQuality);
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly string baseUrl;
|
||||
private readonly string quality;
|
||||
|
||||
private ImageUrl(string baseUrl, string quality){
|
||||
this.baseUrl = baseUrl;
|
||||
this.quality = quality;
|
||||
}
|
||||
|
||||
public string WithNoQuality => baseUrl;
|
||||
|
||||
public string WithQuality(ImageQuality newQuality){
|
||||
if (newQuality == ImageQuality.Best){
|
||||
if (baseUrl.Contains("//ton.twitter.com/") && baseUrl.Contains("/ton/data/dm/")){
|
||||
return baseUrl + ":large";
|
||||
}
|
||||
else if (baseUrl.Contains("//pbs.twimg.com/media/")){
|
||||
return baseUrl + ":orig";
|
||||
}
|
||||
}
|
||||
|
||||
return baseUrl + ':' + quality;
|
||||
}
|
||||
}
|
||||
}
|
53
lib/TweetLib.Core/Features/Twitter/TwitterUrls.cs
Normal file
53
lib/TweetLib.Core/Features/Twitter/TwitterUrls.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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";
|
||||
public const string VersionTag = "1.18.2";
|
||||
|
||||
public static CultureInfo Culture { get; private set; }
|
||||
|
||||
|
@@ -6,6 +6,16 @@ namespace TweetLib.Core.Utils{
|
||||
public static class StringUtils{
|
||||
public static readonly string[] EmptyArray = new string[0];
|
||||
|
||||
public static (string before, string after)? SplitInTwo(string str, char search, int startIndex = 0){
|
||||
int index = str.IndexOf(search, startIndex);
|
||||
|
||||
if (index == -1){
|
||||
return null;
|
||||
}
|
||||
|
||||
return (str.Substring(0, index), str.Substring(index + 1));
|
||||
}
|
||||
|
||||
public static string ExtractBefore(string str, char search, int startIndex = 0){
|
||||
int index = str.IndexOf(search, startIndex);
|
||||
return index == -1 ? str : str.Substring(0, index);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,13 +26,10 @@ namespace TweetLib.Core.Utils{
|
||||
public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){
|
||||
return (sender, args) => {
|
||||
if (args.Cancelled){
|
||||
try{
|
||||
File.Delete(file);
|
||||
}catch{
|
||||
// didn't want it deleted anyways
|
||||
}
|
||||
TryDeleteFile(file);
|
||||
}
|
||||
else if (args.Error != null){
|
||||
TryDeleteFile(file);
|
||||
onFailure?.Invoke(args.Error);
|
||||
}
|
||||
else{
|
||||
@@ -40,5 +37,13 @@ namespace TweetLib.Core.Utils{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string file){
|
||||
try{
|
||||
File.Delete(file);
|
||||
}catch{
|
||||
// didn't want it deleted anyways
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
187
lib/TweetTest.Unit/Core/TestTwitterUrls.fs
Normal file
187
lib/TweetTest.Unit/Core/TestTwitterUrls.fs
Normal 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/"))
|
@@ -1,112 +0,0 @@
|
||||
namespace TweetTest.Core.TwitterUtils
|
||||
|
||||
open Xunit
|
||||
open TweetDuck.Core.Utils
|
||||
|
||||
|
||||
[<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, TwitterUtils.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, TwitterUtils.ImageQuality.Orig)
|
||||
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"))
|
@@ -1,79 +0,0 @@
|
||||
namespace TweetTest.Core.UrlUtils
|
||||
|
||||
open Xunit
|
||||
open TweetLib.Core.Utils
|
||||
|
||||
|
||||
module Check =
|
||||
type Result = UrlUtils.CheckResult
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts HTTP protocol`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("http://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts HTTPS protocol`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("https://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts FTP protocol`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("ftp://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts MAILTO protocol`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("mailto://someone@example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts URL with port, path, query, and hash`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts IPv4 address`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("http://127.0.0.1"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts IPv6 address`` () =
|
||||
Assert.Equal(Result.Fine, UrlUtils.Check("http://[2001:db8:0:0:0:ff00:42:8329]"))
|
||||
|
||||
[<Fact>]
|
||||
let ``recognizes t.co as tracking URL`` () =
|
||||
Assert.Equal(Result.Tracking, UrlUtils.Check("http://t.co/12345"))
|
||||
|
||||
[<Fact>]
|
||||
let ``rejects empty URL`` () =
|
||||
Assert.Equal(Result.Invalid, UrlUtils.Check(""))
|
||||
|
||||
[<Fact>]
|
||||
let ``rejects missing protocol`` () =
|
||||
Assert.Equal(Result.Invalid, UrlUtils.Check("www.example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``rejects banned protocol`` () =
|
||||
Assert.Equal(Result.Invalid, UrlUtils.Check("file://example.com"))
|
||||
|
||||
|
||||
module GetFileNameFromUrl =
|
||||
|
||||
[<Fact>]
|
||||
let ``simple file URL returns file name`` () =
|
||||
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html"))
|
||||
|
||||
[<Fact>]
|
||||
let ``file URL with query returns file name`` () =
|
||||
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
|
||||
|
||||
[<Fact>]
|
||||
let ``file URL w/o extension returns file name`` () =
|
||||
Assert.Equal("index", UrlUtils.GetFileNameFromUrl("http://example.com/index"))
|
||||
|
||||
[<Fact>]
|
||||
let ``file URL with trailing dot returns file name with dot`` () =
|
||||
Assert.Equal("index.", UrlUtils.GetFileNameFromUrl("http://example.com/index."))
|
||||
|
||||
[<Fact>]
|
||||
let ``root URL returns null`` () =
|
||||
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``path URL returns null`` () =
|
||||
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com/path/"))
|
@@ -51,8 +51,7 @@
|
||||
<Import Project="$(FSharpTargetsPath)" />
|
||||
<ItemGroup>
|
||||
<Compile Include="Core\TestStringUtils.fs" />
|
||||
<Compile Include="Core\TestTwitterUtils.fs" />
|
||||
<Compile Include="Core\TestUrlUtils.fs" />
|
||||
<Compile Include="Core\TestTwitterUrls.fs" />
|
||||
<Compile Include="Data\TestCommandLineArgs.fs" />
|
||||
<Compile Include="Data\TestInjectedHTML.fs" />
|
||||
<Compile Include="Data\TestResult.fs" />
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
@@ -10,6 +10,7 @@
|
||||
<RootNamespace>TweetDuck.Browser</RootNamespace>
|
||||
<AssemblyName>TweetDuck.Browser</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<NuGetPackageImportStamp>
|
||||
@@ -18,12 +19,10 @@
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<LangVersion>7</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
|
@@ -98,7 +98,7 @@ namespace TweetDuck.Video{
|
||||
bool needsUpdate = !timerSync.Enabled || (useCompactLayout ? tablePanelFull.Enabled : tablePanelCompactBottom.Enabled);
|
||||
|
||||
if (needsUpdate){
|
||||
void Disable(TableLayoutPanel panel){
|
||||
static void Disable(TableLayoutPanel panel){
|
||||
panel.Controls.Clear();
|
||||
panel.Visible = false;
|
||||
panel.Enabled = false;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
@@ -10,6 +10,7 @@
|
||||
<RootNamespace>TweetDuck.Video</RootNamespace>
|
||||
<AssemblyName>TweetDuck.Video</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
|
||||
@@ -25,7 +26,6 @@
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
@@ -37,7 +37,6 @@
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
|
||||
<LangVersion>7</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
|
||||
|
Reference in New Issue
Block a user