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

Compare commits

...

27 Commits

Author SHA1 Message Date
daa0780644 Release 1.19.0.1 2020-06-09 20:27:29 +02:00
8502f9105f Add sound notification file size warning as it's now loaded into memory 2020-06-08 23:33:43 +02:00
16ced3d827 Fix resource handlers reuse & broken notification sound 2020-06-08 23:33:43 +02:00
1c16187346 Release 1.19 2020-06-08 16:58:58 +02:00
2fe058d9cb Fix crash if reloading plugins reports errors before the main window appears 2020-06-08 09:03:22 +02:00
cefdadd53a Update installer to remove native_blob.bin 2020-06-07 18:01:40 +02:00
c21c10df63 Add a way to exclude <link> tags from being auto-added to notifications 2020-06-07 15:45:55 +02:00
b4d359d30c Add TDPF_createStorage as a plugin replacement for localStorage 2020-06-07 11:33:58 +02:00
1e8c62ac25 Add plugin object validation to TDPF functions 2020-06-07 10:28:55 +02:00
c578f36644 Block CSP reports 2020-06-06 10:00:25 +02:00
c973d0cff4 Fix missing resources from csproj & reorganize it 2020-06-06 09:10:54 +02:00
0c3d9ae46a Refactor main JS code & split code.js into multiple files 2020-06-06 09:01:50 +02:00
a834e8b6a2 Fix broken post-build script import handling 2020-06-06 07:36:03 +02:00
9f5580d983 Eliminate a few post-launch frames of misaligned TweetDeck load icon 2020-06-06 07:36:03 +02:00
e94e3cecf8 Let JS continue even if jQuery or bridge objects are missing 2020-06-06 07:36:02 +02:00
1991f7f50f Bypass 'tdp://' CORS without AddCrossOriginWhitelistEntry 2020-06-06 07:36:02 +02:00
9eb4e623e7 Work around broken smooth scrolling in notifications 2020-06-06 07:36:02 +02:00
ad28d2279f Fix runtime errors & minor tweaks after updating CefSharp 2020-06-05 06:20:19 +02:00
1e3de31fc3 Fix compile errors after updating CefSharp 2020-06-05 06:20:19 +02:00
f85bd41b96 Update CefSharp to 81 2020-06-05 06:20:19 +02:00
563124b68c Fix ResourceHandlerNotification buffer being unnecessarily large 2020-06-05 06:19:50 +02:00
63de08c635 Fix plugin code running in blank notifications from previous commit 2020-06-04 04:33:50 +02:00
8a0a215443 Reduce notification code hackery 2020-06-03 00:57:24 +02:00
f1b7cd633e Refactor WindowsUtils.OpenAssociatedProgram & update installer code 2020-06-02 23:56:03 +02:00
458eeeccda Add tdp:// scheme for plugins (with 'root/' to access root files) 2020-06-02 12:31:34 +02:00
464e758b94 Ensure window.jQuery is available alongside window.$ 2020-06-02 06:45:51 +02:00
4c61047e9b Fix large notification HTML overflowing CEF buffer and silently crashing 2020-06-02 04:05:29 +02:00
54 changed files with 2455 additions and 1932 deletions

View File

@@ -1,9 +1,21 @@
using System.Diagnostics; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using TweetLib.Core.Application; using TweetLib.Core.Application;
namespace TweetDuck.Application{ namespace TweetDuck.Application{
class SystemHandler : IAppSystemHandler{ class SystemHandler : IAppSystemHandler{
void IAppSystemHandler.OpenAssociatedProgram(string path){
try{
using(Process.Start(new ProcessStartInfo{
FileName = path,
ErrorDialog = true
})){}
}catch(Exception e){
Program.Reporter.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
}
}
void IAppSystemHandler.OpenFileExplorer(string path){ void IAppSystemHandler.OpenFileExplorer(string path){
if (File.Exists(path)){ if (File.Exists(path)){
using(Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")){} using(Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")){}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Concurrent;
using CefSharp;
namespace TweetDuck.Browser.Data{
sealed class ResourceHandlers{
private readonly ConcurrentDictionary<string, Func<IResourceHandler>> handlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase);
public bool HasHandler(IRequest request){
return handlers.ContainsKey(request.Url);
}
public IResourceHandler GetHandler(IRequest request){
return handlers.TryGetValue(request.Url, out var factory) ? factory() : null;
}
public bool Register(string url, Func<IResourceHandler> factory){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
handlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory);
return true;
}
return false;
}
public bool Register(ResourceLink link){
return Register(link.Url, link.Factory);
}
public bool Unregister(string url){
return handlers.TryRemove(url, out _);
}
public bool Unregister(ResourceLink link){
return Unregister(link.Url);
}
public static Func<IResourceHandler> ForString(string str){
return () => ResourceHandler.FromString(str);
}
public static Func<IResourceHandler> ForString(string str, string mimeType){
return () => ResourceHandler.FromString(str, mimeType: mimeType);
}
public static Func<IResourceHandler> ForBytes(byte[] bytes, string mimeType){
return () => ResourceHandler.FromByteArray(bytes, mimeType);
}
}
}

View File

@@ -1,13 +1,14 @@
using CefSharp; using System;
using CefSharp;
namespace TweetDuck.Browser.Data{ namespace TweetDuck.Browser.Data{
sealed class ResourceLink{ sealed class ResourceLink{
public string Url { get; } public string Url { get; }
public IResourceHandler Handler { get; } public Func<IResourceHandler> Factory { get; }
public ResourceLink(string url, IResourceHandler handler){ public ResourceLink(string url, Func<IResourceHandler> factory){
this.Url = url; this.Url = url;
this.Handler = handler; this.Factory = factory;
} }
} }
} }

View File

@@ -16,6 +16,7 @@ using TweetDuck.Dialogs;
using TweetDuck.Dialogs.Settings; using TweetDuck.Dialogs.Settings;
using TweetDuck.Management; using TweetDuck.Management;
using TweetDuck.Management.Analytics; using TweetDuck.Management.Analytics;
using TweetDuck.Plugins;
using TweetDuck.Updates; using TweetDuck.Updates;
using TweetDuck.Utils; using TweetDuck.Utils;
using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins;
@@ -43,7 +44,7 @@ namespace TweetDuck.Browser{
} }
} }
public string UpdateInstallerPath { get; private set; } public UpdateInstaller UpdateInstaller { get; private set; }
private bool ignoreUpdateCheckError; private bool ignoreUpdateCheckError;
public AnalyticsFile AnalyticsFile => analytics?.File ?? AnalyticsFile.Dummy; public AnalyticsFile AnalyticsFile => analytics?.File ?? AnalyticsFile.Dummy;
@@ -65,7 +66,7 @@ namespace TweetDuck.Browser{
private VideoPlayer videoPlayer; private VideoPlayer videoPlayer;
private AnalyticsManager analytics; private AnalyticsManager analytics;
public FormBrowser(){ public FormBrowser(PluginSchemeFactory pluginScheme){
InitializeComponent(); InitializeComponent();
Text = Program.BrandName; Text = Program.BrandName;
@@ -74,6 +75,7 @@ namespace TweetDuck.Browser{
this.plugins.Reloaded += plugins_Reloaded; this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed; this.plugins.Executed += plugins_Executed;
this.plugins.Reload(); this.plugins.Reload();
pluginScheme.Setup(plugins);
this.notification = new FormNotificationTweet(this, plugins); this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show(); this.notification.Show();
@@ -144,6 +146,8 @@ namespace TweetDuck.Browser{
private void RestoreWindow(){ private void RestoreWindow(){
Config.BrowserWindow.Restore(this, true); Config.BrowserWindow.Restore(this, true);
browser.PrepareSize(ClientSize);
prevState = WindowState; prevState = WindowState;
isLoaded = true; isLoaded = true;
} }
@@ -212,6 +216,7 @@ namespace TweetDuck.Browser{
} }
timerResize.Stop(); timerResize.Stop();
browser.PrepareSize(ClientSize); // needed to pre-size browser control when launched in maximized state
if (Location != ControlExtensions.InvisibleLocation){ if (Location != ControlExtensions.InvisibleLocation){
Config.BrowserWindow.Save(this); Config.BrowserWindow.Save(this);
@@ -231,7 +236,7 @@ namespace TweetDuck.Browser{
} }
private void FormBrowser_FormClosed(object sender, FormClosedEventArgs e){ private void FormBrowser_FormClosed(object sender, FormClosedEventArgs e){
if (isLoaded && UpdateInstallerPath == null){ if (isLoaded && UpdateInstaller == null){
updateBridge.Cleanup(); updateBridge.Cleanup();
} }
} }
@@ -258,9 +263,7 @@ namespace TweetDuck.Browser{
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){ private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
if (e.HasErrors){ 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);
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){ if (isLoaded){
@@ -309,7 +312,7 @@ namespace TweetDuck.Browser{
UpdateDownloadStatus status = update.DownloadStatus; UpdateDownloadStatus status = update.DownloadStatus;
if (status == UpdateDownloadStatus.Done){ if (status == UpdateDownloadStatus.Done){
UpdateInstallerPath = update.InstallerPath; UpdateInstaller = new UpdateInstaller(update.InstallerPath);
ForceClose(); ForceClose();
} }
else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: " + (update.DownloadError?.Message ?? "unknown error") + "\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){ else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: " + (update.DownloadError?.Message ?? "unknown error") + "\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){

View File

@@ -30,6 +30,6 @@ namespace TweetDuck.Browser.Handling{
return false; return false;
} }
public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IList<DraggableRegion> regions){} public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions){}
} }
} }

View File

@@ -82,7 +82,8 @@ namespace TweetDuck.Browser.Handling.General{
return true; return true;
} }
bool IJsDialogHandler.OnJSBeforeUnload(IWebBrowser browserControl, IBrowser browser, string message, bool isReload, IJsDialogCallback callback){ bool IJsDialogHandler.OnBeforeUnloadDialog(IWebBrowser browserControl, IBrowser browser, string messageText, bool isReload, IJsDialogCallback callback){
callback.Dispose();
return false; return false;
} }

View File

@@ -33,7 +33,7 @@ namespace TweetDuck.Browser.Handling{
} }
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){ bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){
if (type == KeyType.RawKeyDown && !browser.FocusedFrame.Url.StartsWith("chrome-devtools://")){ if (type == KeyType.RawKeyDown && !browser.FocusedFrame.Url.StartsWith("devtools://")){
return HandleRawKey(browserControl, browser, (Keys)windowsKeyCode, modifiers); return HandleRawKey(browserControl, browser, (Keys)windowsKeyCode, modifiers);
} }

View File

@@ -1,79 +1,20 @@
using System; using CefSharp;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using CefSharp;
using CefSharp.Handler; using CefSharp.Handler;
using TweetDuck.Browser.Handling.General; using TweetDuck.Browser.Handling.General;
using TweetDuck.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Handling{ namespace TweetDuck.Browser.Handling{
class RequestHandlerBase : DefaultRequestHandler{ class RequestHandlerBase : RequestHandler{
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){
if (string.IsNullOrEmpty(rules)){
return;
}
TweetDeckHashes.Clear();
foreach(string rule in rules.Replace(" ", "").ToLower().Split(',')){
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);
}
else{
throw new ArgumentException("Invalid hash characters: " + rule);
}
}
}
private readonly bool autoReload; private readonly bool autoReload;
public RequestHandlerBase(bool autoReload){ public RequestHandlerBase(bool autoReload){
this.autoReload = autoReload; this.autoReload = autoReload;
} }
public override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture){ protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture){
return LifeSpanHandler.HandleLinkClick(browserControl, targetDisposition, targetUrl); return LifeSpanHandler.HandleLinkClick(browserControl, targetDisposition, targetUrl);
} }
public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){
if (BrowserUtils.HasDevTools){
NameValueCollection headers = request.Headers;
headers.Remove("x-devtools-emulate-network-conditions-client-id");
request.Headers = headers;
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
public override bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response){
if ((request.ResourceType == ResourceType.Script || request.ResourceType == ResourceType.Stylesheet) && TweetDeckHashes.Count > 0){
string url = request.Url;
Match match = TweetDeckResourceUrl.Match(url);
if (match.Success && TweetDeckHashes.TryGetValue($"{match.Groups[1]}.{match.Groups[3]}", out string hash)){
if (match.Groups[2].Value == hash){
Program.Reporter.LogVerbose("[RequestHandlerBase] Accepting " + url);
}
else{
Program.Reporter.LogVerbose("[RequestHandlerBase] Replacing " + url + " hash with " + hash);
request.Url = TweetDeckResourceUrl.Replace(url, $"/dist/$1.{hash}.$3");
return true;
}
}
}
return base.OnResourceResponse(browserControl, browser, frame, request, response);
}
public override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status){ protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status){
if (autoReload){ if (autoReload){
browser.Reload(); browser.Reload();
} }

View File

@@ -1,42 +1,13 @@
using System.Collections.Specialized; using CefSharp;
using CefSharp;
using TweetDuck.Browser.Handling.Filters;
using TweetDuck.Utils;
using TweetLib.Core.Features.Twitter; using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Handling{ namespace TweetDuck.Browser.Handling{
sealed class RequestHandlerBrowser : RequestHandlerBase{ sealed class RequestHandlerBrowser : RequestHandlerBase{
private const string UrlVendorResource = "/dist/vendor";
private const string UrlLoadingSpinner = "/backgrounds/spinner_blue";
public string BlockNextUserNavUrl { get; set; } public string BlockNextUserNavUrl { get; set; }
public RequestHandlerBrowser() : base(true){} public RequestHandlerBrowser() : base(true){}
public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){ protected override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect){
if (request.ResourceType == ResourceType.MainFrame){
if (request.Url.EndsWith("//twitter.com/")){
request.Url = TwitterUrls.TweetDeck; // redirect plain twitter.com requests, fixes bugs with login 2FA
}
}
else if (request.ResourceType == ResourceType.Script){
string url = request.Url;
if (url.Contains("analytics.")){
callback.Dispose();
return CefReturnValue.Cancel;
}
else if (url.Contains(UrlVendorResource)){
NameValueCollection headers = request.Headers;
headers["Accept-Encoding"] = "identity";
request.Headers = headers;
}
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
public override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect){
if (userGesture && request.TransitionType == TransitionType.LinkClicked){ if (userGesture && request.TransitionType == TransitionType.LinkClicked){
bool block = request.Url == BlockNextUserNavUrl; bool block = request.Url == BlockNextUserNavUrl;
BlockNextUserNavUrl = string.Empty; BlockNextUserNavUrl = string.Empty;
@@ -48,22 +19,5 @@ namespace TweetDuck.Browser.Handling{
return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect); return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
} }
public override bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response){
if (request.ResourceType == ResourceType.Image && request.Url.Contains(UrlLoadingSpinner)){
request.Url = TwitterUtils.LoadingSpinner.Url;
return true;
}
return base.OnResourceResponse(browserControl, browser, frame, request, response);
}
public override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response){
if (request.ResourceType == ResourceType.Script && request.Url.Contains(UrlVendorResource) && int.TryParse(response.ResponseHeaders["Content-Length"], out int totalBytes)){
return new ResponseFilterVendor(totalBytes);
}
return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
}
} }
} }

View File

@@ -1,43 +0,0 @@
using System;
using System.Collections.Concurrent;
using CefSharp;
using TweetDuck.Browser.Data;
namespace TweetDuck.Browser.Handling{
sealed class ResourceHandlerFactory : IResourceHandlerFactory{
public bool HasHandlers => !handlers.IsEmpty;
private readonly ConcurrentDictionary<string, IResourceHandler> handlers = new ConcurrentDictionary<string, IResourceHandler>(StringComparer.OrdinalIgnoreCase);
public IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request){
try{
return handlers.TryGetValue(request.Url, out IResourceHandler handler) ? handler : null;
}finally{
request.Dispose();
}
}
// registration
public bool RegisterHandler(string url, IResourceHandler handler){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
handlers.AddOrUpdate(uri.AbsoluteUri, handler, (key, prev) => handler);
return true;
}
return false;
}
public bool RegisterHandler(ResourceLink link){
return RegisterHandler(link.Url, link.Handler);
}
public bool UnregisterHandler(string url){
return handlers.TryRemove(url, out IResourceHandler _);
}
public bool UnregisterHandler(ResourceLink link){
return UnregisterHandler(link.Url);
}
}
}

View File

@@ -1,7 +1,9 @@
using System.Collections.Specialized; using System;
using System.Collections.Specialized;
using System.IO; using System.IO;
using System.Text; using System.Text;
using CefSharp; using CefSharp;
using CefSharp.Callback;
namespace TweetDuck.Browser.Handling{ namespace TweetDuck.Browser.Handling{
sealed class ResourceHandlerNotification : IResourceHandler{ sealed class ResourceHandlerNotification : IResourceHandler{
@@ -20,8 +22,14 @@ namespace TweetDuck.Browser.Handling{
} }
} }
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback){ bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback){
callback.Continue(); callback.Dispose();
handleRequest = true;
if (dataIn != null){
dataIn.Position = 0;
}
return true; return true;
} }
@@ -31,31 +39,39 @@ namespace TweetDuck.Browser.Handling{
response.MimeType = "text/html"; response.MimeType = "text/html";
response.StatusCode = 200; response.StatusCode = 200;
response.StatusText = "OK"; response.StatusText = "OK";
response.ResponseHeaders = headers; response.Headers = headers;
responseLength = dataIn?.Length ?? -1; responseLength = dataIn?.Length ?? 0;
}
bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback){
callback?.Dispose(); // TODO unnecessary null check once ReadResponse is removed
try{
byte[] buffer = new byte[Math.Min(dataIn.Length - dataIn.Position, dataOut.Length)];
int length = buffer.Length;
dataIn.Read(buffer, 0, length);
dataOut.Write(buffer, 0, length);
bytesRead = length;
}catch{ // catch IOException, possibly NullReferenceException if dataIn is null
bytesRead = 0;
}
return bytesRead > 0;
}
bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback){
bytesSkipped = -2; // ERR_FAILED
callback.Dispose();
return false;
}
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback){
return ((IResourceHandler)this).Open(request, out bool _, callback);
} }
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback){ bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback){
callback.Dispose(); return ((IResourceHandler)this).Read(dataOut, out bytesRead, null);
try{
int length = (int)dataIn.Length;
dataIn.CopyTo(dataOut, length);
bytesRead = length;
return true;
}catch{ // catch IOException, possibly NullReferenceException if dataIn is null
bytesRead = 0;
return false;
}
}
bool IResourceHandler.CanGetCookie(Cookie cookie){
return true;
}
bool IResourceHandler.CanSetCookie(Cookie cookie){
return true;
} }
void IResourceHandler.Cancel(){} void IResourceHandler.Cancel(){}

View File

@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
using CefSharp;
using TweetDuck.Browser.Data;
namespace TweetDuck.Browser.Handling{
abstract class ResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler{
private class SelfFactoryImpl : IResourceRequestHandlerFactory{
private readonly ResourceRequestHandler me;
public SelfFactoryImpl(ResourceRequestHandler me){
this.me = me;
}
bool IResourceRequestHandlerFactory.HasHandlers { get; } = true;
[SuppressMessage("ReSharper", "RedundantAssignment")]
IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling){
disableDefaultHandling = me.ResourceHandlers.HasHandler(request);
return me;
}
}
public IResourceRequestHandlerFactory SelfFactory { get; }
public ResourceHandlers ResourceHandlers { get; }
protected ResourceRequestHandler(){
this.SelfFactory = new SelfFactoryImpl(this);
this.ResourceHandlers = new ResourceHandlers();
}
protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request){
return ResourceHandlers.GetHandler(request);
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using CefSharp;
using TweetDuck.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Handling{
class ResourceRequestHandlerBase : ResourceRequestHandler{
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){
if (string.IsNullOrEmpty(rules)){
return;
}
TweetDeckHashes.Clear();
foreach(string rule in rules.Replace(" ", "").ToLower().Split(',')){
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);
}
else{
throw new ArgumentException("Invalid hash characters: " + rule);
}
}
}
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){
if (request.ResourceType == ResourceType.CspReport){
callback.Dispose();
return CefReturnValue.Cancel;
}
if (BrowserUtils.HasDevTools){
NameValueCollection headers = request.Headers;
headers.Remove("x-devtools-emulate-network-conditions-client-id");
request.Headers = headers;
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
protected override bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response){
if ((request.ResourceType == ResourceType.Script || request.ResourceType == ResourceType.Stylesheet) && TweetDeckHashes.Count > 0){
string url = request.Url;
Match match = TweetDeckResourceUrl.Match(url);
if (match.Success && TweetDeckHashes.TryGetValue($"{match.Groups[1]}.{match.Groups[3]}", out string hash)){
if (match.Groups[2].Value == hash){
Program.Reporter.LogVerbose("[RequestHandlerBase] Accepting " + url);
}
else{
Program.Reporter.LogVerbose("[RequestHandlerBase] Replacing " + url + " hash with " + hash);
request.Url = TweetDeckResourceUrl.Replace(url, $"/dist/$1.{hash}.$3");
return true;
}
}
}
return base.OnResourceResponse(browserControl, browser, frame, request, response);
}
}
}

View File

@@ -0,0 +1,52 @@
using CefSharp;
using TweetDuck.Browser.Handling.Filters;
using TweetDuck.Utils;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Handling{
class ResourceRequestHandlerBrowser : ResourceRequestHandlerBase{
private const string UrlVendorResource = "/dist/vendor";
private const string UrlLoadingSpinner = "/backgrounds/spinner_blue";
private const string UrlVersionCheck = "/web/dist/version.json";
protected 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 = TwitterUrls.TweetDeck; // redirect plain twitter.com requests, fixes bugs with login 2FA
}
}
else if (request.ResourceType == ResourceType.Image){
if (request.Url.Contains(UrlLoadingSpinner)){
request.Url = TwitterUtils.LoadingSpinner.Url;
}
}
else if (request.ResourceType == ResourceType.Script){
string url = request.Url;
if (url.Contains("analytics.")){
callback.Dispose();
return CefReturnValue.Cancel;
}
else if (url.Contains(UrlVendorResource)){
request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true);
}
}
else if (request.ResourceType == ResourceType.Xhr){
if (request.Url.Contains(UrlVersionCheck)){
callback.Dispose();
return CefReturnValue.Cancel;
}
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response){
if (request.ResourceType == ResourceType.Script && request.Url.Contains(UrlVendorResource) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)){
return new ResponseFilterVendor(totalBytes);
}
return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms; using CefSharp.WinForms;
using TweetDuck.Browser.Data; using TweetDuck.Browser.Data;
using TweetDuck.Browser.Handling; using TweetDuck.Browser.Handling;
@@ -14,7 +13,9 @@ using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Notification{ namespace TweetDuck.Browser.Notification{
abstract partial class FormNotificationBase : Form, AnalyticsFile.IProvider{ 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 readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandlers.ForBytes(Properties.Resources.avatar, "image/png"));
protected const string BlankURL = TwitterUrls.TweetDeck + "/?blank";
public static string FontSize = null; public static string FontSize = null;
public static string HeadLayout = null; public static string HeadLayout = null;
@@ -129,17 +130,20 @@ namespace TweetDuck.Browser.Notification{
this.owner = owner; this.owner = owner;
this.owner.FormClosed += owner_FormClosed; this.owner.FormClosed += owner_FormClosed;
var resourceRequestHandler = new ResourceRequestHandlerBase();
var resourceHandlers = resourceRequestHandler.ResourceHandlers;
ResourceHandlerFactory resourceHandlerFactory = new ResourceHandlerFactory(); resourceHandlers.Register(BlankURL, ResourceHandlers.ForString(string.Empty));
resourceHandlerFactory.RegisterHandler(TwitterUrls.TweetDeck, this.resourceHandler); resourceHandlers.Register(TwitterUrls.TweetDeck, () => this.resourceHandler);
resourceHandlerFactory.RegisterHandler(AppLogo); resourceHandlers.Register(AppLogo);
this.browser = new ChromiumWebBrowser("about:blank"){ this.browser = new ChromiumWebBrowser(BlankURL){
MenuHandler = new ContextMenuNotification(this, enableContextMenu), MenuHandler = new ContextMenuNotification(this, enableContextMenu),
JsDialogHandler = new JavaScriptDialogHandler(), JsDialogHandler = new JavaScriptDialogHandler(),
LifeSpanHandler = new LifeSpanHandler(), LifeSpanHandler = new LifeSpanHandler(),
RequestHandler = new RequestHandlerBase(false), RequestHandler = new RequestHandlerBase(false),
ResourceHandlerFactory = resourceHandlerFactory ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
}; };
this.browser.Dock = DockStyle.None; this.browser.Dock = DockStyle.None;
@@ -185,7 +189,7 @@ namespace TweetDuck.Browser.Notification{
// notification methods // notification methods
public virtual void HideNotification(){ public virtual void HideNotification(){
browser.Load("about:blank"); browser.Load(BlankURL);
DisplayTooltip(null); DisplayTooltip(null);
Location = ControlExtensions.InvisibleLocation; Location = ControlExtensions.InvisibleLocation;

View File

@@ -12,6 +12,7 @@ using TweetLib.Core.Data;
using TweetLib.Core.Features.Notifications; using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Notification{ namespace TweetDuck.Browser.Notification{
abstract partial class FormNotificationMain : FormNotificationBase{ abstract partial class FormNotificationMain : FormNotificationBase{
@@ -72,12 +73,12 @@ namespace TweetDuck.Browser.Notification{
this.timerBarHeight = BrowserUtils.Scale(4, DpiScale); this.timerBarHeight = BrowserUtils.Scale(4, DpiScale);
browser.KeyboardHandler = new KeyboardHandlerNotification(this); browser.KeyboardHandler = new KeyboardHandlerNotification(this);
browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge.Notification(owner, this)); browser.RegisterJsBridge("$TD", new TweetDeckBridge.Notification(owner, this));
browser.LoadingStateChanged += Browser_LoadingStateChanged; browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd; browser.FrameLoadEnd += Browser_FrameLoadEnd;
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser)); plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser, url => TwitterUrls.IsTweetDeck(url) && url != BlankURL));
mouseHookDelegate = MouseHookProc; mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook(true); Disposed += (sender, args) => StopMouseHook(true);
@@ -113,7 +114,15 @@ namespace TweetDuck.Browser.Notification{
int eventType = wParam.ToInt32(); int eventType = wParam.ToInt32();
if (eventType == NativeMethods.WM_MOUSEWHEEL && IsCursorOverBrowser){ if (eventType == NativeMethods.WM_MOUSEWHEEL && IsCursorOverBrowser){
browser.SendMouseWheelEvent(0, 0, 0, BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed * 0.01), CefEventFlags.None); int delta = BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed * 0.01);
if (Config.EnableSmoothScrolling){
browser.ExecuteScriptAsync("window.TDGF_scrollSmoothly", (int)Math.Round(-delta / 0.6));
}
else{
browser.SendMouseWheelEvent(0, 0, 0, delta, CefEventFlags.None);
}
return NativeMethods.HOOK_HANDLED; return NativeMethods.HOOK_HANDLED;
} }
else if (eventType == NativeMethods.WM_XBUTTONDOWN && DesktopBounds.Contains(Cursor.Position)){ else if (eventType == NativeMethods.WM_XBUTTONDOWN && DesktopBounds.Contains(Cursor.Position)){
@@ -154,7 +163,7 @@ namespace TweetDuck.Browser.Notification{
} }
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){ private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
if (!e.IsLoading && browser.Address != "about:blank"){ if (!e.IsLoading && browser.Address != BlankURL){
this.InvokeSafe(() => { this.InvokeSafe(() => {
Visible = true; // ensures repaint before moving the window to a visible location Visible = true; // ensures repaint before moving the window to a visible location
timerDisplayDelay.Start(); timerDisplayDelay.Start();
@@ -165,7 +174,7 @@ namespace TweetDuck.Browser.Notification{
private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){ private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
IFrame frame = e.Frame; IFrame frame = e.Frame;
if (frame.IsMain && browser.Address != "about:blank"){ if (frame.IsMain && browser.Address != BlankURL){
frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification)); frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification));
CefScriptExecutor.RunFile(frame, "notification.js"); CefScriptExecutor.RunFile(frame, "notification.js");
} }

View File

@@ -23,7 +23,7 @@ namespace TweetDuck.Browser.Notification.Screenshot{
int realWidth = BrowserUtils.Scale(width, DpiScale); int realWidth = BrowserUtils.Scale(width, DpiScale);
browser.RegisterAsyncJsObject("$TD_NotificationScreenshot", new ScreenshotBridge(this, SetScreenshotHeight, callback)); browser.RegisterJsBridge("$TD_NotificationScreenshot", new ScreenshotBridge(this, SetScreenshotHeight, callback));
browser.LoadingStateChanged += (sender, args) => { browser.LoadingStateChanged += (sender, args) => {
if (args.IsLoading){ if (args.IsLoading){

View File

@@ -1,7 +1,9 @@
using System.Drawing; using System;
using System.Drawing;
using System.IO; using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using CefSharp; using CefSharp;
using TweetDuck.Browser.Data;
using TweetDuck.Controls; using TweetDuck.Controls;
using TweetDuck.Dialogs; using TweetDuck.Dialogs;
using TweetDuck.Dialogs.Settings; using TweetDuck.Dialogs.Settings;
@@ -11,7 +13,7 @@ namespace TweetDuck.Browser.Notification{
static class SoundNotification{ static class SoundNotification{
public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm"; public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
public static IResourceHandler CreateFileHandler(string path){ public static Func<IResourceHandler> CreateFileHandler(string path){
string mimeType = Path.GetExtension(path) switch{ string mimeType = Path.GetExtension(path) switch{
".weba" => "audio/webm", ".weba" => "audio/webm",
".webm" => "audio/webm", ".webm" => "audio/webm",
@@ -24,7 +26,7 @@ namespace TweetDuck.Browser.Notification{
}; };
try{ try{
return ResourceHandler.FromFilePath(path, mimeType); return ResourceHandlers.ForBytes(File.ReadAllBytes(path), mimeType);
}catch{ }catch{
FormBrowser browser = FormManager.TryFind<FormBrowser>(); FormBrowser browser = FormManager.TryFind<FormBrowser>();

View File

@@ -6,6 +6,7 @@ using CefSharp;
using CefSharp.WinForms; using CefSharp.WinForms;
using TweetDuck.Browser.Adapters; using TweetDuck.Browser.Adapters;
using TweetDuck.Browser.Bridge; using TweetDuck.Browser.Bridge;
using TweetDuck.Browser.Data;
using TweetDuck.Browser.Handling; using TweetDuck.Browser.Handling;
using TweetDuck.Browser.Handling.General; using TweetDuck.Browser.Handling.General;
using TweetDuck.Browser.Notification; using TweetDuck.Browser.Notification;
@@ -44,13 +45,16 @@ namespace TweetDuck.Browser{
} }
private readonly ChromiumWebBrowser browser; private readonly ChromiumWebBrowser browser;
private readonly ResourceHandlerFactory resourceHandlerFactory = new ResourceHandlerFactory(); private readonly ResourceHandlers resourceHandlers;
private string prevSoundNotificationPath = null; private string prevSoundNotificationPath = null;
public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge tdBridge, UpdateBridge updateBridge){ public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge tdBridge, UpdateBridge updateBridge){
resourceHandlerFactory.RegisterHandler(FormNotificationBase.AppLogo); var resourceRequestHandler = new ResourceRequestHandlerBrowser();
resourceHandlerFactory.RegisterHandler(TwitterUtils.LoadingSpinner); resourceHandlers = resourceRequestHandler.ResourceHandlers;
resourceHandlers.Register(FormNotificationBase.AppLogo);
resourceHandlers.Register(TwitterUtils.LoadingSpinner);
RequestHandlerBrowser requestHandler = new RequestHandlerBrowser(); RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
@@ -62,7 +66,7 @@ namespace TweetDuck.Browser{
KeyboardHandler = new KeyboardHandlerBrowser(owner), KeyboardHandler = new KeyboardHandlerBrowser(owner),
LifeSpanHandler = new LifeSpanHandler(), LifeSpanHandler = new LifeSpanHandler(),
RequestHandler = requestHandler, RequestHandler = requestHandler,
ResourceHandlerFactory = resourceHandlerFactory ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
}; };
this.browser.LoadingStateChanged += browser_LoadingStateChanged; this.browser.LoadingStateChanged += browser_LoadingStateChanged;
@@ -70,8 +74,8 @@ namespace TweetDuck.Browser{
this.browser.FrameLoadEnd += browser_FrameLoadEnd; this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.browser.LoadError += browser_LoadError; this.browser.LoadError += browser_LoadError;
this.browser.RegisterAsyncJsObject("$TD", tdBridge); this.browser.RegisterJsBridge("$TD", tdBridge);
this.browser.RegisterAsyncJsObject("$TDU", updateBridge); this.browser.RegisterJsBridge("$TDU", updateBridge);
this.browser.BrowserSettings.BackgroundColor = (uint)TwitterUtils.BackgroundColor.ToArgb(); this.browser.BrowserSettings.BackgroundColor = (uint)TwitterUtils.BackgroundColor.ToArgb();
this.browser.Dock = DockStyle.None; this.browser.Dock = DockStyle.None;
@@ -79,7 +83,7 @@ namespace TweetDuck.Browser{
this.browser.SetupZoomEvents(); this.browser.SetupZoomEvents();
owner.Controls.Add(browser); owner.Controls.Add(browser);
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser)); plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser, TwitterUrls.IsTweetDeck));
Config.MuteToggled += Config_MuteToggled; Config.MuteToggled += Config_MuteToggled;
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged; Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
@@ -87,6 +91,12 @@ namespace TweetDuck.Browser{
// setup and management // setup and management
public void PrepareSize(Size size){
if (!Ready){
browser.Size = size;
}
}
private void OnBrowserReady(){ private void OnBrowserReady(){
if (!Ready){ if (!Ready){
browser.Location = Point.Empty; browser.Location = Point.Empty;
@@ -127,7 +137,7 @@ namespace TweetDuck.Browser{
if (TwitterUrls.IsTwitter(url)){ if (TwitterUrls.IsTwitter(url)){
string css = Program.Resources.Load("styles/twitter.css"); string css = Program.Resources.Load("styles/twitter.css");
resourceHandlerFactory.RegisterHandler(TwitterStyleUrl, ResourceHandler.FromString(css, mimeType: "text/css")); resourceHandlers.Register(TwitterStyleUrl, ResourceHandlers.ForString(css, "text/css"));
CefScriptExecutor.RunFile(frame, "twitter.js"); CefScriptExecutor.RunFile(frame, "twitter.js");
} }
@@ -166,7 +176,7 @@ namespace TweetDuck.Browser{
} }
if (url == ErrorUrl){ if (url == ErrorUrl){
resourceHandlerFactory.UnregisterHandler(ErrorUrl); resourceHandlers.Unregister(ErrorUrl);
} }
} }
@@ -182,7 +192,7 @@ namespace TweetDuck.Browser{
string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode); string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty); string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
resourceHandlerFactory.RegisterHandler(ErrorUrl, ResourceHandler.FromString(errorPage.Replace("{err}", errorTitle))); resourceHandlers.Register(ErrorUrl, ResourceHandlers.ForString(errorPage.Replace("{err}", errorTitle)));
browser.Load(ErrorUrl); browser.Load(ErrorUrl);
} }
} }
@@ -202,10 +212,10 @@ namespace TweetDuck.Browser{
prevSoundNotificationPath = newNotificationPath; prevSoundNotificationPath = newNotificationPath;
if (hasCustomSound){ if (hasCustomSound){
resourceHandlerFactory.RegisterHandler(soundUrl, SoundNotification.CreateFileHandler(newNotificationPath)); resourceHandlers.Register(soundUrl, SoundNotification.CreateFileHandler(newNotificationPath));
} }
else{ else{
resourceHandlerFactory.UnregisterHandler(soundUrl); resourceHandlers.Unregister(soundUrl);
} }
} }

View File

@@ -17,7 +17,7 @@ namespace TweetDuck.Dialogs{
private const string GuideUrl = "https://tweetduck.chylex.com/guide/v2/"; private const string GuideUrl = "https://tweetduck.chylex.com/guide/v2/";
private const string GuidePathRegex = @"^guide(?:/v\d+)?(?:/(#.*))?"; private const string GuidePathRegex = @"^guide(?:/v\d+)?(?:/(#.*))?";
private static readonly ResourceLink DummyPage = new ResourceLink("http://td/dummy", ResourceHandler.FromString("")); private static readonly ResourceLink DummyPage = new ResourceLink("http://td/dummy", ResourceHandlers.ForString(string.Empty));
public static bool CheckGuideUrl(string url, out string hash){ public static bool CheckGuideUrl(string url, out string hash){
if (!url.Contains("//tweetduck.chylex.com/guide")){ if (!url.Contains("//tweetduck.chylex.com/guide")){
@@ -69,8 +69,8 @@ namespace TweetDuck.Dialogs{
Size = new Size(owner.Size.Width * 3 / 4, owner.Size.Height * 3 / 4); Size = new Size(owner.Size.Width * 3 / 4, owner.Size.Height * 3 / 4);
VisibleChanged += (sender, args) => this.MoveToCenter(owner); VisibleChanged += (sender, args) => this.MoveToCenter(owner);
ResourceHandlerFactory resourceHandlerFactory = new ResourceHandlerFactory(); var resourceRequestHandler = new ResourceRequestHandlerBase();
resourceHandlerFactory.RegisterHandler(DummyPage); resourceRequestHandler.ResourceHandlers.Register(DummyPage);
this.browser = new ChromiumWebBrowser(url){ this.browser = new ChromiumWebBrowser(url){
MenuHandler = new ContextMenuGuide(owner), MenuHandler = new ContextMenuGuide(owner),
@@ -78,7 +78,7 @@ namespace TweetDuck.Dialogs{
KeyboardHandler = new KeyboardHandlerBase(), KeyboardHandler = new KeyboardHandlerBase(),
LifeSpanHandler = new LifeSpanHandler(), LifeSpanHandler = new LifeSpanHandler(),
RequestHandler = new RequestHandlerBase(true), RequestHandler = new RequestHandlerBase(true),
ResourceHandlerFactory = resourceHandlerFactory ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
}; };
browser.LoadingStateChanged += browser_LoadingStateChanged; browser.LoadingStateChanged += browser_LoadingStateChanged;

View File

@@ -23,7 +23,6 @@ namespace TweetDuck.Dialogs.Settings{
}); });
}; };
this.notification.Activated += notification_Activated;
this.notification.Show(); this.notification.Show();
Disposed += (sender, args) => this.notification.Dispose(); Disposed += (sender, args) => this.notification.Dispose();
@@ -139,11 +138,6 @@ namespace TweetDuck.Dialogs.Settings{
} }
} }
private void notification_Activated(object sender, EventArgs e){
notification.Hide();
notification.Activated -= notification_Activated;
}
private void notification_Move(object sender, EventArgs e){ private void notification_Move(object sender, EventArgs e){
if (radioLocCustom.Checked && notification.Location != ControlExtensions.InvisibleLocation){ if (radioLocCustom.Checked && notification.Location != ControlExtensions.InvisibleLocation){
Config.CustomNotificationPosition = notification.Location; Config.CustomNotificationPosition = notification.Location;

View File

@@ -72,6 +72,14 @@ namespace TweetDuck.Dialogs.Settings{
}; };
if (dialog.ShowDialog() == DialogResult.OK){ if (dialog.ShowDialog() == DialogResult.OK){
try{
if (new FileInfo(dialog.FileName).Length > (1024 * 1024) && !FormMessage.Warning("Sound Notification", "The sound file is larger than 1 MB, this will cause increased memory usage. Use this file anyway?", FormMessage.Yes, FormMessage.No)){
return;
}
}catch{
// ignore
}
tbCustomSound.Text = dialog.FileName; tbCustomSound.Text = dialog.FileName;
} }
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using CefSharp; using CefSharp;
using TweetDuck.Browser.Adapters; using TweetDuck.Browser.Adapters;
using TweetDuck.Utils;
using TweetLib.Core.Browser; using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events; using TweetLib.Core.Features.Plugins.Events;
@@ -12,21 +13,23 @@ namespace TweetDuck.Plugins{
private readonly IWebBrowser browser; private readonly IWebBrowser browser;
private readonly IScriptExecutor executor; private readonly IScriptExecutor executor;
private readonly Func<string, bool> executeOnUrl;
public PluginDispatcher(IWebBrowser browser){ public PluginDispatcher(IWebBrowser browser, Func<string, bool> executeOnUrl){
this.executeOnUrl = executeOnUrl;
this.browser = browser; this.browser = browser;
this.browser.FrameLoadEnd += browser_FrameLoadEnd; this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.executor = new CefScriptExecutor(browser); this.executor = new CefScriptExecutor(browser);
} }
void IPluginDispatcher.AttachBridge(string name, object bridge){ void IPluginDispatcher.AttachBridge(string name, object bridge){
browser.RegisterAsyncJsObject(name, bridge); browser.RegisterJsBridge(name, bridge);
} }
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){ private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
IFrame frame = e.Frame; IFrame frame = e.Frame;
if (frame.IsMain && TwitterUrls.IsTweetDeck(frame.Url)){ if (frame.IsMain && executeOnUrl(frame.Url)){
Ready?.Invoke(this, new PluginDispatchEventArgs(executor)); Ready?.Invoke(this, new PluginDispatchEventArgs(executor));
} }
} }

View File

@@ -0,0 +1,47 @@
using System.IO;
using System.Net;
using System.Text;
using CefSharp;
using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Plugins{
sealed class PluginSchemeFactory : ISchemeHandlerFactory{
public const string Name = PluginSchemeHandler<IResourceHandler>.Name;
private readonly PluginSchemeHandler<IResourceHandler> handler = new PluginSchemeHandler<IResourceHandler>(new ResourceProvider());
internal void Setup(PluginManager plugins){
handler.Setup(plugins);
}
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request){
return handler.Process(request.Url);
}
private sealed class ResourceProvider : IResourceProvider<IResourceHandler>{
private static ResourceHandler CreateHandler(byte[] bytes){
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
handler.Headers.Set("Access-Control-Allow-Origin", "*");
return handler;
}
public IResourceHandler Status(HttpStatusCode code, string message){
var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
handler.StatusCode = (int)code;
return handler;
}
public IResourceHandler File(byte[] bytes, string extension){
if (bytes.Length == 0){
return Status(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
}
else{
var handler = CreateHandler(bytes);
handler.MimeType = Cef.GetMimeType(extension);
return handler;
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ using TweetDuck.Browser.Handling.General;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Dialogs; using TweetDuck.Dialogs;
using TweetDuck.Management; using TweetDuck.Management;
using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetDuck.Utils; using TweetDuck.Utils;
using TweetLib.Core; using TweetLib.Core;
@@ -143,11 +144,12 @@ namespace TweetDuck{
if (Arguments.HasFlag(Arguments.ArgUpdated)){ if (Arguments.HasFlag(Arguments.ArgUpdated)){
WindowsUtils.TryDeleteFolderWhenAble(InstallerPath, 8000); WindowsUtils.TryDeleteFolderWhenAble(InstallerPath, 8000);
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(StoragePath, "Service Worker"), 4000);
BrowserCache.TryClearNow(); BrowserCache.TryClearNow();
} }
try{ try{
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze)); ResourceRequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
}catch(Exception e){ }catch(Exception e){
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK); FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK);
return; return;
@@ -160,7 +162,7 @@ namespace TweetDuck{
CefSettings settings = new CefSettings{ CefSettings settings = new CefSettings{
UserAgent = BrowserUtils.UserAgentChrome, UserAgent = BrowserUtils.UserAgentChrome,
BrowserSubprocessPath = BrandName + ".Browser.exe", BrowserSubprocessPath = Path.Combine(ProgramPath, BrandName + ".Browser.exe"),
CachePath = StoragePath, CachePath = StoragePath,
UserDataPath = CefDataPath, UserDataPath = CefDataPath,
LogFile = ConsoleLogFilePath, LogFile = ConsoleLogFilePath,
@@ -168,6 +170,17 @@ namespace TweetDuck{
LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable
#endif #endif
}; };
var pluginScheme = new PluginSchemeFactory();
settings.RegisterScheme(new CefCustomScheme{
SchemeName = PluginSchemeFactory.Name,
IsStandard = false,
IsSecure = true,
IsCorsEnabled = true,
IsCSPBypassing = true,
SchemeHandlerFactory = pluginScheme
});
CommandLineArgs.ReadCefArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs); CommandLineArgs.ReadCefArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs); BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
@@ -176,18 +189,14 @@ namespace TweetDuck{
Win.Application.ApplicationExit += (sender, args) => ExitCleanup(); Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser(); FormBrowser mainForm = new FormBrowser(pluginScheme);
Resources.Initialize(mainForm); Resources.Initialize(mainForm);
Win.Application.Run(mainForm); Win.Application.Run(mainForm);
if (mainForm.UpdateInstallerPath != null){ if (mainForm.UpdateInstaller != null){
ExitCleanup(); ExitCleanup();
// ProgramPath has a trailing backslash if (mainForm.UpdateInstaller.Launch()){
string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\"" + ProgramPath + "\" /RUNARGS=\"" + Arguments.GetCurrentForInstallerCmd() + "\"" + (IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !FileUtils.CheckFolderWritePermission(ProgramPath);
if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
Win.Application.Exit(); Win.Application.Exit();
} }
else{ else{

View File

@@ -68,6 +68,7 @@ let main (argv: string[]) =
line.TrimStart() line.TrimStart()
// Functions (File Management) // Functions (File Management)
let copyFile source target = let copyFile source target =
File.Copy(source, target, true) File.Copy(source, target, true)
@@ -94,7 +95,7 @@ let main (argv: string[]) =
// Functions (File Processing) // Functions (File Processing)
let byPattern path pattern = let byPattern path pattern =
Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories) Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories) |> Seq.filter (fun (file: string) -> not (file.Contains(importsDir)))
let exceptEndingWith name = let exceptEndingWith name =
Seq.filter (fun (file: string) -> not (file.EndsWith(name))) Seq.filter (fun (file: string) -> not (file.EndsWith(name)))
@@ -121,7 +122,7 @@ let main (argv: string[]) =
let includeVersion = relativePath.StartsWith(@"scripts\") && not (relativePath.StartsWith(@"scripts\imports\")) let includeVersion = relativePath.StartsWith(@"scripts\") && not (relativePath.StartsWith(@"scripts\imports\"))
let finalLines = if includeVersion then seq { yield "#" + version; yield! lines } else lines let finalLines = if includeVersion then seq { yield "#" + version; yield! lines } else lines
File.WriteAllLines(fullPath, finalLines |> filterNotEmpty |> Seq.toArray) File.WriteAllLines(fullPath, finalLines |> Seq.toArray)
printfn "Processed %s" relativePath printfn "Processed %s" relativePath
let processFiles (files: string seq) (extProcessors: IDictionary<string, (string seq -> string seq)>) = let processFiles (files: string seq) (extProcessors: IDictionary<string, (string seq -> string seq)>) =
@@ -184,6 +185,7 @@ let main (argv: string[]) =
|> replaceRegex @"^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$" "$1" |> replaceRegex @"^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$" "$1"
|> replaceRegex @"(?<!\w)(return|throw)(\s.*?)? if (.*?);" "if ($3)$1$2;" |> replaceRegex @"(?<!\w)(return|throw)(\s.*?)? if (.*?);" "if ($3)$1$2;"
) )
|> filterNotEmpty
); );
".css", (fun (lines: string seq) -> ".css", (fun (lines: string seq) ->
@@ -204,6 +206,7 @@ let main (argv: string[]) =
".html", (fun (lines: string seq) -> ".html", (fun (lines: string seq) ->
lines lines
|> Seq.map trimStart |> Seq.map trimStart
|> filterNotEmpty
); );
".meta", (fun (lines: string seq) -> ".meta", (fun (lines: string seq) ->

View File

@@ -34,6 +34,7 @@ try{
$contents = [IO.File]::ReadAllText($browserProj) $contents = [IO.File]::ReadAllText($browserProj)
$contents = $contents -Replace '(?<=<HintPath>\.\.\\packages\\CefSharp\.Common\.)(.*?)(?=\\)', $sharpVersion $contents = $contents -Replace '(?<=<HintPath>\.\.\\packages\\CefSharp\.Common\.)(.*?)(?=\\)', $sharpVersion
$contents = $contents -Replace '(?<=<Reference Include="CefSharp, Version=)(\d+)', $sharpVersion.Split(".")[0]
$contents = $contents -Replace '(?<=<Reference Include="CefSharp\.BrowserSubprocess\.Core, Version=)(\d+)', $sharpVersion.Split(".")[0] $contents = $contents -Replace '(?<=<Reference Include="CefSharp\.BrowserSubprocess\.Core, Version=)(\d+)', $sharpVersion.Split(".")[0]
[IO.File]::WriteAllText($browserProj, $contents) [IO.File]::WriteAllText($browserProj, $contents)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
<div id="tweetduck-conn-issues" class="Layer NotificationListLayer">
<ul class="NotificationList">
<li class="Notification Notification--red" style="height:63px;">
<div class="Notification-inner">
<div class="Notification-icon"><span class="Icon Icon--medium Icon--circleError"></span></div>
<div class="Notification-content"><div class="Notification-body">Experiencing connection issues</div></div>
<button type="button" class="Notification-closeButton" aria-label="Close"><span class="Icon Icon--smallest Icon--close" aria-hidden="true"></span></button>
</div>
</li>
</ul>
</div>

View File

@@ -0,0 +1,667 @@
(function(){
//
// Function: Event callback for a new tweet.
//
const onNewTweet = (function(){
const recentMessages = new Set();
const recentTweets = new Set();
let recentTweetTimer = null;
const resetRecentTweets = () => {
recentTweetTimer = null;
recentTweets.clear();
};
const startRecentTweetTimer = () => {
recentTweetTimer && window.clearTimeout(recentTweetTimer);
recentTweetTimer = window.setTimeout(resetRecentTweets, 20000);
};
const checkTweetCache = (set, id) => {
return true if set.has(id);
if (set.size > 50){
set.clear();
}
set.add(id);
return false;
};
const isSensitive = (tweet) => {
const main = tweet.getMainTweet && tweet.getMainTweet();
return true if main && main.possiblySensitive; // TODO these don't show media badges when hiding sensitive media
const related = tweet.getRelatedTweet && tweet.getRelatedTweet();
return true if related && related.possiblySensitive;
const quoted = tweet.quotedTweet;
return true if quoted && quoted.possiblySensitive;
return false;
};
const fixMedia = (html, media) => {
return html.find("a[data-media-entity-id='" + media.mediaId + "'], .media-item").first().removeClass("is-zoomable").css("background-image", 'url("' + media.small() + '")');
};
return function(column, tweet){
if (tweet instanceof TD.services.TwitterConversation || tweet instanceof TD.services.TwitterConversationMessageEvent){
return if checkTweetCache(recentMessages, tweet.id);
}
else{
return if checkTweetCache(recentTweets, tweet.id);
}
startRecentTweetTimer();
if (column.model.getHasNotification()){
const sensitive = isSensitive(tweet);
const previews = $TDX.notificationMediaPreviews && (!sensitive || TD.settings.getDisplaySensitiveMedia());
// TODO new cards don't have either previews or links
const html = $(tweet.render({
withFooter: false,
withTweetActions: false,
withMediaPreview: true,
isMediaPreviewOff: !previews,
isMediaPreviewSmall: previews,
isMediaPreviewLarge: false,
isMediaPreviewCompact: false,
isMediaPreviewInQuoted: previews,
thumbSizeClass: "media-size-medium",
mediaPreviewSize: "medium"
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".js-quote-detail").removeClass("is-actionable margin-b--8"); // prevent quoted tweets from changing the cursor and reduce bottom margin
if (previews){
html.find(".reverse-image-search").remove();
const container = html.find(".js-media");
for(let media of tweet.getMedia()){
fixMedia(container, media);
}
if (tweet.quotedTweet){
for(let media of tweet.quotedTweet.getMedia()){
fixMedia(container, media).addClass("media-size-medium");
}
}
}
else if (tweet instanceof TD.services.TwitterActionOnTweet){
html.find(".js-media").remove();
}
html.find("a[data-full-url]").each(function(){ // bypass t.co on all links
this.href = this.getAttribute("data-full-url");
});
html.find("a[href='#']").each(function(){ // remove <a> tags around links that don't lead anywhere (such as account names the tweet replied to)
this.outerHTML = this.innerHTML;
});
html.find("p.link-complex-target").filter(function(){
return $(this).text() === "Show this thread";
}).first().each(function(){
this.id = "tduck-show-thread";
const moveBefore = html.find(".tweet-body > .js-media, .tweet-body > .js-media-preview-container, .quoted-tweet");
if (moveBefore){
$(this).css("margin-top", "5px").removeClass("margin-b--5").parent("span").detach().insertBefore(moveBefore);
}
});
if (tweet.quotedTweet){
html.find("p.txt-mute").filter(function(){
return $(this).text() === "Show this thread";
}).first().remove();
}
const type = tweet.getChirpType();
if (type === "follow"){
html.find(".js-user-actions-menu").parent().remove();
html.find(".account-bio").removeClass("padding-t--5").css("padding-top", "2px");
}
else if ((type.startsWith("favorite") || type.startsWith("retweet")) && tweet.isAboutYou()){
html.children().first().addClass("td-notification-padded");
}
else if (type.includes("list_member")){
html.children().first().addClass("td-notification-padded td-notification-padded-alt");
html.find(".activity-header").css("margin-top", "2px");
html.find(".avatar").first().css("margin-bottom", "0");
}
if (sensitive){
html.find(".media-badge").each(function(){
$(this)[0].lastChild.textContent += " (possibly sensitive)";
});
}
const source = tweet.getRelatedTweet();
const duration = source ? source.text.length + (source.quotedTweet ? source.quotedTweet.text.length : 0) : tweet.text.length;
const chirpId = source ? source.id : "";
const tweetUrl = source ? source.getChirpURL() : "";
const quoteUrl = source && source.quotedTweet ? source.quotedTweet.getChirpURL() : "";
$TD.onTweetPopup(column.model.privateState.apiid, chirpId, window.TDGF_getColumnName(column), html.html(), duration, tweetUrl, quoteUrl);
}
if (column.model.getHasSound()){
$TD.onTweetSound();
}
};
})();
//
// Block: Enable popup notifications.
//
execSafe(function hookDesktopNotifications(){
ensurePropertyExists(TD, "controller", "notifications");
TD.controller.notifications.hasNotifications = function(){
return true;
};
TD.controller.notifications.isPermissionGranted = function(){
return true;
};
$.subscribe("/notifications/new", function(obj){
for(let index = obj.items.length - 1; index >= 0; index--){
onNewTweet(obj.column, obj.items[index]);
}
});
});
//
// Block: Fix DM notifications not showing if the conversation is open.
//
if (checkPropertyExists(TD, "vo", "Column", "prototype", "mergeMissingChirps")){
TD.vo.Column.prototype.mergeMissingChirps = prependToFunction(TD.vo.Column.prototype.mergeMissingChirps, function(e){
const model = this.model;
if (model && model.state && model.state.type === "privateMe" && !this.notificationsDisabled && e.poller.feed.managed){
const unread = [];
for(let chirp of e.chirps){
if (Array.isArray(chirp.messages)){
Array.prototype.push.apply(unread, chirp.messages.filter(message => message.read === false));
}
}
if (unread.length > 0){
if (checkPropertyExists(TD, "util", "chirpReverseColumnSort")){
unread.sort(TD.util.chirpReverseColumnSort);
}
for(let message of unread){
onNewTweet(this, message);
}
// TODO sound notifications are borked as well
// TODO figure out what to do with missed notifications at startup
}
}
});
}
})();
//
// Block: Mute sound notifications.
//
HTMLAudioElement.prototype.play = prependToFunction(HTMLAudioElement.prototype.play, function(){
return $TDX.muteNotifications;
});
//
// Block: Add additional link information to context menu.
//
execSafe(function setupLinkContextMenu(){
$(document.body).delegate("a", "contextmenu", function(){
const me = $(this)[0];
if (me.classList.contains("js-media-image-link")){
const hovered = getHoveredTweet();
return if !hovered;
const tweet = hovered.obj.hasMedia() ? hovered.obj : hovered.obj.quotedTweet;
const media = tweet.getMedia().find(media => media.mediaId === me.getAttribute("data-media-entity-id"));
if ((media.isVideo && media.service === "twitter") || media.isAnimatedGif){
$TD.setRightClickedLink("video", media.chooseVideoVariant().url);
}
else{
$TD.setRightClickedLink("image", media.large());
}
}
else if (me.classList.contains("js-gif-play")){
$TD.setRightClickedLink("video", $(this).closest(".js-media-gif-container").find("video").attr("src"));
}
else if (me.hasAttribute("data-full-url")){
$TD.setRightClickedLink("link", me.getAttribute("data-full-url"));
}
});
});
//
// Block: Add tweet-related options to context menu.
//
execSafe(function setupTweetContextMenu(){
ensurePropertyExists(TD, "controller", "columnManager", "get");
ensurePropertyExists(TD, "services", "ChirpBase", "TWEET");
ensurePropertyExists(TD, "services", "TwitterActionFollow");
const processMedia = function(chirp){
return chirp.getMedia().filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https + ":small").join(";");
};
app.delegate("section.js-column", "contextmenu", function(){
const hovered = getHoveredTweet();
return if !hovered;
const tweet = hovered.obj;
const quote = tweet.quotedTweet;
if (tweet.chirpType === TD.services.ChirpBase.TWEET){
const tweetUrl = tweet.getChirpURL();
const quoteUrl = quote && quote.getChirpURL();
const chirpAuthors = quote ? [ tweet.getMainUser().screenName, quote.getMainUser().screenName ].join(";") : tweet.getMainUser().screenName;
const chirpImages = tweet.hasImage() ? processMedia(tweet) : quote && quote.hasImage() ? processMedia(quote) : "";
$TD.setRightClickedChirp(tweetUrl || "", quoteUrl || "", chirpAuthors, chirpImages);
}
else if (tweet instanceof TD.services.TwitterActionFollow){
$TD.setRightClickedLink("link", tweet.following.getProfileURL());
}
});
});
//
// Block: Expand shortened links on hover or display tooltip.
//
execSafe(function setupLinkExpansionOrTooltip(){
let prevMouseX = -1, prevMouseY = -1;
let tooltipTimer, tooltipDisplayed;
$(document.body).delegate("a[data-full-url]", {
mouseenter: function(){
const me = $(this);
const text = me.text();
return if text.charCodeAt(text.length - 1) !== 8230 && text.charCodeAt(0) !== 8230; // horizontal ellipsis
if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){
me.attr("td-prev-text", text);
me.text(me.attr("data-full-url").replace(/^https?:\/\/(www\.)?/, ""));
}, 200);
}
else{
tooltipTimer = window.setTimeout(function(){
$TD.displayTooltip(me.attr("data-full-url"));
tooltipDisplayed = true;
}, 400);
}
},
mouseleave: function(){
const me = $(this)[0];
if (me.hasAttribute("td-prev-text")){
me.innerText = me.getAttribute("td-prev-text");
}
window.clearTimeout(tooltipTimer);
if (tooltipDisplayed){
tooltipDisplayed = false;
$TD.displayTooltip(null);
}
},
mousemove: function(e){
if (tooltipDisplayed && (prevMouseX !== e.clientX || prevMouseY !== e.clientY)){
$TD.displayTooltip($(this).attr("data-full-url"));
prevMouseX = e.clientX;
prevMouseY = e.clientY;
}
}
});
});
//
// Block: Support for extra mouse buttons.
//
execSafe(function supportExtraMouseButtons(){
const tryClickSelector = function(selector, parent){
return $(selector, parent).click().length;
};
const tryCloseModal1 = function(){
const modal = $("#open-modal");
return modal.is(":visible") && tryClickSelector("a.mdl-dismiss", modal);
};
const tryCloseModal2 = function(){
const modal = $(".js-modals-container");
return modal.length && tryClickSelector("a.mdl-dismiss", modal);
};
const tryCloseHighlightedColumn = function(){
const column = getHoveredColumn();
return false if !column;
const ele = $(column.ele);
return ((ele.is(".is-shifted-2") && tryClickSelector(".js-tweet-social-proof-back", ele)) || (ele.is(".is-shifted-1") && tryClickSelector(".js-column-back", ele)));
};
window.TDGF_onMouseClickExtra = function(button){
if (button === 1){ // back button
tryClickSelector(".is-shifted-2 .js-tweet-social-proof-back", ".js-modal-panel") ||
tryClickSelector(".is-shifted-1 .js-column-back", ".js-modal-panel") ||
tryCloseModal1() ||
tryCloseModal2() ||
tryClickSelector(".js-inline-compose-close") ||
tryCloseHighlightedColumn() ||
tryClickSelector(".js-app-content.is-open .js-drawer-close:visible") ||
tryClickSelector(".is-shifted-2 .js-tweet-social-proof-back, .is-shifted-2 .js-dm-participants-back") ||
$(".is-shifted-1 .js-column-back").click();
}
else if (button === 2){ // forward button
const hovered = getHoveredTweet();
if (hovered){
$(hovered.ele).children().first().click();
}
}
};
});
//
// Block: Allow drag & drop behavior for dropping links on columns to open their detail view.
//
execSafe(function supportDragDropOverColumns(){
const regexTweet = /^https?:\/\/twitter\.com\/[A-Za-z0-9_]+\/status\/(\d+)\/?\??/;
const regexAccount = /^https?:\/\/twitter\.com\/(?!signup$|tos$|privacy$|search$|search-)([^/?]+)\/?$/;
let dragType = false;
const events = {
dragover: function(e){
e.originalEvent.dataTransfer.dropEffect = dragType ? "all" : "none";
e.preventDefault();
e.stopPropagation();
},
drop: function(e){
const url = e.originalEvent.dataTransfer.getData("URL");
if (dragType === "tweet"){
const match = regexTweet.exec(url);
if (match.length === 2){
const column = TD.controller.columnManager.get($(this).attr("data-column"));
if (column){
TD.controller.clients.getPreferredClient().show(match[1], function(chirp){
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp) || chirp);
$(document).trigger("uiGridClearSelection");
}, function(){
alert("error|Could not retrieve the requested tweet.");
});
}
}
}
else if (dragType === "account"){
const match = regexAccount.exec(url);
if (match.length === 2){
$(document).trigger("uiShowProfile", { id: match[1] });
}
}
e.preventDefault();
e.stopPropagation();
}
};
const selectors = {
tweet: "section.js-column",
account: app
};
window.TDGF_onGlobalDragStart = function(type, data){
if (dragType){
app.undelegate(selectors[dragType], events);
dragType = null;
}
if (type === "link"){
dragType = regexTweet.test(data) ? "tweet" : regexAccount.test(data) ? "account": null;
app.delegate(selectors[dragType], events);
}
};
});
//
// Block: Make middle click on tweet reply icon open the compose drawer, retweet icon trigger a quote, and favorite icon open a 'Like from accounts...' modal.
//
execSafe(function supportMiddleClickTweetActions(){
app.delegate(".tweet-action,.tweet-detail-action", "auxclick", function(e){
return if e.which !== 2;
const column = TD.controller.columnManager.get($(this).closest("section.js-column").attr("data-column"));
return if !column;
const ele = $(this).closest("article");
const tweet = column.findChirp(ele.attr("data-tweet-id")) || column.findChirp(ele.attr("data-key"));
return if !tweet;
switch($(this).attr("rel")){
case "reply":
const main = tweet.getMainTweet();
$(document).trigger("uiDockedComposeTweet", {
type: "reply",
from: [ tweet.account.getKey() ],
inReplyTo: {
id: tweet.id,
htmlText: main.htmlText,
user: {
screenName: main.user.screenName,
name: main.user.name,
profileImageURL: main.user.profileImageURL
}
},
mentions: tweet.getReplyUsers(),
element: ele
});
break;
case "favorite":
$(document).trigger("uiShowFavoriteFromOptions", { tweet });
break;
case "retweet":
TD.controller.stats.quoteTweet();
$(document).trigger("uiComposeTweet", {
type: "tweet",
from: [ tweet.account.getKey() ],
quotedTweet: tweet.getMainTweet(),
element: ele // triggers reply-account plugin
});
break;
default:
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
});
//
// Block: Add a pin icon to make tweet compose drawer stay open.
//
execSafe(function setupStayOpenPin(){
$(document).on("tduckOldComposerActive", function(e){
const ele = $(`#import "markup/pin.html"`).appendTo(".js-docked-compose .js-compose-header");
ele.click(function(){
if (TD.settings.getComposeStayOpen()){
ele.css("transform", "rotate(0deg)");
TD.settings.setComposeStayOpen(false);
}
else{
ele.css("transform", "rotate(90deg)");
TD.settings.setComposeStayOpen(true);
}
});
if (TD.settings.getComposeStayOpen()){
ele.css("transform", "rotate(90deg)");
}
});
});
//
// Block: Make submitting search queries while holding Ctrl or middle-clicking the search icon open the search externally.
//
onAppReady.push(function setupSearchTriggerHook(){
const openSearchExternally = function(event, input){
$TD.openBrowser("https://twitter.com/search/?q=" + encodeURIComponent(input.val() || ""));
event.preventDefault();
event.stopPropagation();
input.val("").blur();
app.click(); // unfocus everything
};
$$(".js-app-search-input").keydown(function(e){
(e.ctrlKey && e.keyCode === 13) && openSearchExternally(e, $(this)); // enter
});
$$(".js-perform-search").on("click auxclick", function(e){
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $(".js-app-search-input:visible"));
}).each(function(){
window.TDGF_prioritizeNewestEvent($(this)[0], "click");
});
$$("[data-action='show-search']").on("click auxclick", function(e){
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $());
});
});
//
// Block: Setup video player hooks.
//
execSafe(function setupVideoPlayer(){
const getGifLink = function(ele){
return ele.attr("src") || ele.children("source[video-src]").first().attr("video-src");
};
const getVideoTweetLink = function(obj){
let parent = obj.closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
return link.attr("href");
};
const getUsername = function(tweet){
return tweet && (tweet.quotedTweet || tweet).getMainUser().screenName;
};
app.delegate(".js-gif-play", {
click: function(e){
let src = !e.ctrlKey && getGifLink($(this).closest(".js-media-gif-container").find("video"));
let tweet = getVideoTweetLink($(this));
if (src){
let hovered = getHoveredTweet();
window.TDGF_playVideo(src, tweet, getUsername(hovered && hovered.obj));
}
else{
$TD.openBrowser(tweet);
}
e.stopPropagation();
},
mousedown: function(e){
if (e.button === 1){
e.preventDefault();
}
},
mouseup: function(e){
if (e.button === 1){
$TD.openBrowser(getVideoTweetLink($(this)));
e.preventDefault();
}
}
});
window.TDGF_injectMustache("status/media_thumb.mustache", "append", "is-gif", " is-paused");
TD.mustaches["media/native_video.mustache"] = '<div class="js-media-gif-container media-item nbfc is-video" style="background-image:url({{imageSrc}})"><video class="js-media-gif media-item-gif full-width block {{#isPossiblySensitive}}is-invisible{{/isPossiblySensitive}}" loop src="{{videoUrl}}"></video><a class="js-gif-play pin-all is-actionable">{{> media/video_overlay}}</a></div>';
ensurePropertyExists(TD, "components", "MediaGallery", "prototype", "_loadTweet");
ensurePropertyExists(TD, "components", "BaseModal", "prototype", "setAndShowContainer");
ensurePropertyExists(TD, "ui", "Column", "prototype", "playGifIfNotManuallyPaused");
let cancelModal = false;
TD.components.MediaGallery.prototype._loadTweet = appendToFunction(TD.components.MediaGallery.prototype._loadTweet, function(){
const media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service === "twitter"){
window.TDGF_playVideo(media.chooseVideoVariant().url, this.chirp.getChirpURL(), getUsername(this.chirp));
cancelModal = true;
}
});
TD.components.BaseModal.prototype.setAndShowContainer = prependToFunction(TD.components.BaseModal.prototype.setAndShowContainer, function(){
if (cancelModal){
cancelModal = false;
return true;
}
});
TD.ui.Column.prototype.playGifIfNotManuallyPaused = function(){};
});
//
// Block: Detect and notify about connection issues.
//
(function(){
const onConnectionError = function(){
return if $("#tweetduck-conn-issues").length;
const ele = $(`#import "markup/offline.html"`).appendTo(document.body);
ele.find("button").click(function(){
ele.fadeOut(200);
});
};
const onConnectionFine = function(){
const ele = $("#tweetduck-conn-issues");
ele.fadeOut(200, function(){
ele.remove();
});
};
window.addEventListener("offline", onConnectionError);
window.addEventListener("online", onConnectionFine);
})();

View File

@@ -0,0 +1,279 @@
//
// Functions: Responds to updating $TDX properties.
//
(function(){
let callbacks = [];
window.TDGF_registerPropertyUpdateCallback = function(callback){
callbacks.push(callback);
};
window.TDGF_onPropertiesUpdated = function(){
callbacks.forEach(func => func($TDX));
};
})();
//
// Function: Injects custom HTML into mustache templates.
//
window.TDGF_injectMustache = function(name, operation, search, custom){
let replacement;
switch(operation){
case "replace": replacement = custom; break;
case "append": replacement = search + custom; break;
case "prepend": replacement = custom + search; break;
default: throw "Invalid mustache injection operation. Only 'replace', 'append', 'prepend' are supported.";
}
const prev = TD.mustaches && TD.mustaches[name];
if (!prev){
crashDebug("Mustache injection is referencing an invalid mustache: " + name);
return false;
}
TD.mustaches[name] = prev.replace(search, replacement);
if (prev === TD.mustaches[name]){
crashDebug("Mustache injection had no effect: " + name);
return false;
}
return true;
};
//
// Function: Pushes the newest jQuery event to the beginning of the event handler list.
//
window.TDGF_prioritizeNewestEvent = function(element, event){
const events = $._data(element, "events");
const handlers = events[event];
const newHandler = handlers[handlers.length - 1];
for(let index = handlers.length - 1; index > 0; index--){
handlers[index] = handlers[index - 1];
}
handlers[0] = newHandler;
};
//
// Function: Returns a display name for a column object.
//
window.TDGF_getColumnName = (function(){
const titles = {
"icon-home": "Home",
"icon-mention": "Mentions",
"icon-message": "Messages",
"icon-notifications": "Notifications",
"icon-follow": "Followers",
"icon-activity": "Activity",
"icon-favorite": "Likes",
"icon-user": "User",
"icon-search": "Search",
"icon-list": "List",
"icon-custom-timeline": "Timeline",
"icon-dataminr": "Dataminr",
"icon-play-video": "Live video",
"icon-schedule": "Scheduled"
};
return function(column){
return titles[column._tduck_icon] || "";
};
})();
//
// Function: Adds a search column with the specified query.
//
onAppReady.push(() => execSafe(function setupSearchFunction(){
const context = $._data(document, "events")["uiSearchInputSubmit"][0].handler.context;
window.TDGF_performSearch = function(query){
context.performSearch({ query, tduckResetInput: true });
};
}, function(){
window.TDGF_performSearch = function(){
alert("error|This feature is not available due to an internal error.");
};
}));
//
// Function: Plays sound notification.
//
window.TDGF_playSoundNotification = function(){
document.getElementById("update-sound").play();
};
//
// Function: Plays video using the internal player.
//
window.TDGF_playVideo = function(videoUrl, tweetUrl, username){
return if !videoUrl;
$TD.playVideo(videoUrl, tweetUrl || videoUrl, username || null, function(){
$('<div id="td-video-player-overlay" class="ovl" style="display:block"></div>').on("click contextmenu", function(){
$TD.stopVideo();
}).appendTo(app);
});
};
//
// Function: Shows tweet detail, used in notification context menu.
//
execSafe(function setupShowTweetDetail(){
ensurePropertyExists(TD, "ui", "updates", "showDetailView");
ensurePropertyExists(TD, "controller", "columnManager", "showColumn");
ensurePropertyExists(TD, "controller", "columnManager", "getByApiid");
ensurePropertyExists(TD, "controller", "clients", "getPreferredClient");
const showTweetDetailInternal = function(column, chirp){
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp) || chirp);
TD.controller.columnManager.showColumn(column.model.privateState.key);
$(document).trigger("uiGridClearSelection");
};
window.TDGF_showTweetDetail = function(columnId, chirpId, fallbackUrl){
if (!TD.ready){
onAppReady.push(function(){
window.TDGF_showTweetDetail(columnId, chirpId, fallbackUrl);
});
return;
}
const column = TD.controller.columnManager.getByApiid(columnId);
if (!column){
if (confirm("error|The column which contained the tweet no longer exists. Would you like to open the tweet in your browser instead?")){
$TD.openBrowser(fallbackUrl);
}
return;
}
const chirp = column.findMostInterestingChirp(chirpId);
if (chirp){
showTweetDetailInternal(column, chirp);
}
else{
TD.controller.clients.getPreferredClient().show(chirpId, function(chirp){
showTweetDetailInternal(column, chirp);
}, function(){
if (confirm("error|Could not retrieve the requested tweet. Would you like to open the tweet in your browser instead?")){
$TD.openBrowser(fallbackUrl);
}
});
}
};
}, function(){
window.TDGF_showTweetDetail = function(){
alert("error|This feature is not available due to an internal error.");
};
});
//
// Function: Screenshots tweet to clipboard.
//
execSafe(function setupTweetScreenshot(){
window.TDGF_triggerScreenshot = function(){
const hovered = getHoveredTweet();
return if !hovered;
const columnWidth = $(hovered.column.ele).width();
const tweet = hovered.wrap || hovered.obj;
const html = $(tweet.render({
withFooter: false,
withTweetActions: false,
isInConvo: false,
isFavorite: false,
isRetweeted: false, // keeps retweet mark above tweet
isPossiblySensitive: false,
mediaPreviewSize: hovered.column.obj.getMediaPreviewSize()
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".td-screenshot-remove").remove();
html.find("p.link-complex-target,p.txt-mute").filter(function(){
return $(this).text() === "Show this thread";
}).remove();
html.addClass($(document.documentElement).attr("class"));
html.addClass($(document.body).attr("class"));
html.css("background-color", getClassStyleProperty("stream-item", "background-color"));
html.css("border", "none");
for(let selector of [ ".js-quote-detail", ".js-media-preview-container", ".js-media" ]){
const ele = html.find(selector);
if (ele.length){
ele[0].style.setProperty("margin-bottom", "2px", "important");
break;
}
}
const gif = html.find(".js-media-gif-container");
if (gif.length){
gif.css("background-image", 'url("'+tweet.getMedia()[0].small()+'")');
}
const type = tweet.getChirpType();
if ((type.startsWith("favorite") || type.startsWith("retweet")) && tweet.isAboutYou()){
html.addClass("td-notification-padded");
}
$TD.screenshotTweet(html[0].outerHTML, columnWidth);
};
}, function(){
window.TDGF_triggerScreenshot = function(){
alert("error|This feature is not available due to an internal error.");
};
});
//
// Function: Apply ROT13 to input selection.
//
window.TDGF_applyROT13 = function(){
const ele = document.activeElement;
return if !ele || !ele.value;
const selection = ele.value.substring(ele.selectionStart, ele.selectionEnd);
return if !selection;
document.execCommand("insertText", false, selection.replace(/[a-zA-Z]/g, function(chr){
const code = chr.charCodeAt(0);
const start = code <= 90 ? 65 : 97;
return String.fromCharCode(start + (code - start + 13) % 26);
}));
};
//
// Function: Reloads all columns.
//
if (checkPropertyExists(TD, "controller", "columnManager", "getAll")){
window.TDGF_reloadColumns = function(){
Object.values(TD.controller.columnManager.getAll()).forEach(column => column.reloadTweets());
};
}
else{
window.TDGF_reloadColumns = function(){};
}
//
// Function: Reloads the website with memory cleanup.
//
window.TDGF_reload = function(){
window.gc && window.gc();
window.location.reload();
window.TDGF_reload = function(){}; // redefine to prevent reloading multiple times
};

View File

@@ -0,0 +1,468 @@
//
// Block: Paste images when tweeting.
//
onAppReady.push(function supportImagePaste(){
const uploader = $._data(document, "events")["uiComposeAddImageClick"][0].handler.context;
app.delegate(".js-compose-text,.js-reply-tweetbox,.td-detect-image-paste", "paste", function(e){
for(let item of e.originalEvent.clipboardData.items){
if (item.type.startsWith("image/")){
if (!$(this).closest(".rpl").find(".js-reply-popout").click().length){ // popout direct messages
return if $(".js-add-image-button").is(".is-disabled"); // tweetdeck does not check upload count properly
}
uploader.addFilesToUpload([ item.getAsFile() ]);
break;
}
}
});
});
//
// Block: Allow changing first day of week in date picker.
//
if (checkPropertyExists($, "tools", "dateinput", "conf", "firstDay")){
$.tools.dateinput.conf.firstDay = $TDX.firstDayOfWeek;
onAppReady.push(function setupDatePickerFirstDayCallback(){
window.TDGF_registerPropertyUpdateCallback(function($TDX){
$.tools.dateinput.conf.firstDay = $TDX.firstDayOfWeek;
});
});
}
//
// Block: Override language used for translations.
//
if (checkPropertyExists(TD, "languages", "getSystemLanguageCode")){
const prevFunc = TD.languages.getSystemLanguageCode;
TD.languages.getSystemLanguageCode = function(returnShortCode){
return returnShortCode ? ($TDX.translationTarget || "en") : prevFunc.apply(this, arguments);
};
}
//
// Block: Add missing languages for Bing Translator (Bengali, Icelandic, Tagalog, Tamil, Telugu, Urdu).
//
execSafe(function addMissingTranslationLanguages(){
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: Bypass t.co when clicking/dragging links, media, and in user profiles.
//
execSafe(function setupShortenerBypass(){
$(document.body).delegate("a[data-full-url]", "click auxclick", function(e){
// event.which seems to be borked in auxclick
// tweet links open directly in the column
if ((e.button === 0 || e.button === 1) && $(this).attr("rel") !== "tweet"){
$TD.openBrowser($(this).attr("data-full-url"));
e.preventDefault();
}
});
$(document.body).delegate("a[data-full-url]", "dragstart", function(e){
const url = $(this).attr("data-full-url");
const data = e.originalEvent.dataTransfer;
data.clearData();
data.setData("text/uri-list", url);
data.setData("text/plain", url);
data.setData("text/html", `<a href="${url}">${url}</a>`);
});
if (checkPropertyExists(TD, "services", "TwitterStatus", "prototype", "_generateHTMLText")){
TD.services.TwitterStatus.prototype._generateHTMLText = prependToFunction(TD.services.TwitterStatus.prototype._generateHTMLText, function(){
const card = this.card;
const entities = this.entities;
return if !(card && entities);
const urls = entities.urls;
return if !(urls && urls.length);
const shortUrl = card.url;
const urlObj = entities.urls.find(obj => obj.url === shortUrl && obj.expanded_url);
if (urlObj){
const expandedUrl = urlObj.expanded_url;
card.url = expandedUrl;
const values = card.binding_values;
if (values && values.card_url){
values.card_url.string_value = expandedUrl;
}
}
});
}
if (checkPropertyExists(TD, "services", "TwitterMedia", "prototype", "fromMediaEntity")){
const prevFunc = TD.services.TwitterMedia.prototype.fromMediaEntity;
TD.services.TwitterMedia.prototype.fromMediaEntity = function(){
const obj = prevFunc.apply(this, arguments);
const e = arguments[0];
if (e.expanded_url){
if (obj.url === obj.shortUrl){
obj.shortUrl = e.expanded_url;
}
obj.url = e.expanded_url;
}
return obj;
};
}
if (checkPropertyExists(TD, "services", "TwitterUser", "prototype", "fromJSONObject")){
const prevFunc = TD.services.TwitterUser.prototype.fromJSONObject;
TD.services.TwitterUser.prototype.fromJSONObject = function(){
const obj = prevFunc.apply(this, arguments);
const e = arguments[0].entities;
if (e && e.url && e.url.urls && e.url.urls.length && e.url.urls[0].expanded_url){
obj.url = e.url.urls[0].expanded_url;
}
return obj;
};
}
});
//
// Block: Fix youtu.be previews not showing up for https links.
//
execSafe(function fixYouTubePreviews(){
ensurePropertyExists(TD, "services", "TwitterMedia");
const media = TD.services.TwitterMedia;
ensurePropertyExists(media, "YOUTUBE_TINY_RE");
ensurePropertyExists(media, "YOUTUBE_LONG_RE");
ensurePropertyExists(media, "YOUTUBE_RE");
ensurePropertyExists(media, "SERVICES", "youtube");
media.YOUTUBE_TINY_RE = new RegExp(media.YOUTUBE_TINY_RE.source.replace("http:", "https?:"));
media.YOUTUBE_RE = new RegExp(media.YOUTUBE_LONG_RE.source + "|" + media.YOUTUBE_TINY_RE.source);
media.SERVICES["youtube"] = media.YOUTUBE_RE;
});
//
// Block: Refocus the textbox after switching accounts.
//
onAppReady.push(function setupAccountSwitchRefocus(){
const refocusInput = function(){
document.querySelector(".js-docked-compose .js-compose-text").focus();
};
const accountItemClickEvent = function(e){
setTimeout(refocusInput, 0);
};
$(document).on("tduckOldComposerActive", function(e){
$$(".js-account-list", ".js-docked-compose").delegate(".js-account-item", "click", accountItemClickEvent);
});
});
//
// Block: Fix docked composer not re-focusing after Alt+Tab & image upload.
//
onAppReady.push(function fixDockedComposerRefocus(){
$(document).on("tduckOldComposerActive", function(e){
const ele = $$(".js-compose-text", ".js-docked-compose");
const node = ele[0];
let cancelBlur = false;
ele.on("blur", function(e){
cancelBlur = true;
setTimeout(function(){ cancelBlur = false; }, 0);
});
window.TDGF_prioritizeNewestEvent(node, "blur");
node.blur = prependToFunction(node.blur, function(){
return cancelBlur;
});
});
ensureEventExists(document, "uiComposeImageAdded");
$(document).on("uiComposeImageAdded", function(){
document.querySelector(".js-docked-compose .js-compose-text").focus();
});
});
//
// Block: Fix DM reply input box not getting focused after opening a conversation.
//
if (checkPropertyExists(TD, "components", "ConversationDetailView", "prototype", "showChirp")){
TD.components.ConversationDetailView.prototype.showChirp = appendToFunction(TD.components.ConversationDetailView.prototype.showChirp, function(){
return if !$TDX.focusDmInput;
setTimeout(function(){
document.querySelector(".js-reply-tweetbox").focus();
}, 100);
});
}
//
// Block: Hold Shift to restore cleared column.
//
execSafe(function supportShiftToClearColumn(){
ensurePropertyExists(TD, "vo", "Column", "prototype", "clear");
let holdingShift = false;
const updateShiftState = (pressed) => {
if (pressed != holdingShift){
holdingShift = pressed;
$("button[data-action='clear']").children("span").text(holdingShift ? "Restore" : "Clear");
}
};
const resetActiveFocus = () => {
document.activeElement.blur();
};
document.addEventListener("keydown", function(e){
if (e.shiftKey && (document.activeElement === null || !("value" in document.activeElement))){
updateShiftState(true);
}
});
document.addEventListener("keyup", function(e){
if (!e.shiftKey){
updateShiftState(false);
}
});
TD.vo.Column.prototype.clear = prependToFunction(TD.vo.Column.prototype.clear, function(){
window.setTimeout(resetActiveFocus, 0); // unfocuses the Clear button, otherwise it steals keyboard input
if (holdingShift){
this.model.setClearedTimestamp(0);
this.reloadTweets();
return true;
}
});
});
//
// Block: Make temporary search column appear as the first one and clear the input box.
//
execSafe(function setupSearchColumnHook(){
ensurePropertyExists(TD, "controller", "columnManager", "_columnOrder");
ensurePropertyExists(TD, "controller", "columnManager", "move");
$(document).on("uiSearchNoTemporaryColumn", function(e, data){
if (data.query && data.searchScope !== "users" && !data.columnKey){
if ($TDX.openSearchInFirstColumn){
const order = TD.controller.columnManager._columnOrder;
if (order.length > 1){
const columnKey = order[order.length - 1];
order.splice(order.length - 1, 1);
order.splice(1, 0, columnKey);
TD.controller.columnManager.move(columnKey, "left");
}
}
if (!("tduckResetInput" in data)){
$(".js-app-search-input").val("");
$(".js-perform-search").blur();
}
}
});
});
//
// Block: Reorder search results to move accounts above hashtags.
//
onAppReady.push(function reorderSearchResults(){
const container = $(".js-search-in-popover");
const hashtags = $$(".js-typeahead-topic-list", container);
$$(".js-typeahead-user-list", container).insertBefore(hashtags);
hashtags.addClass("list-divider");
});
//
// Block: Revert Like/Follow dialogs being closed after clicking an action.
//
execSafe(function setupLikeFollowDialogRevert(){
const prevSetTimeout = window.setTimeout;
const overrideState = function(){
return if !$TDX.keepLikeFollowDialogsOpen;
window.setTimeout = function(func, timeout){
return timeout !== 500 && prevSetTimeout.apply(this, arguments);
};
};
const restoreState = function(context, key){
window.setTimeout = prevSetTimeout;
if ($TDX.keepLikeFollowDialogsOpen && key in context.state){
context.state[key] = false;
}
};
$(document).on("uiShowFavoriteFromOptions", function(){
$(".js-btn-fav", ".js-modal-inner").each(function(){
let event = $._data(this, "events").click[0];
let handler = event.handler;
event.handler = function(){
overrideState();
handler.apply(this, arguments);
restoreState($._data(document, "events").dataFavoriteState[0].handler.context, "stopSubsequentLikes");
};
});
});
$(document).on("uiShowFollowFromOptions", function(){
$(".js-component", ".js-modal-inner").each(function(){
let event = $._data(this, "events").click[0];
let handler = event.handler;
let context = handler.context;
event.handler = function(){
overrideState();
handler.apply(this, arguments);
restoreState(context, "stopSubsequentFollows");
};
});
});
});
//
// Block: Fix broken horizontal scrolling of column container when holding Shift.
//
if (checkPropertyExists(TD, "ui", "columns", "setupColumnScrollListeners")){
TD.ui.columns.setupColumnScrollListeners = appendToFunction(TD.ui.columns.setupColumnScrollListeners, function(column){
const ele = document.querySelector(".js-column[data-column='" + column.model.getKey() + "']");
return if ele == null;
$(ele).off("onmousewheel").on("mousewheel", ".scroll-v", function(e){
e.stopImmediatePropagation();
});
window.TDGF_prioritizeNewestEvent(ele, "mousewheel");
});
}
//
// Block: Fix DM image previews and GIF thumbnails not loading due to new URLs.
//
if (checkPropertyExists(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 DMs not being marked as read when replying to them.
//
execSafe(function markRepliedDMsAsRead(){
ensurePropertyExists(TD, "controller", "clients", "getClient");
ensurePropertyExists(TD, "services", "Conversations", "prototype", "getConversation");
$(document).on("dataDmSent", function(e, data){
const client = TD.controller.clients.getClient(data.request.accountKey);
return if !client;
const conversation = client.conversations.getConversation(data.request.conversationId);
return if !conversation;
conversation.markAsRead();
});
});
//
// Block: Limit amount of loaded DMs to avoid massive lag from re-opening them several times.
//
if (checkPropertyExists(TD, "services", "TwitterConversation", "prototype", "renderThread")){
const prevFunc = TD.services.TwitterConversation.prototype.renderThread;
TD.services.TwitterConversation.prototype.renderThread = function(){
const prevMessages = this.messages;
this.messages = prevMessages.slice(0, 100);
const result = prevFunc.apply(this, arguments);
this.messages = prevMessages;
return result;
};
}
//
// Block: Fix scheduled tweets not showing up sometimes.
//
execSafe(function fixScheduledTweets(){
ensurePropertyExists(TD, "controller", "columnManager", "getAll");
ensureEventExists(document, "dataTweetSent");
$(document).on("dataTweetSent", function(e, data){
if (data.response.state && data.response.state === "scheduled"){
const column = Object.values(TD.controller.columnManager.getAll()).find(column => column.model.state.type === "scheduled");
return if !column;
setTimeout(function(){
column.reloadTweets();
}, 1000);
}
});
});
//
// Block: Let's make retweets lowercase again.
//
window.TDGF_injectMustache("status/tweet_single.mustache", "replace", "{{_i}} Retweeted{{/i}}", "{{_i}} retweeted{{/i}}");
if (checkPropertyExists(TD, "services", "TwitterActionRetweet", "prototype", "generateText")){
TD.services.TwitterActionRetweet.prototype.generateText = appendToFunction(TD.services.TwitterActionRetweet.prototype.generateText, function(){
this.text = this.text.replace(" Retweeted", " retweeted");
this.htmlText = this.htmlText.replace(" Retweeted", " retweeted");
});
}
if (checkPropertyExists(TD, "services", "TwitterActionRetweetedInteraction", "prototype", "generateText")){
TD.services.TwitterActionRetweetedInteraction.prototype.generateText = appendToFunction(TD.services.TwitterActionRetweetedInteraction.prototype.generateText, function(){
this.htmlText = this.htmlText.replace(" Retweeted", " retweeted").replace(" Retweet", " retweet");
});
}

View File

@@ -1,22 +1,39 @@
(function($TDP){ (function(){
if (!("$TDP" in window)){
console.error("Missing $TDP");
}
const validatePluginObject = function(pluginObject){
if (!("$token" in pluginObject)){
throw "Invalid plugin object.";
}
};
// //
// Block: Setup a simple JavaScript object configuration loader. // Block: Setup a simple JavaScript object configuration loader.
// //
window.TDPF_loadConfigurationFile = function(pluginObject, fileNameUser, fileNameDefault, onSuccess, onFailure){ window.TDPF_loadConfigurationFile = function(pluginObject, fileNameUser, fileNameDefault, onSuccess, onFailure){
validatePluginObject(pluginObject);
let identifier = pluginObject.$id; let identifier = pluginObject.$id;
let token = pluginObject.$token; let token = pluginObject.$token;
$TDP.checkFileExists(token, fileNameUser).then(exists => { $TDP.checkFileExists(token, fileNameUser).then(exists => {
let fileName = exists ? fileNameUser : fileNameDefault; let fileName = exists ? fileNameUser : fileNameDefault;
if (fileName === null){
onSuccess && onSuccess({});
return;
}
(exists ? $TDP.readFile(token, fileName, true) : $TDP.readFileRoot(token, fileName)).then(contents => { (exists ? $TDP.readFile(token, fileName, true) : $TDP.readFileRoot(token, fileName)).then(contents => {
let obj; let obj;
try{ try{
obj = eval("("+contents+")"); obj = eval("(" + contents + ")");
}catch(err){ }catch(err){
if (!(onFailure && onFailure(err))){ if (!(onFailure && onFailure(err))){
$TD.alert("warning", "Problem loading '"+fileName+"' file for '"+identifier+"' plugin, the JavaScript syntax is invalid: "+err.message); $TD.alert("warning", "Problem loading '" + fileName + "' file for '" + identifier + "' plugin, the JavaScript syntax is invalid: " + err.message);
} }
return; return;
@@ -25,12 +42,12 @@
onSuccess && onSuccess(obj); onSuccess && onSuccess(obj);
}).catch(err => { }).catch(err => {
if (!(onFailure && onFailure(err))){ if (!(onFailure && onFailure(err))){
$TD.alert("warning", "Problem loading '"+fileName+"' file for '"+identifier+"' plugin: "+err.message); $TD.alert("warning", "Problem loading '" + fileName + "' file for '" + identifier + "' plugin: " + err.message);
} }
}); });
}).catch(err => { }).catch(err => {
if (!(onFailure && onFailure(err))){ if (!(onFailure && onFailure(err))){
$TD.alert("warning", "Problem checking '"+fileNameUser+"' file for '"+identifier+"' plugin: "+err.message); $TD.alert("warning", "Problem checking '" + fileNameUser + "' file for '" + identifier + "' plugin: " + err.message);
} }
}); });
}; };
@@ -39,8 +56,10 @@
// Block: Setup a function to add/remove custom CSS. // Block: Setup a function to add/remove custom CSS.
// //
window.TDPF_createCustomStyle = function(pluginObject){ window.TDPF_createCustomStyle = function(pluginObject){
validatePluginObject(pluginObject);
let element = document.createElement("style"); let element = document.createElement("style");
element.id = "plugin-"+pluginObject.$id+"-"+Math.random().toString(36).substring(2, 7); element.id = "plugin-" + pluginObject.$id + "-"+Math.random().toString(36).substring(2, 7);
document.head.appendChild(element); document.head.appendChild(element);
return { return {
@@ -49,4 +68,105 @@
element: element element: element
}; };
}; };
})($TDP);
//
// Block: Setup a function to mimic a Storage object that will be saved in the plugin.
//
window.TDPF_createStorage = function(pluginObject, onReady){
validatePluginObject(pluginObject);
if ("$storage" in pluginObject){
if (pluginObject.$storage !== null){ // set to null while the file is still loading
onReady(pluginObject.$storage);
}
return;
}
class Storage{
get length(){
return Object.keys(this).length;
}
key(index){
return Object.keys(this)[index];
}
getItem(key){
return this[key] || null;
}
setItem(key, value){
this[key] = value;
updateFile();
}
removeItem(key){
delete this[key];
updateFile();
}
clear(){
for(key of Object.keys(this)){
delete this[key];
}
updateFile();
}
replace(obj, silent){
for(let key of Object.keys(this)){
delete this[key];
}
for(let key in obj){
this[key] = obj[key];
}
if (!silent){
updateFile();
}
}
};
var storage = new Proxy(new Storage(), {
get: function(obj, prop, receiver){
const value = obj[prop];
return typeof value === "function" ? value.bind(obj) : value;
},
set: function(obj, prop, value){
obj.setItem(prop, value);
return true;
},
deleteProperty: function(obj, prop){
obj.removeItem(prop);
return true;
},
enumerate: function(obj){
return Object.keys(obj);
}
});
var delay = -1;
const updateFile = function(){
window.clearTimeout(delay);
delay = window.setTimeout(function(){
$TDP.writeFile(pluginObject.$token, ".storage", JSON.stringify(storage));
}, 0);
};
pluginObject.$storage = null;
window.TDPF_loadConfigurationFile(pluginObject, ".storage", null, function(obj){
storage.replace(obj, true);
onReady(pluginObject.$storage = storage);
}, function(){
onReady(pluginObject.$storage = storage);
});
};
})();

View File

@@ -1,10 +1,11 @@
(function($, $TD){ (function(){
$(document).one("TD.ready", function(){ $(document).one("TD.ready", function(){
let css = $(`<style>#import "styles/introduction.css"</style>`).appendTo(document.head); const css = $(`<style>#import "styles/introduction.css"</style>`).appendTo(document.head);
let ele = $(`#import "markup/introduction.html"`).appendTo(".js-app"); const ele = $(`#import "markup/introduction.html"`).appendTo(".js-app");
let tdUser = null; let tdUser = null;
let loadTweetDuckUser = (onSuccess, onError) => {
const loadTweetDuckUser = (onSuccess, onError) => {
if (tdUser !== null){ if (tdUser !== null){
onSuccess(tdUser); onSuccess(tdUser);
} }
@@ -28,8 +29,8 @@
}); });
ele.find("button, a.mdl-dismiss").click(function(){ ele.find("button, a.mdl-dismiss").click(function(){
let showGuide = $(this)[0].hasAttribute("data-guide"); const showGuide = $(this)[0].hasAttribute("data-guide");
let allowDataCollection = $("#td-anonymous-data").is(":checked"); const allowDataCollection = $("#td-anonymous-data").is(":checked");
ele.fadeOut(200, function(){ ele.fadeOut(200, function(){
$TD.onIntroductionClosed(showGuide, allowDataCollection); $TD.onIntroductionClosed(showGuide, allowDataCollection);
@@ -38,4 +39,4 @@
}); });
}); });
}); });
})($, $TD); })();

View File

@@ -1,4 +1,4 @@
(function($TD, $TDX){ (function(){
// //
// Variable: Collection of all <a> tags. // Variable: Collection of all <a> tags.
// //
@@ -19,13 +19,13 @@
(function(){ (function(){
const onLinkClick = function(e){ const onLinkClick = function(e){
if (e.button === 0 || e.button === 1){ if (e.button === 0 || e.button === 1){
let ele = e.currentTarget; const ele = e.currentTarget;
$TD.openBrowser(ele.href); $TD.openBrowser(ele.href);
e.preventDefault(); e.preventDefault();
if ($TDX.skipOnLinkClick){ if ($TDX.skipOnLinkClick){
let parentClasses = ele.parentNode.classList; const parentClasses = ele.parentNode.classList;
if (parentClasses.contains("js-tweet-text") || parentClasses.contains("js-quoted-tweet-text") || parentClasses.contains("js-timestamp")){ if (parentClasses.contains("js-tweet-text") || parentClasses.contains("js-quoted-tweet-text") || parentClasses.contains("js-timestamp")){
$TD.loadNextNotification(); $TD.loadNextNotification();
@@ -46,12 +46,12 @@
let tooltipTimer, tooltipDisplayed; let tooltipTimer, tooltipDisplayed;
addEventListener(links, "mouseenter", function(e){ addEventListener(links, "mouseenter", function(e){
let me = e.currentTarget; const me = e.currentTarget;
let url = me.getAttribute("data-full-url"); const url = me.getAttribute("data-full-url");
return if !url; return if !url;
let text = me.textContent; const text = me.textContent;
return if text.charCodeAt(text.length-1) !== 8230 && text.charCodeAt(0) !== 8230; // horizontal ellipsis return if text.charCodeAt(text.length-1) !== 8230 && text.charCodeAt(0) !== 8230; // horizontal ellipsis
if ($TDX.expandLinksOnHover){ if ($TDX.expandLinksOnHover){
@@ -72,7 +72,7 @@
return if !e.currentTarget.hasAttribute("data-full-url"); return if !e.currentTarget.hasAttribute("data-full-url");
if ($TDX.expandLinksOnHover){ if ($TDX.expandLinksOnHover){
let prevText = e.currentTarget.getAttribute("td-prev-text"); const prevText = e.currentTarget.getAttribute("td-prev-text");
if (prevText){ if (prevText){
e.currentTarget.innerHTML = prevText; e.currentTarget.innerHTML = prevText;
@@ -89,7 +89,7 @@
addEventListener(links, "mousemove", function(e){ addEventListener(links, "mousemove", function(e){
if (tooltipDisplayed && (prevMouseX !== e.clientX || prevMouseY !== e.clientY)){ if (tooltipDisplayed && (prevMouseX !== e.clientX || prevMouseY !== e.clientY)){
let url = e.currentTarget.getAttribute("data-full-url"); const url = e.currentTarget.getAttribute("data-full-url");
return if !url; return if !url;
$TD.displayTooltip(url); $TD.displayTooltip(url);
@@ -99,10 +99,46 @@
}); });
})(); })();
//
// Block: Work around broken smooth scrolling.
//
(function(){;
let targetY = 0;
let delay = -1;
let scrolling = false;
window.TDGF_scrollSmoothly = function(delta){
targetY += delta;
if (targetY < 0){
targetY = 0;
}
else if (targetY > document.body.offsetHeight - window.innerHeight){
targetY = document.body.offsetHeight - window.innerHeight;
}
const prevY = window.scrollY;
window.scrollTo({ top: targetY, left: window.scrollX, behavior: "smooth" });
scrolling = true;
const diff = Math.abs(targetY - prevY);
const time = 420 * (Math.log(diff + 510) - 6);
clearTimeout(delay);
delay = setTimeout(function(){ scrolling = false; }, time);
};
window.addEventListener("scroll", function(){
if (!scrolling){
targetY = window.scrollY;
}
});
})();
// //
// Block: Work around clipboard HTML formatting. // Block: Work around clipboard HTML formatting.
// //
document.addEventListener("copy", function(e){ document.addEventListener("copy", function(){
window.setTimeout($TD.fixClipboard, 0); window.setTimeout($TD.fixClipboard, 0);
}); });
@@ -110,7 +146,7 @@
// Block: Setup a handler for 'Show this thread'. // Block: Setup a handler for 'Show this thread'.
// //
(function(){ (function(){
let btn = document.getElementById("tduck-show-thread"); const btn = document.getElementById("tduck-show-thread");
return if !btn; return if !btn;
btn.addEventListener("click", function(){ btn.addEventListener("click", function(){
@@ -145,4 +181,4 @@
// Block: Force a reset of scroll position on every load. // Block: Force a reset of scroll position on every load.
// //
history.scrollRestoration = "manual"; history.scrollRestoration = "manual";
})($TD, $TDX); })();

View File

@@ -1,15 +1,15 @@
(function($TD){ (function($TD){
let ele = document.getElementsByTagName("article")[0]; const ele = document.getElementsByTagName("article")[0];
ele.style.width = "{width}px"; ele.style.width = "{width}px";
ele.style.position = "absolute"; ele.style.position = "absolute";
let contentHeight = ele.offsetHeight; const contentHeight = ele.offsetHeight;
ele.style.position = "static"; ele.style.position = "static";
let avatar = ele.querySelector(".tweet-avatar"); const avatar = ele.querySelector(".tweet-avatar");
let avatarBottom = avatar ? avatar.getBoundingClientRect().bottom : 0; const avatarBottom = avatar ? avatar.getBoundingClientRect().bottom : 0;
$TD.setHeight(Math.floor(Math.max(contentHeight, avatarBottom+9))).then(() => { $TD.setHeight(Math.floor(Math.max(contentHeight, avatarBottom + 9))).then(() => {
let framesLeft = {frames}; // basic render is done in 1 frame, large media take longer let framesLeft = {frames}; // basic render is done in 1 frame, large media take longer
let onNextFrame = function(){ let onNextFrame = function(){

View File

@@ -11,7 +11,7 @@
return; return;
} }
let link = document.createElement("link"); const link = document.createElement("link");
link.rel = "stylesheet"; link.rel = "stylesheet";
link.href = "https://abs.twimg.com/tduck/css"; link.href = "https://abs.twimg.com/tduck/css";
@@ -32,7 +32,7 @@
// //
const triggerWhenExists = function(query, callback){ const triggerWhenExists = function(query, callback){
let id = window.setInterval(function(){ let id = window.setInterval(function(){
let ele = document.querySelector(query); const ele = document.querySelector(query);
if (ele && callback(ele)){ if (ele && callback(ele)){
window.clearInterval(id); window.clearInterval(id);

View File

@@ -1,4 +1,9 @@
(function($TDU){ (function(){
if (!("$TDU" in window)){
console.error("Missing $TDU");
return;
}
// //
// Function: Creates the update notification element. Removes the old one if already exists. // Function: Creates the update notification element. Removes the old one if already exists.
// //
@@ -32,7 +37,7 @@
// notification // notification
let ele = document.getElementById("tweetduck-update"); let ele = document.getElementById("tweetduck-update");
let existed = !!ele; const existed = !!ele;
if (existed){ if (existed){
ele.remove(); ele.remove();
@@ -136,10 +141,11 @@
}; };
try{ try{
throw false if !($._data(document, "events").TD.some(obj => obj.namespace === "ready")); throw "Missing jQuery or TD.ready event" if !($._data(document, "events").TD.some(obj => obj.namespace === "ready"));
$(document).one("TD.ready", triggerCheck); $(document).one("TD.ready", triggerCheck);
}catch(err){ }catch(err){
console.warn(err);
setTimeout(triggerCheck, 500); setTimeout(triggerCheck, 500);
} }
@@ -147,4 +153,4 @@
// Block: Setup global functions. // Block: Setup global functions.
// //
window.TDUF_displayNotification = displayNotification; window.TDUF_displayNotification = displayNotification;
})($TDU); })();

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.81.3.100\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.81.3.100\build\CefSharp.Common.props')" />
<Import Project="packages\cef.redist.x86.81.3.10\build\cef.redist.x86.props" Condition="Exists('packages\cef.redist.x86.81.3.10\build\cef.redist.x86.props')" />
<Import Project="packages\cef.redist.x64.81.3.10\build\cef.redist.x64.props" Condition="Exists('packages\cef.redist.x64.81.3.10\build\cef.redist.x64.props')" />
<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="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="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props')" />
<Import Project="packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props" Condition="Exists('packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props')" />
<Import Project="packages\cef.redist.x64.3.3396.1786\build\cef.redist.x64.props" Condition="Exists('packages\cef.redist.x64.3.3396.1786\build\cef.redist.x64.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -53,15 +53,71 @@
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Version.cs" /> <Compile Include="Application\LockHandler.cs" />
<Compile Include="Configuration\Arguments.cs" /> <Compile Include="Application\SystemHandler.cs" />
<Compile Include="Configuration\ConfigManager.cs" />
<Compile Include="Configuration\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Browser\Adapters\CefScriptExecutor.cs" /> <Compile Include="Browser\Adapters\CefScriptExecutor.cs" />
<Compile Include="Browser\Bridge\PropertyBridge.cs" /> <Compile Include="Browser\Bridge\PropertyBridge.cs" />
<Compile Include="Browser\Bridge\TweetDeckBridge.cs" />
<Compile Include="Browser\Bridge\UpdateBridge.cs" /> <Compile Include="Browser\Bridge\UpdateBridge.cs" />
<Compile Include="Browser\Data\ContextInfo.cs" />
<Compile Include="Browser\Data\ResourceHandlers.cs" />
<Compile Include="Browser\Data\ResourceLink.cs" />
<Compile Include="Browser\Data\WindowState.cs" />
<Compile Include="Browser\Handling\ContextMenuBase.cs" />
<Compile Include="Browser\Handling\ContextMenuBrowser.cs" />
<Compile Include="Browser\Handling\ContextMenuGuide.cs" />
<Compile Include="Browser\Handling\ContextMenuNotification.cs" />
<Compile Include="Browser\Handling\DragHandlerBrowser.cs" />
<Compile Include="Browser\Handling\Filters\ResponseFilterBase.cs" />
<Compile Include="Browser\Handling\Filters\ResponseFilterVendor.cs" />
<Compile Include="Browser\Handling\General\BrowserProcessHandler.cs" />
<Compile Include="Browser\Handling\General\FileDialogHandler.cs" />
<Compile Include="Browser\Handling\General\JavaScriptDialogHandler.cs" />
<Compile Include="Browser\Handling\General\LifeSpanHandler.cs" />
<Compile Include="Browser\Handling\KeyboardHandlerBase.cs" />
<Compile Include="Browser\Handling\KeyboardHandlerBrowser.cs" />
<Compile Include="Browser\Handling\KeyboardHandlerNotification.cs" />
<Compile Include="Browser\Handling\RequestHandlerBase.cs" />
<Compile Include="Browser\Handling\RequestHandlerBrowser.cs" />
<Compile Include="Browser\Handling\ResourceHandlerNotification.cs" />
<Compile Include="Browser\Handling\ResourceRequestHandler.cs" />
<Compile Include="Browser\Handling\ResourceRequestHandlerBase.cs" />
<Compile Include="Browser\Handling\ResourceRequestHandlerBrowser.cs" />
<Compile Include="Browser\Notification\Screenshot\ScreenshotBridge.cs" />
<Compile Include="Browser\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Browser\Notification\SoundNotification.cs" />
<Compile Include="Browser\TweetDeckBrowser.cs" />
<Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\ConfigManager.cs" />
<Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Configuration\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Controls\ControlExtensions.cs" /> <Compile Include="Controls\ControlExtensions.cs" />
<Compile Include="Management\Analytics\AnalyticsFile.cs" />
<Compile Include="Management\Analytics\AnalyticsManager.cs" />
<Compile Include="Management\Analytics\AnalyticsReport.cs" />
<Compile Include="Management\Analytics\AnalyticsReportGenerator.cs" />
<Compile Include="Management\BrowserCache.cs" />
<Compile Include="Management\ClipboardManager.cs" />
<Compile Include="Management\FormManager.cs" />
<Compile Include="Management\ProfileManager.cs" />
<Compile Include="Management\VideoPlayer.cs" />
<Compile Include="Plugins\PluginDispatcher.cs" />
<Compile Include="Plugins\PluginSchemeFactory.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reporter.cs" />
<Compile Include="Resources\ScriptLoader.cs" />
<Compile Include="Resources\ScriptLoaderDebug.cs" />
<Compile Include="Updates\UpdateCheckClient.cs" />
<Compile Include="Updates\UpdateInstaller.cs" />
<Compile Include="Utils\BrowserUtils.cs" />
<Compile Include="Utils\NativeMethods.cs" />
<Compile Include="Utils\TwitterUtils.cs" />
<Compile Include="Utils\WindowsUtils.cs" />
<Compile Include="Version.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controls\FlatButton.cs"> <Compile Include="Controls\FlatButton.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
@@ -74,30 +130,12 @@
<Compile Include="Controls\NumericUpDownEx.cs"> <Compile Include="Controls\NumericUpDownEx.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
<Compile Include="Management\ClipboardManager.cs" />
<Compile Include="Management\FormManager.cs" />
<Compile Include="Browser\Handling\ContextMenuGuide.cs" />
<Compile Include="Browser\Handling\DragHandlerBrowser.cs" />
<Compile Include="Browser\Handling\Filters\ResponseFilterBase.cs" />
<Compile Include="Browser\Handling\Filters\ResponseFilterVendor.cs" />
<Compile Include="Browser\Handling\General\BrowserProcessHandler.cs" />
<Compile Include="Browser\Handling\ContextMenuBase.cs" />
<Compile Include="Browser\Handling\ContextMenuBrowser.cs" />
<Compile Include="Browser\FormBrowser.cs"> <Compile Include="Browser\FormBrowser.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Include="Browser\FormBrowser.Designer.cs"> <Compile Include="Browser\FormBrowser.Designer.cs">
<DependentUpon>FormBrowser.cs</DependentUpon> <DependentUpon>FormBrowser.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Browser\Handling\General\FileDialogHandler.cs" />
<Compile Include="Browser\Handling\KeyboardHandlerBase.cs" />
<Compile Include="Browser\Handling\KeyboardHandlerBrowser.cs" />
<Compile Include="Browser\Handling\KeyboardHandlerNotification.cs" />
<Compile Include="Browser\Handling\RequestHandlerBase.cs" />
<Compile Include="Browser\Handling\RequestHandlerBrowser.cs" />
<Compile Include="Browser\Handling\ResourceHandlerFactory.cs" />
<Compile Include="Browser\Handling\ResourceHandlerNotification.cs" />
<Compile Include="Browser\Data\ContextInfo.cs" />
<Compile Include="Browser\Notification\Example\FormNotificationExample.cs"> <Compile Include="Browser\Notification\Example\FormNotificationExample.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -113,20 +151,12 @@
<Compile Include="Browser\Notification\FormNotificationBase.Designer.cs"> <Compile Include="Browser\Notification\FormNotificationBase.Designer.cs">
<DependentUpon>FormNotificationBase.cs</DependentUpon> <DependentUpon>FormNotificationBase.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Browser\Handling\ContextMenuNotification.cs" />
<Compile Include="Browser\Handling\General\JavaScriptDialogHandler.cs" />
<Compile Include="Browser\Handling\General\LifeSpanHandler.cs" />
<Compile Include="Browser\Notification\FormNotificationTweet.cs"> <Compile Include="Browser\Notification\FormNotificationTweet.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Include="Browser\Notification\FormNotificationTweet.Designer.cs"> <Compile Include="Browser\Notification\FormNotificationTweet.Designer.cs">
<DependentUpon>FormNotificationTweet.cs</DependentUpon> <DependentUpon>FormNotificationTweet.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Browser\Notification\SoundNotification.cs" />
<Compile Include="Management\Analytics\AnalyticsFile.cs" />
<Compile Include="Management\Analytics\AnalyticsManager.cs" />
<Compile Include="Management\Analytics\AnalyticsReport.cs" />
<Compile Include="Management\Analytics\AnalyticsReportGenerator.cs" />
<Compile Include="Dialogs\FormAbout.cs"> <Compile Include="Dialogs\FormAbout.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -151,7 +181,6 @@
<Compile Include="Dialogs\FormPlugins.Designer.cs"> <Compile Include="Dialogs\FormPlugins.Designer.cs">
<DependentUpon>FormPlugins.cs</DependentUpon> <DependentUpon>FormPlugins.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Management\VideoPlayer.cs" />
<Compile Include="Dialogs\Settings\DialogSettingsAnalytics.cs"> <Compile Include="Dialogs\Settings\DialogSettingsAnalytics.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -206,9 +235,6 @@
<Compile Include="Dialogs\Settings\TabSettingsTray.Designer.cs"> <Compile Include="Dialogs\Settings\TabSettingsTray.Designer.cs">
<DependentUpon>TabSettingsTray.cs</DependentUpon> <DependentUpon>TabSettingsTray.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Browser\TweetDeckBrowser.cs" />
<Compile Include="Utils\TwitterUtils.cs" />
<Compile Include="Management\ProfileManager.cs" />
<Compile Include="Dialogs\Settings\TabSettingsAdvanced.cs"> <Compile Include="Dialogs\Settings\TabSettingsAdvanced.cs">
<SubType>UserControl</SubType> <SubType>UserControl</SubType>
</Compile> </Compile>
@@ -233,41 +259,29 @@
<Compile Include="Dialogs\Settings\TabSettingsNotifications.Designer.cs"> <Compile Include="Dialogs\Settings\TabSettingsNotifications.Designer.cs">
<DependentUpon>TabSettingsNotifications.cs</DependentUpon> <DependentUpon>TabSettingsNotifications.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Browser\Notification\Screenshot\ScreenshotBridge.cs" />
<Compile Include="Browser\Notification\Screenshot\FormNotificationScreenshotable.cs"> <Compile Include="Browser\Notification\Screenshot\FormNotificationScreenshotable.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Include="Browser\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Browser\Data\ResourceLink.cs" />
<Compile Include="Browser\Data\WindowState.cs" />
<Compile Include="Utils\WindowsUtils.cs" />
<Compile Include="Browser\Bridge\TweetDeckBridge.cs" />
<Compile Include="Dialogs\FormSettings.cs"> <Compile Include="Dialogs\FormSettings.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Include="Dialogs\FormSettings.Designer.cs"> <Compile Include="Dialogs\FormSettings.Designer.cs">
<DependentUpon>FormSettings.cs</DependentUpon> <DependentUpon>FormSettings.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Application\LockHandler.cs" />
<Compile Include="Application\SystemHandler.cs" />
<Compile Include="Plugins\PluginControl.cs"> <Compile Include="Plugins\PluginControl.cs">
<SubType>UserControl</SubType> <SubType>UserControl</SubType>
</Compile> </Compile>
<Compile Include="Plugins\PluginControl.Designer.cs"> <Compile Include="Plugins\PluginControl.Designer.cs">
<DependentUpon>PluginControl.cs</DependentUpon> <DependentUpon>PluginControl.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Plugins\PluginDispatcher.cs" />
<Compile Include="Controls\FlowLayoutPanelNoHScroll.cs"> <Compile Include="Controls\FlowLayoutPanelNoHScroll.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
<Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Properties\Resources.Designer.cs"> <Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon> <DependentUpon>Resources.resx</DependentUpon>
</Compile> </Compile>
<Compile Include="Reporter.cs" />
<Compile Include="Resources\ScriptLoaderDebug.cs" />
<Compile Include="Updates\FormUpdateDownload.cs"> <Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -280,13 +294,6 @@
<Compile Include="Browser\TrayIcon.Designer.cs"> <Compile Include="Browser\TrayIcon.Designer.cs">
<DependentUpon>TrayIcon.cs</DependentUpon> <DependentUpon>TrayIcon.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Management\BrowserCache.cs" />
<Compile Include="Utils\BrowserUtils.cs" />
<Compile Include="Utils\NativeMethods.cs" />
<Compile Include="Updates\UpdateCheckClient.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Resources\ScriptLoader.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5"> <BootstrapperPackage Include="Microsoft.Net.Client.3.5">
@@ -314,12 +321,12 @@
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
<None Include="Resources\Images\avatar.png" /> <None Include="Resources\Images\avatar.png" />
<None Include="Resources\Images\icon.ico" />
<None Include="Resources\Images\icon-tray-muted.ico" />
<None Include="Resources\Images\icon-muted.ico" /> <None Include="Resources\Images\icon-muted.ico" />
<None Include="Resources\Images\icon-small.ico" /> <None Include="Resources\Images\icon-small.ico" />
<None Include="Resources\Images\icon-tray-muted.ico" />
<None Include="Resources\Images\icon-tray-new.ico" /> <None Include="Resources\Images\icon-tray-new.ico" />
<None Include="Resources\Images\icon-tray.ico" /> <None Include="Resources\Images\icon-tray.ico" />
<None Include="Resources\Images\icon.ico" />
<None Include="Resources\Images\spinner.apng" /> <None Include="Resources\Images\spinner.apng" />
<None Include="Resources\Plugins\.debug\.meta" /> <None Include="Resources\Plugins\.debug\.meta" />
<None Include="Resources\Plugins\.debug\browser.js" /> <None Include="Resources\Plugins\.debug\browser.js" />
@@ -346,6 +353,11 @@
<None Include="Resources\PostCefUpdate.ps1" /> <None Include="Resources\PostCefUpdate.ps1" />
<None Include="Resources\Scripts\code.js" /> <None Include="Resources\Scripts\code.js" />
<None Include="Resources\Scripts\imports\markup\introduction.html" /> <None Include="Resources\Scripts\imports\markup\introduction.html" />
<None Include="Resources\Scripts\imports\markup\offline.html" />
<None Include="Resources\Scripts\imports\markup\pin.html" />
<None Include="Resources\Scripts\imports\scripts\browser.features.js" />
<None Include="Resources\Scripts\imports\scripts\browser.globals.js" />
<None Include="Resources\Scripts\imports\scripts\browser.tweaks.js" />
<None Include="Resources\Scripts\imports\scripts\plugins.base.js" /> <None Include="Resources\Scripts\imports\scripts\plugins.base.js" />
<None Include="Resources\Scripts\imports\styles\introduction.css" /> <None Include="Resources\Scripts\imports\styles\introduction.css" />
<None Include="Resources\Scripts\imports\styles\twitter.base.css" /> <None Include="Resources\Scripts\imports\styles\twitter.base.css" />
@@ -360,6 +372,7 @@
<None Include="Resources\Scripts\screenshot.js" /> <None Include="Resources\Scripts\screenshot.js" />
<None Include="Resources\Scripts\styles\browser.css" /> <None Include="Resources\Scripts\styles\browser.css" />
<None Include="Resources\Scripts\styles\notification.css" /> <None Include="Resources\Scripts\styles\notification.css" />
<None Include="Resources\Scripts\styles\twitter.css" />
<None Include="Resources\Scripts\twitter.js" /> <None Include="Resources\Scripts\twitter.js" />
<None Include="Resources\Scripts\update.js" /> <None Include="Resources\Scripts\update.js" />
</ItemGroup> </ItemGroup>
@@ -413,14 +426,14 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
<PropertyGroup> <PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup> </PropertyGroup>
<Error Condition="!Exists('packages\cef.redist.x64.3.3396.1786\build\cef.redist.x64.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.3.3396.1786\build\cef.redist.x64.props'))" />
<Error Condition="!Exists('packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets'))" />
<Error Condition="!Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" /> <Error Condition="!Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
<Error Condition="!Exists('packages\cef.redist.x64.81.3.10\build\cef.redist.x64.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.81.3.10\build\cef.redist.x64.props'))" />
<Error Condition="!Exists('packages\cef.redist.x86.81.3.10\build\cef.redist.x86.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.81.3.10\build\cef.redist.x86.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.81.3.100\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.81.3.100\build\CefSharp.Common.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.81.3.100\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.81.3.100\build\CefSharp.Common.targets'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.props'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.targets'))" />
</Target> </Target>
<Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" /> <Import Project="packages\CefSharp.Common.81.3.100\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.81.3.100\build\CefSharp.Common.targets')" />
<Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" /> <Import Project="packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.targets')" />
</Project> </Project>

View File

@@ -0,0 +1,37 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using TweetDuck.Configuration;
using TweetLib.Core.Utils;
namespace TweetDuck.Updates{
sealed class UpdateInstaller{
public string Path { get; }
public UpdateInstaller(string path){
this.Path = path;
}
public bool Launch(){
// ProgramPath has a trailing backslash
string arguments = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\"" + Program.ProgramPath + "\" /RUNARGS=\"" + Arguments.GetCurrentForInstallerCmd() + "\"" + (Program.IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !Program.IsPortable || !FileUtils.CheckFolderWritePermission(Program.ProgramPath);
try{
using(Process.Start(new ProcessStartInfo{
FileName = Path,
Arguments = arguments,
Verb = runElevated ? "runas" : string.Empty,
ErrorDialog = true
})){
return true;
}
}catch(Win32Exception e) when (e.NativeErrorCode == 0x000004C7){ // operation canceled by the user
return false;
}catch(Exception e){
Program.Reporter.HandleException("Update Installer Error", "Could not launch update installer.", true, e);
return false;
}
}
}
}

View File

@@ -9,6 +9,7 @@ using TweetDuck.Browser;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Dialogs; using TweetDuck.Dialogs;
using TweetDuck.Management; using TweetDuck.Management;
using TweetLib.Core;
using TweetLib.Core.Features.Twitter; using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Utils{ namespace TweetDuck.Utils{
@@ -27,17 +28,7 @@ namespace TweetDuck.Utils{
args["disable-gpu-compositing"] = "1"; args["disable-gpu-compositing"] = "1";
} }
if (Config.EnableSmoothScrolling){ if (!Config.EnableSmoothScrolling){
args["disable-threaded-scrolling"] = "1";
if (args.TryGetValue("disable-features", out string disabledFeatures)){
args["disable-features"] = "TouchpadAndWheelScrollLatching," + disabledFeatures;
}
else{
args["disable-features"] = "TouchpadAndWheelScrollLatching";
}
}
else{
args["disable-smooth-scrolling"] = "1"; args["disable-smooth-scrolling"] = "1";
} }
@@ -80,6 +71,11 @@ namespace TweetDuck.Utils{
}; };
} }
public static void RegisterJsBridge(this IWebBrowser browserControl, string name, object bridge){
CefSharpSettings.LegacyJavascriptBindingEnabled = true;
browserControl.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder);
}
public static void OpenDevToolsCustom(this IWebBrowser browser){ public static void OpenDevToolsCustom(this IWebBrowser browser){
var info = new WindowInfo(); var info = new WindowInfo();
info.SetAsPopup(IntPtr.Zero, "Dev Tools"); info.SetAsPopup(IntPtr.Zero, "Dev Tools");
@@ -105,7 +101,7 @@ namespace TweetDuck.Utils{
string browserPath = Config.BrowserPath; string browserPath = Config.BrowserPath;
if (browserPath == null || !File.Exists(browserPath)){ if (browserPath == null || !File.Exists(browserPath)){
WindowsUtils.OpenAssociatedProgram(url); App.SystemHandler.OpenAssociatedProgram(url);
} }
else{ else{
string quotedUrl = '"' + url + '"'; string quotedUrl = '"' + url + '"';

View File

@@ -9,6 +9,7 @@ using CefSharp;
using TweetDuck.Browser.Data; using TweetDuck.Browser.Data;
using TweetDuck.Dialogs; using TweetDuck.Dialogs;
using TweetDuck.Management; using TweetDuck.Management;
using TweetLib.Core;
using TweetLib.Core.Features.Twitter; using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils; using TweetLib.Core.Utils;
using Cookie = CefSharp.Cookie; using Cookie = CefSharp.Cookie;
@@ -18,7 +19,7 @@ namespace TweetDuck.Utils{
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153); 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 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")); public static readonly ResourceLink LoadingSpinner = new ResourceLink("https://ton.twimg.com/tduck/spinner", ResourceHandlers.ForBytes(Properties.Resources.spinner, "image/apng"));
public static readonly string[] DictionaryWords = { public static readonly string[] DictionaryWords = {
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD" "tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
@@ -44,7 +45,7 @@ namespace TweetDuck.Utils{
string ext = Path.GetExtension(path); string ext = Path.GetExtension(path);
if (ImageUrl.ValidExtensions.Contains(ext)){ if (ImageUrl.ValidExtensions.Contains(ext)){
WindowsUtils.OpenAssociatedProgram(path); App.SystemHandler.OpenAssociatedProgram(path);
} }
else{ else{
FormMessage.Error("Image Download", "Invalid file extension " + ext, FormMessage.OK); FormMessage.Error("Image Download", "Invalid file extension " + ext, FormMessage.OK);

View File

@@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using Microsoft.Win32; using Microsoft.Win32;
@@ -18,24 +16,6 @@ namespace TweetDuck.Utils{
return ver.Major == major && ver.Minor == minor; return ver.Major == major && ver.Minor == minor;
} }
public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
try{
using(Process.Start(new ProcessStartInfo{
FileName = file,
Arguments = arguments,
Verb = runElevated ? "runas" : string.Empty,
ErrorDialog = true
})){
return true;
}
}catch(Win32Exception e) when (e.NativeErrorCode == 0x000004C7){ // operation canceled by the user
return false;
}catch(Exception e){
Program.Reporter.HandleException("Error Opening Program", "Could not open the associated program for " + file, true, e);
return false;
}
}
public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){ public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){
for(int waited = 0; waited < timeoutMillis; waited += timeStepMillis){ for(int waited = 0; waited < timeoutMillis; waited += timeStepMillis){
if (test()){ if (test()){

View File

@@ -6,6 +6,6 @@ using Version = TweetDuck.Version;
namespace TweetDuck{ namespace TweetDuck{
internal static class Version{ internal static class Version{
public const string Tag = "1.18.6"; public const string Tag = "1.19.0.1";
} }
} }

Binary file not shown.

View File

@@ -65,6 +65,7 @@ Type: filesandordirs; Name: "{localappdata}\{#MyAppName}\GPUCache"
[InstallDelete] [InstallDelete]
Type: files; Name: "{app}\CEFSHARP-LICENSE.txt" Type: files; Name: "{app}\CEFSHARP-LICENSE.txt"
Type: files; Name: "{app}\LICENSE.txt" Type: files; Name: "{app}\LICENSE.txt"
Type: files; Name: "{app}\natives_blob.bin"
Type: filesandordirs; Name: "{app}\scripts" Type: filesandordirs; Name: "{app}\scripts"
Type: filesandordirs; Name: "{app}\plugins\official" Type: filesandordirs; Name: "{app}\plugins\official"

View File

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

View File

@@ -0,0 +1,8 @@
using System.Net;
namespace TweetLib.Core.Browser{
public interface IResourceProvider<T>{
T Status(HttpStatusCode code, string message);
T File(byte[] bytes, string extension);
}
}

View File

@@ -47,7 +47,7 @@ namespace TweetLib.Core.Features.Plugins{
return token; return token;
} }
private Plugin? GetPluginFromToken(int token){ internal Plugin? GetPluginFromToken(int token){
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null; return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
} }

View File

@@ -16,14 +16,14 @@ namespace TweetLib.Core.Features.Plugins{
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections; public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
public IPluginConfig Config { get; } public IPluginConfig Config { get; }
public event EventHandler<PluginErrorEventArgs>? Reloaded; public event EventHandler<PluginErrorEventArgs>? Reloaded;
public event EventHandler<PluginErrorEventArgs>? Executed; public event EventHandler<PluginErrorEventArgs>? Executed;
private readonly string pluginFolder; private readonly string pluginFolder;
private readonly string pluginDataFolder; private readonly string pluginDataFolder;
private readonly PluginBridge bridge; internal readonly PluginBridge bridge;
private IScriptExecutor? browserExecutor; private IScriptExecutor? browserExecutor;
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>(); private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();

View File

@@ -0,0 +1,69 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetLib.Core.Features.Plugins{
public sealed class PluginSchemeHandler<T> where T : class{
public const string Name = "tdp";
private readonly IResourceProvider<T> resourceProvider;
private PluginBridge? bridge = null;
public PluginSchemeHandler(IResourceProvider<T> resourceProvider){
this.resourceProvider = resourceProvider;
}
public void Setup(PluginManager plugins){
if (this.bridge != null){
throw new InvalidOperationException("Plugin scheme handler is already setup.");
}
this.bridge = plugins.bridge;
}
public T? Process(string url){
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Name || !int.TryParse(uri.Authority, out var identifier)){
return null;
}
var segments = uri.Segments.Select(segment => segment.TrimEnd('/')).Where(segment => !string.IsNullOrEmpty(segment)).ToArray();
if (segments.Length > 0){
var handler = segments[0] switch{
"root" => DoReadRootFile(identifier, segments),
_ => null
};
if (handler != null){
return handler;
}
}
return resourceProvider.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath);
}
private T? DoReadRootFile(int identifier, string[] segments){
string path = string.Join("/", segments, 1, segments.Length - 1);
Plugin? plugin = bridge?.GetPluginFromToken(identifier);
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(PluginFolder.Root, path);
if (fullPath.Length == 0){
return resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.");
}
try{
return resourceProvider.File(File.ReadAllBytes(fullPath), Path.GetExtension(path));
}catch(FileNotFoundException){
return resourceProvider.Status(HttpStatusCode.NotFound, "File not found.");
}catch(DirectoryNotFoundException){
return resourceProvider.Status(HttpStatusCode.NotFound, "Directory not found.");
}catch(Exception e){
return resourceProvider.Status(HttpStatusCode.InternalServerError, e.Message);
}
}
}
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="cef.redist.x64" version="3.3396.1786" targetFramework="net452" /> <package id="cef.redist.x64" version="81.3.10" targetFramework="net472" />
<package id="cef.redist.x86" version="3.3396.1786" targetFramework="net452" /> <package id="cef.redist.x86" version="81.3.10" targetFramework="net472" />
<package id="CefSharp.Common" version="67.0.0" targetFramework="net452" /> <package id="CefSharp.Common" version="81.3.100" targetFramework="net472" />
<package id="CefSharp.WinForms" version="67.0.0" targetFramework="net452" /> <package id="CefSharp.WinForms" version="81.3.100" targetFramework="net472" />
<package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" /> <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
</packages> </packages>

View File

@@ -22,11 +22,11 @@ namespace TweetDuck.Browser{
Task.Factory.StartNew(() => KillWhenHung(parentId), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() => KillWhenHung(parentId), TaskCreationOptions.LongRunning);
if (FindArg(typePrefix) == "renderer"){ if (FindArg(typePrefix) == "renderer"){
using SubProcess subProcess = new SubProcess(args); using SubProcess subProcess = new SubProcess(null, args);
return subProcess.Run(); return subProcess.Run();
} }
else{ else{
return SubProcess.ExecuteProcess(); return SubProcess.ExecuteProcess(args);
} }
} }

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <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="..\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')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -29,9 +29,13 @@
<StartupObject /> <StartupObject />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="CefSharp.BrowserSubprocess.Core, Version=67.0.0.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=x86"> <Reference Include="CefSharp, Version=81.0.0.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=x86">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\CefSharp.Common.67.0.0\CefSharp\x86\CefSharp.BrowserSubprocess.Core.dll</HintPath> <HintPath>..\packages\CefSharp.Common.81.3.100\CefSharp\x86\CefSharp.dll</HintPath>
</Reference>
<Reference Include="CefSharp.BrowserSubprocess.Core, Version=81.0.0.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=x86">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\CefSharp.Common.81.3.100\CefSharp\x86\CefSharp.BrowserSubprocess.Core.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
</ItemGroup> </ItemGroup>