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

Compare commits

...

39 Commits
1.6.4 ... 1.6.7

Author SHA1 Message Date
6387ab41b3 Delay initial tab selection in the Plugin form until after the window is fully shown 2017-03-16 20:50:57 +01:00
4df16b7f15 Fix 'Reload All' button in Plugins form hiding the panel scrollbar resized 2017-03-16 20:42:18 +01:00
ed387a2873 Add a validity check when opening URLs from the internet and plugins 2017-03-16 18:37:24 +01:00
9e225530a6 Add BrowserUtils.IsValidUrl for http(s)/ftp/mailto url checking with unit tests 2017-03-16 18:36:31 +01:00
7b23686dc6 Remove a mailto TODO comment as it's no longer necessary 2017-03-16 18:02:29 +01:00
4de31453fd Update reply-account plugin to fix a search column issue due to a TweetDeck update 2017-03-16 12:19:39 +01:00
4c59526e39 Minor code refactoring, fix potential event memory leaks 2017-03-14 23:47:30 +01:00
9ec1764194 Update tweet detail screenshot code to work with recent TweetDeck changes 2017-03-13 23:38:50 +01:00
47afa32902 Minor code tweak in update.js to avoid a redeclaration 2017-03-13 22:55:35 +01:00
2a09487b55 Remove non-gendered duplicate emoji 2017-03-13 22:17:18 +01:00
563c856dd3 Rewrite tweet screenshot functionality to use native methods 2017-03-13 21:40:15 +01:00
69ea242408 More refactoring of notifications, cache notification scripts 2017-03-13 16:13:32 +01:00
d6e0e0726f Completely refactor FormNotification into multiple classes 2017-03-13 02:06:31 +01:00
73d460d40a Add a compact skin tone selector to emoji keyboard 2017-03-12 21:30:16 +01:00
1f27d96ac9 Release 1.6.6 2017-03-10 16:52:16 +01:00
93e9f28d69 Make update installer download the portable version for portable installations 2017-03-10 16:52:09 +01:00
ec2e26752a Fix link clicking bug caused by a CefSharp bug 2017-03-10 16:50:41 +01:00
fadd95f3e6 Fix installation path detection via registry in update installer 2017-03-10 16:00:00 +01:00
00acc677e6 Release 1.6.5 2017-03-10 14:34:16 +01:00
1a799881e8 Protect against accessing MainWindowHandle on locking process when already existed 2017-03-10 14:07:09 +01:00
f75677593a Update build tools to remove/ignore .pdb files 2017-03-10 11:37:22 +01:00
19e3bd19f0 Update build guide in readme 2017-03-10 11:23:51 +01:00
85701b0a3c Update CefSharp to 55 2017-03-10 10:59:01 +01:00
014cb18dcb Remove unused 'using' statement from FormUpdateDownload 2017-03-09 20:40:21 +01:00
e71e1c853f Refactor FormBrowser.ReloadBrowser 2017-03-09 20:39:12 +01:00
ee9d9196f5 Rewrite image paste click simulation to use CEF events instead of WinAPI 2017-03-09 19:46:12 +01:00
53c8272e01 Remove decimal point in update download label 2017-03-09 19:13:17 +01:00
7f7b6b1e2a Minor code changes, including InvokeAsyncSafe in a couple more places 2017-03-09 19:08:33 +01:00
405777e0f5 Fix tray restoration code to no longer restore windows of all existing TweetDuck processes
Closes #108
2017-03-09 13:59:37 +01:00
df2b624cb5 Update Program to use TrySleepUntil 2017-03-09 13:47:47 +01:00
8a48d5c2f9 Update LockManager to use TrySleepUntil 2017-03-09 13:35:18 +01:00
c55ee71442 Add WindowsUtils.TrySleepUntil to make timeoutable waiting easier 2017-03-09 13:23:13 +01:00
3f82745f5b Improve main window detection and skip kill if already exited in LockManager 2017-03-09 03:06:47 +01:00
404187a1ae Rewrite tray restoration code to detect deadlocked process and allow killing it 2017-03-09 02:54:19 +01:00
2b7b3f586b Allow LockManager to forcibly kill the process if the attempt to close it times out 2017-03-09 02:52:04 +01:00
04959a3493 Make the update check run at the beginning of each hour instead of each hour after startup 2017-03-09 01:17:03 +01:00
97cf4932ae Move a comment in Program.cs 2017-03-09 00:56:36 +01:00
b0d88a0a37 Add a safeguard to updater to open browser if the update installer is missing 2017-03-09 00:52:12 +01:00
67a2e40622 Ninja fix deadlock when exiting after update 2017-03-08 22:10:06 +01:00
41 changed files with 1121 additions and 739 deletions

View File

@@ -1,7 +1,8 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using TweetDck.Core.Utils;
namespace TweetDck.Configuration{ namespace TweetDck.Configuration{
sealed class LockManager{ sealed class LockManager{
@@ -114,25 +115,45 @@ namespace TweetDck.Configuration{
return result; return result;
} }
public bool CloseLockingProcess(int timeout){ public bool CloseLockingProcess(int closeTimeout, int killTimeout){
if (LockingProcess != null){ if (LockingProcess != null){
LockingProcess.CloseMainWindow(); try{
if (LockingProcess.CloseMainWindow()){
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, closeTimeout, 250);
}
for(int waited = 0; waited < timeout && !LockingProcess.HasExited; waited += 250){ if (!LockingProcess.HasExited){
LockingProcess.Refresh(); LockingProcess.Kill();
Thread.Sleep(250); WindowsUtils.TrySleepUntil(CheckLockingProcessExited, killTimeout, 250);
} }
if (LockingProcess.HasExited){ if (LockingProcess.HasExited){
LockingProcess.Dispose(); LockingProcess.Dispose();
LockingProcess = null; LockingProcess = null;
return true; return true;
}
}catch(Exception ex){
if (ex is InvalidOperationException || ex is Win32Exception){
if (LockingProcess != null){
LockingProcess.Refresh();
bool hasExited = LockingProcess.HasExited;
LockingProcess.Dispose();
return hasExited;
}
}
else throw;
} }
} }
return false; return false;
} }
private bool CheckLockingProcessExited(){
LockingProcess.Refresh();
return LockingProcess.HasExited;
}
// Utility functions // Utility functions
private static void WriteIntToStream(Stream stream, int value){ private static void WriteIntToStream(Stream stream, int value){

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <packages xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<package id="cef.redist.x64" version="3.2785.1486" targetFramework="net452" /> <package id="cef.redist.x64" version="3.2883.1552" targetFramework="net452" xmlns="" />
<package id="cef.redist.x86" version="3.2785.1486" targetFramework="net452" /> <package id="cef.redist.x86" version="3.2883.1552" targetFramework="net452" xmlns="" />
<package id="CefSharp.Common" version="53.0.1" targetFramework="net452" /> <package id="CefSharp.Common" version="55.0.0" targetFramework="net452" xmlns="" />
<package id="CefSharp.WinForms" version="53.0.1" targetFramework="net452" /> <package id="CefSharp.WinForms" version="55.0.0" targetFramework="net452" xmlns="" />
<package id="Microsoft.VC120.CRT.JetBrains" version="12.0.21005.2" targetFramework="net452" /> <package id="Microsoft.VC120.CRT.JetBrains" version="12.0.21005.2" targetFramework="net452" xmlns="" />
</packages> </packages>

View File

@@ -19,21 +19,21 @@ namespace TweetDck.Core.Bridge{
} }
private readonly FormBrowser form; private readonly FormBrowser form;
private readonly FormNotification notification; private readonly FormNotificationMain notification;
public TweetDeckBridge(FormBrowser form, FormNotification notification){ public TweetDeckBridge(FormBrowser form, FormNotificationMain notification){
this.form = form; this.form = form;
this.notification = notification; this.notification = notification;
} }
public void LoadFontSizeClass(string fsClass){ public void LoadFontSizeClass(string fsClass){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
TweetNotification.SetFontSizeClass(fsClass); TweetNotification.SetFontSizeClass(fsClass);
}); });
} }
public void LoadNotificationHeadContents(string headContents){ public void LoadNotificationHeadContents(string headContents){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
TweetNotification.SetHeadTag(headContents); TweetNotification.SetHeadTag(headContents);
}); });
} }
@@ -62,7 +62,7 @@ namespace TweetDck.Core.Bridge{
} }
public void OnTweetPopup(string tweetHtml, string tweetUrl, int tweetCharacters){ public void OnTweetPopup(string tweetHtml, string tweetUrl, int tweetCharacters){
notification.InvokeSafe(() => { notification.InvokeAsyncSafe(() => {
form.OnTweetNotification(); form.OnTweetNotification();
notification.ShowNotification(new TweetNotification(tweetHtml, tweetUrl, tweetCharacters)); notification.ShowNotification(new TweetNotification(tweetHtml, tweetUrl, tweetCharacters));
}); });
@@ -85,7 +85,7 @@ namespace TweetDck.Core.Bridge{
} }
public void LoadNextNotification(){ public void LoadNextNotification(){
notification.InvokeAsyncSafe(notification.FinishCurrentTweet); notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
} }
public void TryPasteImage(){ public void TryPasteImage(){
@@ -109,15 +109,7 @@ namespace TweetDck.Core.Bridge{
} }
public void ClickUploadImage(int offsetX, int offsetY){ public void ClickUploadImage(int offsetX, int offsetY){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => form.TriggerImageUpload(offsetX, offsetY));
Point prevPos = Cursor.Position;
Cursor.Position = form.PointToScreen(new Point(offsetX, offsetY));
NativeMethods.SimulateMouseClick(NativeMethods.MouseButton.Left);
Cursor.Position = prevPos;
form.OnImagePastedFinish();
});
} }
public void ScreenshotTweet(string html, int width, int height){ public void ScreenshotTweet(string html, int width, int height){

View File

@@ -17,6 +17,7 @@ using TweetDck.Core.Bridge;
using TweetDck.Core.Notification; using TweetDck.Core.Notification;
using TweetDck.Core.Notification.Screenshot; using TweetDck.Core.Notification.Screenshot;
using TweetDck.Updates.Events; using TweetDck.Updates.Events;
using System.Diagnostics;
namespace TweetDck.Core{ namespace TweetDck.Core{
sealed partial class FormBrowser : Form{ sealed partial class FormBrowser : Form{
@@ -31,7 +32,7 @@ namespace TweetDck.Core{
private readonly ChromiumWebBrowser browser; private readonly ChromiumWebBrowser browser;
private readonly PluginManager plugins; private readonly PluginManager plugins;
private readonly UpdateHandler updates; private readonly UpdateHandler updates;
private readonly FormNotification notification; private readonly FormNotificationTweet notification;
private FormSettings currentFormSettings; private FormSettings currentFormSettings;
private FormAbout currentFormAbout; private FormAbout currentFormAbout;
@@ -52,12 +53,14 @@ namespace TweetDck.Core{
this.plugins.Reloaded += plugins_Reloaded; this.plugins.Reloaded += plugins_Reloaded;
this.plugins.PluginChangedState += plugins_PluginChangedState; this.plugins.PluginChangedState += plugins_PluginChangedState;
this.notification = CreateNotificationForm(NotificationFlags.AutoHide | NotificationFlags.TopMost); this.notification = new FormNotificationTweet(this, plugins, NotificationFlags.TopMost){
#if DEBUG #if DEBUG
this.notification.CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt; CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt
#else #else
this.notification.CanMoveWindow = () => false; CanMoveWindow = () => false
#endif #endif
};
this.notification.Show(); this.notification.Show();
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){ this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
@@ -230,7 +233,7 @@ namespace TweetDck.Core{
} }
private void plugins_Reloaded(object sender, PluginLoadEventArgs e){ private void plugins_Reloaded(object sender, PluginLoadEventArgs e){
ReloadBrowser(); browser.GetBrowser().Reload();
} }
private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){ private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
@@ -243,6 +246,7 @@ namespace TweetDck.Core{
FormUpdateDownload downloadForm = new FormUpdateDownload(e.UpdateInfo); FormUpdateDownload downloadForm = new FormUpdateDownload(e.UpdateInfo);
downloadForm.MoveToCenter(this); downloadForm.MoveToCenter(this);
downloadForm.ShowDialog(); downloadForm.ShowDialog();
downloadForm.Dispose();
if (downloadForm.UpdateStatus == FormUpdateDownload.Status.Succeeded){ if (downloadForm.UpdateStatus == FormUpdateDownload.Status.Succeeded){
UpdateInstallerPath = downloadForm.InstallerPath; UpdateInstallerPath = downloadForm.InstallerPath;
@@ -263,7 +267,12 @@ namespace TweetDck.Core{
protected override void WndProc(ref Message m){ protected override void WndProc(ref Message m){
if (isLoaded && m.Msg == Program.WindowRestoreMessage){ if (isLoaded && m.Msg == Program.WindowRestoreMessage){
trayIcon_ClickRestore(trayIcon, new EventArgs()); using(Process process = Process.GetCurrentProcess()){
if (process.Id == m.WParam.ToInt32()){
trayIcon_ClickRestore(trayIcon, new EventArgs());
}
}
return; return;
} }
@@ -277,8 +286,8 @@ namespace TweetDck.Core{
// notification helpers // notification helpers
public FormNotification CreateNotificationForm(NotificationFlags flags){ public FormNotificationMain CreateNotificationForm(NotificationFlags flags){
return new FormNotification(this, plugins, flags); return new FormNotificationMain(this, plugins, flags);
} }
public void PauseNotification(){ public void PauseNotification(){
@@ -399,16 +408,14 @@ namespace TweetDck.Core{
browser.ExecuteScriptAsync("TDGF_tryPasteImage()"); browser.ExecuteScriptAsync("TDGF_tryPasteImage()");
} }
public void OnImagePastedFinish(){ public void TriggerImageUpload(int offsetX, int offsetY){
browser.ExecuteScriptAsync("TDGF_tryPasteImageFinish()"); IBrowserHost host = browser.GetBrowser().GetHost();
host.SendMouseClickEvent(offsetX, offsetY, MouseButtonType.Left, false, 1, CefEventFlags.None);
host.SendMouseClickEvent(offsetX, offsetY, MouseButtonType.Left, true, 1, CefEventFlags.None);
} }
public void TriggerTweetScreenshot(){ public void TriggerTweetScreenshot(){
browser.ExecuteScriptAsync("TDGF_triggerScreenshot()"); browser.ExecuteScriptAsync("TDGF_triggerScreenshot()");
} }
public void ReloadBrowser(){
browser.ExecuteScriptAsync("window.location.reload()");
}
} }
} }

View File

@@ -1,443 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDck.Configuration;
using TweetDck.Core.Bridge;
using TweetDck.Core.Handling;
using TweetDck.Resources;
using TweetDck.Core.Utils;
using TweetDck.Plugins;
using TweetDck.Plugins.Enums;
using TweetDck.Core.Controls;
using TweetDck.Core.Notification;
namespace TweetDck.Core{
partial class FormNotification : Form{
private const string NotificationScriptFile = "notification.js";
private static readonly string NotificationScriptIdentifier = ScriptLoader.GetRootIdentifier(NotificationScriptFile);
private static readonly string PluginScriptIdentifier = ScriptLoader.GetRootIdentifier(PluginManager.PluginNotificationScriptFile);
public Func<bool> CanMoveWindow = () => true;
public bool IsNotificationVisible{
get{
return Location != ControlExtensions.InvisibleLocation;
}
}
public new Point Location{
get{
return base.Location;
}
set{
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
}
}
private readonly Control owner;
private readonly PluginManager plugins;
protected readonly NotificationFlags flags;
protected readonly ChromiumWebBrowser browser;
private readonly Queue<TweetNotification> tweetQueue = new Queue<TweetNotification>(4);
private int timeLeft, totalTime;
private readonly NativeMethods.HookProc mouseHookDelegate;
private IntPtr mouseHook;
private bool? prevDisplayTimer;
private int? prevFontSize;
private bool RequiresResize{
get{
return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Program.UserConfig.DisplayNotificationTimer || prevFontSize != TweetNotification.FontSizeLevel;
}
set{
if (value){
prevDisplayTimer = null;
prevFontSize = null;
}
else{
prevDisplayTimer = Program.UserConfig.DisplayNotificationTimer;
prevFontSize = TweetNotification.FontSizeLevel;
}
}
}
private readonly string notificationJS;
private readonly string pluginJS;
protected override bool ShowWithoutActivation{
get{
return true;
}
}
public bool FreezeTimer { get; set; }
public bool ContextMenuOpen { get; set; }
public string CurrentUrl { get; private set; }
public string CurrentQuotedTweetUrl { get; set; }
public EventHandler Initialized;
private int pauseCounter;
private bool pausedDuringNotification;
public bool IsPaused{
get{
return pauseCounter > 0;
}
}
private static int BaseClientWidth{
get{
int level = TweetNotification.FontSizeLevel;
return level == 0 ? 284 : (int)Math.Round(284.0*(1.0+0.05*level));
}
}
private static int BaseClientHeight{
get{
int level = TweetNotification.FontSizeLevel;
return level == 0 ? 118 : (int)Math.Round(118.0*(1.0+0.075*level));
}
}
public FormNotification(FormBrowser owner, PluginManager pluginManager, NotificationFlags flags){
InitializeComponent();
this.owner = owner;
this.plugins = pluginManager;
this.flags = flags;
owner.FormClosed += (sender, args) => Close();
browser = new ChromiumWebBrowser("about:blank"){
MenuHandler = new ContextMenuNotification(this, !flags.HasFlag(NotificationFlags.DisableContextMenu)),
LifeSpanHandler = new LifeSpanHandler()
};
#if DEBUG
browser.ConsoleMessage += BrowserUtils.HandleConsoleMessage;
#endif
browser.IsBrowserInitializedChanged += Browser_IsBrowserInitializedChanged;
browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd;
if (!flags.HasFlag(NotificationFlags.DisableScripts)){
notificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge(owner, this));
if (plugins != null){
pluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile);
browser.RegisterAsyncJsObject("$TDP", plugins.Bridge);
}
}
panelBrowser.Controls.Add(browser);
if (flags.HasFlag(NotificationFlags.AutoHide)){
Program.UserConfig.MuteToggled += Config_MuteToggled;
Disposed += (sender, args) => Program.UserConfig.MuteToggled -= Config_MuteToggled;
if (Program.UserConfig.MuteNotifications){
PauseNotification();
}
}
mouseHookDelegate = MouseHookProc;
Disposed += FormNotification_Disposed;
}
protected override void WndProc(ref Message m){
if (m.Msg == 0x0112 && (m.WParam.ToInt32() & 0xFFF0) == 0xF010 && !CanMoveWindow()){ // WM_SYSCOMMAND, SC_MOVE
return;
}
base.WndProc(ref m);
}
// mouse wheel hook
private void StartMouseHook(){
if (mouseHook == IntPtr.Zero){
mouseHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, mouseHookDelegate, IntPtr.Zero, 0);
}
}
private void StopMouseHook(){
if (mouseHook != IntPtr.Zero){
NativeMethods.UnhookWindowsHookEx(mouseHook);
mouseHook = IntPtr.Zero;
}
}
private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam){
if (!ContainsFocus && wParam.ToInt32() == NativeMethods.WH_MOUSEWHEEL && browser.Bounds.Contains(PointToClient(Cursor.Position))){
// fuck it, Activate() doesn't work with this
Point prevPos = Cursor.Position;
Cursor.Position = PointToScreen(new Point(0, -1));
NativeMethods.SimulateMouseClick(NativeMethods.MouseButton.Left);
Cursor.Position = prevPos;
}
return NativeMethods.CallNextHookEx(mouseHook, nCode, wParam, lParam);
}
// event handlers
private void timerDisplayDelay_Tick(object sender, EventArgs e){
OnNotificationReady();
timerDisplayDelay.Stop();
}
private void timerHideProgress_Tick(object sender, EventArgs e){
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen)return;
timeLeft -= timerProgress.Interval;
int value = (int)Math.Round(1025.0*(totalTime-timeLeft)/totalTime);
progressBarTimer.SetValueInstant(Math.Min(1000, Math.Max(0, Program.UserConfig.NotificationTimerCountDown ? 1000-value : value)));
if (timeLeft <= 0){
FinishCurrentTweet();
}
}
private void Config_MuteToggled(object sender, EventArgs e){
if (Program.UserConfig.MuteNotifications){
PauseNotification();
}
else{
ResumeNotification();
}
}
private void Browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e){
if (e.IsBrowserInitialized && Initialized != null){
Initialized(this, new EventArgs());
}
}
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
if (!e.IsLoading && browser.Address != "about:blank" && !flags.HasFlag(NotificationFlags.ManualDisplay)){
this.InvokeSafe(() => {
Visible = true; // ensures repaint before moving the window to a visible location
timerDisplayDelay.Start();
});
}
}
private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && notificationJS != null && browser.Address != "about:blank" && !flags.HasFlag(NotificationFlags.DisableScripts)){
e.Frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Properties.ExpandLinksOnHover));
ScriptLoader.ExecuteScript(e.Frame, notificationJS, NotificationScriptIdentifier);
if (plugins != null && plugins.HasAnyPlugin(PluginEnvironment.Notification)){
ScriptLoader.ExecuteScript(e.Frame, pluginJS, PluginScriptIdentifier);
ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginGlobalScriptFile);
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Notification, false);
}
}
}
private void FormNotification_FormClosing(object sender, FormClosingEventArgs e){
if (e.CloseReason == CloseReason.UserClosing){
HideNotification(false);
tweetQueue.Clear();
e.Cancel = true;
}
}
private void FormNotification_Disposed(object sender, EventArgs e){
browser.Dispose();
StopMouseHook();
}
// notification methods
public void ShowNotification(TweetNotification notification){
if (IsPaused){
tweetQueue.Enqueue(notification);
}
else{
tweetQueue.Enqueue(notification);
UpdateTitle();
if (totalTime == 0){
LoadNextNotification();
}
}
}
public void ShowNotificationForSettings(bool reset){
if (reset){
LoadTweet(TweetNotification.ExampleTweet);
}
else{
PrepareAndDisplayWindow();
}
}
public void HideNotification(bool loadBlank){
if (loadBlank){
browser.LoadHtml("", "about:blank");
}
Location = ControlExtensions.InvisibleLocation;
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? 1000 : 0;
timerProgress.Stop();
totalTime = 0;
StopMouseHook();
}
public void FinishCurrentTweet(){
if (tweetQueue.Count > 0){
LoadNextNotification();
}
else if (flags.HasFlag(NotificationFlags.AutoHide)){
HideNotification(true);
}
else{
timerProgress.Stop();
}
}
public void PauseNotification(){
if (pauseCounter++ == 0){
pausedDuringNotification = IsNotificationVisible;
if (IsNotificationVisible){
Location = ControlExtensions.InvisibleLocation;
timerProgress.Stop();
StopMouseHook();
}
}
}
public void ResumeNotification(){
if (pauseCounter > 0 && --pauseCounter == 0){
if (pausedDuringNotification){
OnNotificationReady();
}
else if (tweetQueue.Count > 0){
LoadNextNotification();
}
}
}
private void LoadNextNotification(){
LoadTweet(tweetQueue.Dequeue());
}
private void LoadTweet(TweetNotification tweet){
CurrentUrl = tweet.Url;
CurrentQuotedTweetUrl = string.Empty; // load from JS
timerProgress.Stop();
totalTime = timeLeft = tweet.GetDisplayDuration(Program.UserConfig.NotificationDurationValue);
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? 1000 : 0;
string bodyClasses = browser.Bounds.Contains(PointToClient(Cursor.Position)) ? "td-hover" : string.Empty;
browser.LoadHtml(tweet.GenerateHtml(bodyClasses), "http://tweetdeck.twitter.com/?"+DateTime.Now.Ticks);
}
private void PrepareAndDisplayWindow(){
if (RequiresResize){
RequiresResize = false;
SetNotificationSize(BaseClientWidth, BaseClientHeight, Program.UserConfig.DisplayNotificationTimer);
}
MoveToVisibleLocation();
StartMouseHook();
}
protected void SetNotificationSize(int width, int height, bool displayTimer){
if (displayTimer){
ClientSize = new Size(width, height+4);
progressBarTimer.Visible = true;
}
else{
ClientSize = new Size(width, height);
progressBarTimer.Visible = false;
}
panelBrowser.Height = height;
}
protected void MoveToVisibleLocation(){
UserConfig config = Program.UserConfig;
Screen screen = Screen.FromControl(owner);
if (config.NotificationDisplay > 0 && config.NotificationDisplay <= Screen.AllScreens.Length){
screen = Screen.AllScreens[config.NotificationDisplay-1];
}
bool needsReactivating = Location == ControlExtensions.InvisibleLocation;
int edgeDist = config.NotificationEdgeDistance;
switch(config.NotificationPosition){
case TweetNotification.Position.TopLeft:
Location = new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+edgeDist);
break;
case TweetNotification.Position.TopRight:
Location = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
break;
case TweetNotification.Position.BottomLeft:
Location = new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
break;
case TweetNotification.Position.BottomRight:
Location = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
break;
case TweetNotification.Position.Custom:
if (!config.IsCustomNotificationPositionSet){
config.CustomNotificationPosition = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
config.Save();
}
Location = config.CustomNotificationPosition;
break;
}
if (needsReactivating && flags.HasFlag(NotificationFlags.TopMost)){
NativeMethods.SetFormPos(this, NativeMethods.HWND_TOPMOST, NativeMethods.SWP_NOACTIVATE);
}
}
protected void UpdateTitle(){
Text = tweetQueue.Count > 0 ? Program.BrandName+" ("+tweetQueue.Count+" more left)" : Program.BrandName;
}
protected void OnNotificationReady(){
UpdateTitle();
PrepareAndDisplayWindow();
timerProgress.Start();
}
public void DisplayTooltip(string text){
if (string.IsNullOrEmpty(text)){
toolTip.Hide(this);
}
else{
Point position = PointToClient(Cursor.Position);
position.Offset(20, 5);
toolTip.Show(text, this, position); // TODO figure out flickering when moving the mouse
}
}
}
}

View File

@@ -1,5 +1,6 @@
using CefSharp; using CefSharp;
using TweetDck.Core.Controls; using TweetDck.Core.Controls;
using TweetDck.Core.Notification;
namespace TweetDck.Core.Handling{ namespace TweetDck.Core.Handling{
class ContextMenuNotification : ContextMenuBase{ class ContextMenuNotification : ContextMenuBase{
@@ -8,10 +9,10 @@ namespace TweetDck.Core.Handling{
private const int MenuCopyTweetUrl = 26602; private const int MenuCopyTweetUrl = 26602;
private const int MenuCopyQuotedTweetUrl = 26603; private const int MenuCopyQuotedTweetUrl = 26603;
private readonly FormNotification form; private readonly FormNotificationBase form;
private readonly bool enableCustomMenu; private readonly bool enableCustomMenu;
public ContextMenuNotification(FormNotification form, bool enableCustomMenu) : base(form){ public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu) : base(form){
this.form = form; this.form = form;
this.enableCustomMenu = enableCustomMenu; this.enableCustomMenu = enableCustomMenu;
} }
@@ -59,7 +60,7 @@ namespace TweetDck.Core.Handling{
switch((int)commandId){ switch((int)commandId){
case MenuSkipTweet: case MenuSkipTweet:
form.InvokeAsyncSafe(form.FinishCurrentTweet); form.InvokeAsyncSafe(form.FinishCurrentNotification);
return true; return true;
case MenuFreeze: case MenuFreeze:

View File

@@ -7,10 +7,11 @@ namespace TweetDck.Core.Handling{
newBrowser = null; newBrowser = null;
switch(targetDisposition){ switch(targetDisposition){
case WindowOpenDisposition.SingletonTab: // TODO remove when CefSharp is updated to 57; enums don't line up in 55
case WindowOpenDisposition.NewBackgroundTab: case WindowOpenDisposition.NewBackgroundTab:
case WindowOpenDisposition.NewForegroundTab: case WindowOpenDisposition.NewForegroundTab:
case WindowOpenDisposition.NewPopup: case WindowOpenDisposition.NewPopup:
case WindowOpenDisposition.NewWindow: // TODO case WindowOpenDisposition.NewWindow:
BrowserUtils.OpenExternalBrowser(targetUrl); BrowserUtils.OpenExternalBrowser(targetUrl);
return true; return true;

View File

@@ -0,0 +1,66 @@
namespace TweetDck.Core.Notification {
partial class FormNotificationBase {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.panelBrowser = new System.Windows.Forms.Panel();
this.toolTip = new System.Windows.Forms.ToolTip(this.components);
this.SuspendLayout();
//
// panelBrowser
//
this.panelBrowser.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.panelBrowser.BackColor = System.Drawing.Color.White;
this.panelBrowser.Location = new System.Drawing.Point(0, 0);
this.panelBrowser.Margin = new System.Windows.Forms.Padding(0);
this.panelBrowser.Name = "panelBrowser";
this.panelBrowser.Size = new System.Drawing.Size(284, 122);
this.panelBrowser.TabIndex = 0;
//
// FormNotification
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.SystemColors.Control;
this.ClientSize = new System.Drawing.Size(284, 122);
this.Controls.Add(this.panelBrowser);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Location = TweetDck.Core.Controls.ControlExtensions.InvisibleLocation;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "FormNotification";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.ResumeLayout(false);
}
#endregion
protected System.Windows.Forms.Panel panelBrowser;
private System.Windows.Forms.ToolTip toolTip;
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDck.Configuration;
using TweetDck.Core.Controls;
using TweetDck.Core.Handling;
using TweetDck.Core.Utils;
namespace TweetDck.Core.Notification{
partial class FormNotificationBase : Form{
public bool IsNotificationVisible{
get{
return Location != ControlExtensions.InvisibleLocation;
}
}
public new Point Location{
get{
return base.Location;
}
set{
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
}
}
public Func<bool> CanMoveWindow = () => true;
private readonly Control owner;
protected readonly NotificationFlags flags;
protected readonly ChromiumWebBrowser browser;
private int pauseCounter;
public bool IsPaused{
get{
return pauseCounter > 0;
}
}
protected override bool ShowWithoutActivation{
get{
return true;
}
}
public bool FreezeTimer { get; set; }
public bool ContextMenuOpen { get; set; }
public string CurrentUrl { get; private set; }
public string CurrentQuotedTweetUrl { get; set; }
public event EventHandler Initialized;
public FormNotificationBase(Form owner, NotificationFlags flags){
InitializeComponent();
this.owner = owner;
this.flags = flags;
owner.FormClosed += owner_FormClosed;
browser = new ChromiumWebBrowser("about:blank"){
MenuHandler = new ContextMenuNotification(this, !flags.HasFlag(NotificationFlags.DisableContextMenu)),
LifeSpanHandler = new LifeSpanHandler()
};
#if DEBUG
browser.ConsoleMessage += BrowserUtils.HandleConsoleMessage;
#endif
browser.IsBrowserInitializedChanged += Browser_IsBrowserInitializedChanged;
panelBrowser.Controls.Add(browser);
Disposed += (sender, args) => {
browser.Dispose();
owner.FormClosed -= owner_FormClosed;
};
// ReSharper disable once VirtualMemberCallInContructor
UpdateTitle();
}
protected override void WndProc(ref Message m){
if (m.Msg == 0x0112 && (m.WParam.ToInt32() & 0xFFF0) == 0xF010 && !CanMoveWindow()){ // WM_SYSCOMMAND, SC_MOVE
return;
}
base.WndProc(ref m);
}
// event handlers
private void owner_FormClosed(object sender, FormClosedEventArgs e){
Close();
}
private void Browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e){
if (e.IsBrowserInitialized && Initialized != null){
Initialized(this, new EventArgs());
}
}
// notification methods
public virtual void HideNotification(bool loadBlank){
if (loadBlank){
browser.LoadHtml("", "about:blank");
}
Location = ControlExtensions.InvisibleLocation;
}
public virtual void FinishCurrentNotification(){}
public virtual void PauseNotification(){
if (pauseCounter++ == 0 && IsNotificationVisible){
Location = ControlExtensions.InvisibleLocation;
}
}
public virtual void ResumeNotification(){
if (pauseCounter > 0){
--pauseCounter;
}
}
protected virtual void LoadTweet(TweetNotification tweet){
CurrentUrl = tweet.Url;
CurrentQuotedTweetUrl = string.Empty; // load from JS
string bodyClasses = browser.Bounds.Contains(PointToClient(Cursor.Position)) ? "td-hover" : string.Empty;
browser.LoadHtml(tweet.GenerateHtml(bodyClasses), "http://tweetdeck.twitter.com/?"+DateTime.Now.Ticks);
}
protected virtual void SetNotificationSize(int width, int height){
ClientSize = new Size(width, height);
panelBrowser.Height = height;
}
protected void MoveToVisibleLocation(){
UserConfig config = Program.UserConfig;
Screen screen = Screen.FromControl(owner);
if (config.NotificationDisplay > 0 && config.NotificationDisplay <= Screen.AllScreens.Length){
screen = Screen.AllScreens[config.NotificationDisplay-1];
}
bool needsReactivating = Location == ControlExtensions.InvisibleLocation;
int edgeDist = config.NotificationEdgeDistance;
switch(config.NotificationPosition){
case TweetNotification.Position.TopLeft:
Location = new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+edgeDist);
break;
case TweetNotification.Position.TopRight:
Location = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
break;
case TweetNotification.Position.BottomLeft:
Location = new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
break;
case TweetNotification.Position.BottomRight:
Location = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
break;
case TweetNotification.Position.Custom:
if (!config.IsCustomNotificationPositionSet){
config.CustomNotificationPosition = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
config.Save();
}
Location = config.CustomNotificationPosition;
break;
}
if (needsReactivating && flags.HasFlag(NotificationFlags.TopMost)){
NativeMethods.SetFormPos(this, NativeMethods.HWND_TOPMOST, NativeMethods.SWP_NOACTIVATE);
}
}
protected virtual void OnNotificationReady(){
MoveToVisibleLocation();
}
protected virtual void UpdateTitle(){
Text = Program.BrandName;
}
public void DisplayTooltip(string text){
if (string.IsNullOrEmpty(text)){
toolTip.Hide(this);
}
else{
Point position = PointToClient(Cursor.Position);
position.Offset(20, 5);
toolTip.Show(text, this, position); // TODO figure out flickering when moving the mouse
}
}
}
}

View File

@@ -1,7 +1,5 @@
using TweetDck.Core.Controls; namespace TweetDck.Core.Notification {
partial class FormNotificationMain {
namespace TweetDck.Core {
partial class FormNotification {
/// <summary> /// <summary>
/// Required designer variable. /// Required designer variable.
/// </summary> /// </summary>
@@ -26,23 +24,15 @@ namespace TweetDck.Core {
/// </summary> /// </summary>
private void InitializeComponent() { private void InitializeComponent() {
this.components = new System.ComponentModel.Container(); this.components = new System.ComponentModel.Container();
this.panelBrowser = new System.Windows.Forms.Panel(); this.timerDisplayDelay = new System.Windows.Forms.Timer(this.components);
this.timerProgress = new System.Windows.Forms.Timer(this.components); this.timerProgress = new System.Windows.Forms.Timer(this.components);
this.progressBarTimer = new TweetDck.Core.Controls.FlatProgressBar(); this.progressBarTimer = new TweetDck.Core.Controls.FlatProgressBar();
this.toolTip = new System.Windows.Forms.ToolTip(this.components);
this.timerDisplayDelay = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout(); this.SuspendLayout();
// //
// panelBrowser // timerDisplayDelay
// //
this.panelBrowser.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) this.timerDisplayDelay.Interval = 17;
| System.Windows.Forms.AnchorStyles.Right))); this.timerDisplayDelay.Tick += new System.EventHandler(this.timerDisplayDelay_Tick);
this.panelBrowser.BackColor = System.Drawing.Color.White;
this.panelBrowser.Location = new System.Drawing.Point(0, 0);
this.panelBrowser.Margin = new System.Windows.Forms.Padding(0);
this.panelBrowser.Name = "panelBrowser";
this.panelBrowser.Size = new System.Drawing.Size(284, 118);
this.panelBrowser.TabIndex = 0;
// //
// timerProgress // timerProgress
// //
@@ -62,11 +52,6 @@ namespace TweetDck.Core {
this.progressBarTimer.Size = new System.Drawing.Size(284, 4); this.progressBarTimer.Size = new System.Drawing.Size(284, 4);
this.progressBarTimer.TabIndex = 1; this.progressBarTimer.TabIndex = 1;
// //
// timerDisplayDelay
//
this.timerDisplayDelay.Interval = 17;
this.timerDisplayDelay.Tick += new System.EventHandler(this.timerDisplayDelay_Tick);
//
// FormNotification // FormNotification
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -74,15 +59,6 @@ namespace TweetDck.Core {
this.BackColor = System.Drawing.SystemColors.Control; this.BackColor = System.Drawing.SystemColors.Control;
this.ClientSize = new System.Drawing.Size(284, 122); this.ClientSize = new System.Drawing.Size(284, 122);
this.Controls.Add(this.progressBarTimer); this.Controls.Add(this.progressBarTimer);
this.Controls.Add(this.panelBrowser);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Location = TweetDck.Core.Controls.ControlExtensions.InvisibleLocation;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "FormNotification";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormNotification_FormClosing); this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormNotification_FormClosing);
this.ResumeLayout(false); this.ResumeLayout(false);
@@ -90,10 +66,8 @@ namespace TweetDck.Core {
#endregion #endregion
private System.Windows.Forms.Panel panelBrowser;
private Controls.FlatProgressBar progressBarTimer;
private System.Windows.Forms.Timer timerProgress;
private System.Windows.Forms.ToolTip toolTip;
private System.Windows.Forms.Timer timerDisplayDelay; private System.Windows.Forms.Timer timerDisplayDelay;
protected System.Windows.Forms.Timer timerProgress;
private Controls.FlatProgressBar progressBarTimer;
} }
} }

View File

@@ -0,0 +1,243 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using TweetDck.Core.Bridge;
using TweetDck.Core.Controls;
using TweetDck.Core.Utils;
using TweetDck.Plugins;
using TweetDck.Plugins.Enums;
using TweetDck.Resources;
namespace TweetDck.Core.Notification{
partial class FormNotificationMain : FormNotificationBase{
private const string NotificationScriptFile = "notification.js";
private static readonly string NotificationScriptIdentifier = ScriptLoader.GetRootIdentifier(NotificationScriptFile);
private static readonly string PluginScriptIdentifier = ScriptLoader.GetRootIdentifier(PluginManager.PluginNotificationScriptFile);
private static readonly string NotificationJS, PluginJS;
static FormNotificationMain(){
NotificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
PluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile);
}
private static int BaseClientWidth{
get{
int level = TweetNotification.FontSizeLevel;
return level == 0 ? 284 : (int)Math.Round(284.0*(1.0+0.05*level));
}
}
private static int BaseClientHeight{
get{
int level = TweetNotification.FontSizeLevel;
return level == 0 ? 118 : (int)Math.Round(118.0*(1.0+0.075*level));
}
}
private readonly PluginManager plugins;
protected int timeLeft, totalTime;
protected bool pausedDuringNotification;
private readonly NativeMethods.HookProc mouseHookDelegate;
private IntPtr mouseHook;
private bool? prevDisplayTimer;
private int? prevFontSize;
private bool RequiresResize{
get{
return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Program.UserConfig.DisplayNotificationTimer || prevFontSize != TweetNotification.FontSizeLevel;
}
set{
if (value){
prevDisplayTimer = null;
prevFontSize = null;
}
else{
prevDisplayTimer = Program.UserConfig.DisplayNotificationTimer;
prevFontSize = TweetNotification.FontSizeLevel;
}
}
}
public FormNotificationMain(FormBrowser owner, PluginManager pluginManager, NotificationFlags flags) : base(owner, flags){
InitializeComponent();
this.plugins = pluginManager;
browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge(owner, this));
browser.RegisterAsyncJsObject("$TDP", plugins.Bridge);
browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd;
mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook();
}
// mouse wheel hook
private void StartMouseHook(){
if (mouseHook == IntPtr.Zero){
mouseHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, mouseHookDelegate, IntPtr.Zero, 0);
}
}
private void StopMouseHook(){
if (mouseHook != IntPtr.Zero){
NativeMethods.UnhookWindowsHookEx(mouseHook);
mouseHook = IntPtr.Zero;
}
}
private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam){
if (!ContainsFocus && wParam.ToInt32() == NativeMethods.WH_MOUSEWHEEL && browser.Bounds.Contains(PointToClient(Cursor.Position))){
// fuck it, Activate() doesn't work with this
Point prevPos = Cursor.Position;
Cursor.Position = PointToScreen(new Point(0, -1));
NativeMethods.SimulateMouseClick(NativeMethods.MouseButton.Left);
Cursor.Position = prevPos;
}
return NativeMethods.CallNextHookEx(mouseHook, nCode, wParam, lParam);
}
// event handlers
private void FormNotification_FormClosing(object sender, FormClosingEventArgs e){
if (e.CloseReason == CloseReason.UserClosing){
HideNotification(false);
e.Cancel = true;
}
}
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
if (!e.IsLoading && browser.Address != "about:blank"){
this.InvokeSafe(() => {
Visible = true; // ensures repaint before moving the window to a visible location
timerDisplayDelay.Start();
});
}
}
private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && NotificationJS != null && browser.Address != "about:blank"){
e.Frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Properties.ExpandLinksOnHover));
ScriptLoader.ExecuteScript(e.Frame, NotificationJS, NotificationScriptIdentifier);
if (plugins.HasAnyPlugin(PluginEnvironment.Notification)){
ScriptLoader.ExecuteScript(e.Frame, PluginJS, PluginScriptIdentifier);
ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginGlobalScriptFile);
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Notification, false);
}
}
}
private void timerDisplayDelay_Tick(object sender, EventArgs e){
OnNotificationReady();
timerDisplayDelay.Stop();
}
private void timerHideProgress_Tick(object sender, EventArgs e){
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen)return;
timeLeft -= timerProgress.Interval;
int value = (int)Math.Round(1025.0*(totalTime-timeLeft)/totalTime);
progressBarTimer.SetValueInstant(Math.Min(1000, Math.Max(0, Program.UserConfig.NotificationTimerCountDown ? 1000-value : value)));
if (timeLeft <= 0){
FinishCurrentNotification();
}
}
// notification methods
public virtual void ShowNotification(TweetNotification notification){
LoadTweet(notification);
}
public void ShowNotificationForSettings(bool reset){
if (reset){
LoadTweet(TweetNotification.ExampleTweet);
}
else{
PrepareAndDisplayWindow();
}
}
public override void HideNotification(bool loadBlank){
base.HideNotification(loadBlank);
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? 1000 : 0;
timerProgress.Stop();
totalTime = 0;
StopMouseHook();
}
public override void FinishCurrentNotification(){
timerProgress.Stop();
}
public override void PauseNotification(){
if (!IsPaused){
pausedDuringNotification = IsNotificationVisible;
timerProgress.Stop();
StopMouseHook();
}
base.PauseNotification();
}
public override void ResumeNotification(){
bool wasPaused = IsPaused;
base.ResumeNotification();
if (wasPaused && !IsPaused && pausedDuringNotification){
OnNotificationReady();
}
}
protected override void LoadTweet(TweetNotification tweet){
timerProgress.Stop();
totalTime = timeLeft = tweet.GetDisplayDuration(Program.UserConfig.NotificationDurationValue);
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? 1000 : 0;
base.LoadTweet(tweet);
}
protected override void SetNotificationSize(int width, int height){
if (Program.UserConfig.DisplayNotificationTimer){
ClientSize = new Size(width, height+4);
progressBarTimer.Visible = true;
}
else{
ClientSize = new Size(width, height);
progressBarTimer.Visible = false;
}
panelBrowser.Height = height;
}
private void PrepareAndDisplayWindow(){
if (RequiresResize){
RequiresResize = false;
SetNotificationSize(BaseClientWidth, BaseClientHeight);
}
MoveToVisibleLocation();
StartMouseHook();
}
protected override void OnNotificationReady(){
PrepareAndDisplayWindow();
timerProgress.Start();
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using TweetDck.Plugins;
using System.Windows.Forms;
namespace TweetDck.Core.Notification{
sealed class FormNotificationTweet : FormNotificationMain{
private readonly Queue<TweetNotification> tweetQueue = new Queue<TweetNotification>(4);
public FormNotificationTweet(FormBrowser owner, PluginManager pluginManager, NotificationFlags flags) : base(owner, pluginManager, flags){
Program.UserConfig.MuteToggled += Config_MuteToggled;
Disposed += (sender, args) => Program.UserConfig.MuteToggled -= Config_MuteToggled;
if (Program.UserConfig.MuteNotifications){
PauseNotification();
}
FormClosing += FormNotificationTweet_FormClosing;
}
private void FormNotificationTweet_FormClosing(object sender, FormClosingEventArgs e){
if (e.CloseReason == CloseReason.UserClosing){
tweetQueue.Clear(); // already canceled
}
}
// event handlers
private void Config_MuteToggled(object sender, EventArgs e){
if (Program.UserConfig.MuteNotifications){
PauseNotification();
}
else{
ResumeNotification();
}
}
// notification methods
public override void ShowNotification(TweetNotification notification){
if (IsPaused){
tweetQueue.Enqueue(notification);
}
else{
tweetQueue.Enqueue(notification);
UpdateTitle();
if (totalTime == 0){
LoadNextNotification();
}
}
}
public override void FinishCurrentNotification(){
if (tweetQueue.Count > 0){
LoadNextNotification();
}
else{
HideNotification(true);
}
}
public override void ResumeNotification(){
bool wasPaused = IsPaused;
base.ResumeNotification();
if (wasPaused && !IsPaused && !pausedDuringNotification && tweetQueue.Count > 0){
LoadNextNotification();
}
}
private void LoadNextNotification(){
LoadTweet(tweetQueue.Dequeue());
}
protected override void UpdateTitle(){
Text = tweetQueue.Count > 0 ? Program.BrandName+" ("+tweetQueue.Count+" more left)" : Program.BrandName;
}
protected override void OnNotificationReady(){
UpdateTitle();
base.OnNotificationReady();
}
}
}

View File

@@ -4,10 +4,7 @@ namespace TweetDck.Core.Notification{
[Flags] [Flags]
public enum NotificationFlags{ public enum NotificationFlags{
None = 0, None = 0,
AutoHide = 1, DisableContextMenu = 1,
DisableScripts = 2, TopMost = 2
DisableContextMenu = 4,
TopMost = 8,
ManualDisplay = 16
} }
} }

View File

@@ -4,10 +4,13 @@ using CefSharp;
using TweetDck.Core.Bridge; using TweetDck.Core.Bridge;
using TweetDck.Core.Controls; using TweetDck.Core.Controls;
using TweetDck.Resources; using TweetDck.Resources;
using System.Drawing;
using System.Drawing.Imaging;
using TweetDck.Core.Utils;
namespace TweetDck.Core.Notification.Screenshot{ namespace TweetDck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotification{ sealed class FormNotificationScreenshotable : FormNotificationBase{
public FormNotificationScreenshotable(Action callback, FormBrowser owner, NotificationFlags flags) : base(owner, null, flags){ public FormNotificationScreenshotable(Action callback, Form owner, NotificationFlags flags) : base(owner, flags){
browser.RegisterAsyncJsObject("$TD_NotificationScreenshot", new CallbackBridge(this, callback)); browser.RegisterAsyncJsObject("$TD_NotificationScreenshot", new CallbackBridge(this, callback));
browser.FrameLoadEnd += (sender, args) => { browser.FrameLoadEnd += (sender, args) => {
@@ -15,23 +18,36 @@ namespace TweetDck.Core.Notification.Screenshot{
ScriptLoader.ExecuteScript(args.Frame, "window.setTimeout($TD_NotificationScreenshot.trigger, 25)", "gen:screenshot"); ScriptLoader.ExecuteScript(args.Frame, "window.setTimeout($TD_NotificationScreenshot.trigger, 25)", "gen:screenshot");
} }
}; };
UpdateTitle();
} }
public void LoadNotificationForScreenshot(TweetNotification tweet, int width, int height){ public void LoadNotificationForScreenshot(TweetNotification tweet, int width, int height){
browser.LoadHtml(tweet.GenerateHtml(enableCustomCSS: false), "http://tweetdeck.twitter.com/?"+DateTime.Now.Ticks); browser.LoadHtml(tweet.GenerateHtml(enableCustomCSS: false), "http://tweetdeck.twitter.com/?"+DateTime.Now.Ticks);
Location = ControlExtensions.InvisibleLocation; Location = ControlExtensions.InvisibleLocation;
FormBorderStyle = Program.UserConfig.ShowScreenshotBorder ? FormBorderStyle.FixedToolWindow : FormBorderStyle.None; SetNotificationSize(width, height);
SetNotificationSize(width, height, false);
} }
public void TakeScreenshotAndHide(){ public void TakeScreenshotAndHide(){
MoveToVisibleLocation(); MoveToVisibleLocation();
Activate();
SendKeys.SendWait("%{PRTSC}"); bool border = Program.UserConfig.ShowScreenshotBorder;
IntPtr context = NativeMethods.GetDeviceContext(this, border);
if (context == IntPtr.Zero){
MessageBox.Show("Could not retrieve a graphics context handle for the notification window to take the screenshot.", "Screenshot Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else{
using(Bitmap bmp = new Bitmap(border ? Width : ClientSize.Width, border ? Height : ClientSize.Height, PixelFormat.Format32bppRgb)){
try{
NativeMethods.RenderSourceIntoBitmap(context, bmp);
}finally{
NativeMethods.ReleaseDeviceContext(this, context);
}
Clipboard.SetImage(bmp);
}
}
Reset(); Reset();
} }

View File

@@ -4,14 +4,11 @@ using TweetDck.Core.Utils;
namespace TweetDck.Core.Notification.Screenshot{ namespace TweetDck.Core.Notification.Screenshot{
sealed class TweetScreenshotManager : IDisposable{ sealed class TweetScreenshotManager : IDisposable{
private readonly FormBrowser browser;
private readonly FormNotificationScreenshotable screenshot; private readonly FormNotificationScreenshotable screenshot;
private readonly Timer timeout; private readonly Timer timeout;
public TweetScreenshotManager(FormBrowser browser){ public TweetScreenshotManager(Form owner){
this.browser = browser; this.screenshot = new FormNotificationScreenshotable(Callback, owner, NotificationFlags.DisableContextMenu | NotificationFlags.TopMost){
this.screenshot = new FormNotificationScreenshotable(Callback, browser, NotificationFlags.DisableScripts | NotificationFlags.DisableContextMenu | NotificationFlags.TopMost | NotificationFlags.ManualDisplay){
CanMoveWindow = () => false CanMoveWindow = () => false
}; };
@@ -31,15 +28,12 @@ namespace TweetDck.Core.Notification.Screenshot{
} }
timeout.Stop(); timeout.Stop();
browser.PauseNotification();
screenshot.TakeScreenshotAndHide(); screenshot.TakeScreenshotAndHide();
browser.ResumeNotification();
} }
public void Dispose(){ public void Dispose(){
timeout.Dispose();
screenshot.Dispose(); screenshot.Dispose();
timeout.Dispose();
} }
} }
} }

View File

@@ -19,7 +19,7 @@ namespace TweetDck.Core.Other{
} }
private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e){ private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e){
BrowserUtils.OpenExternalBrowser(e.Link.LinkData as string); BrowserUtils.OpenExternalBrowserUnsafe(e.Link.LinkData as string);
} }
} }
} }

View File

@@ -36,17 +36,19 @@ namespace TweetDck.Core.Other{
this.tabBtnOfficial = tabPanelPlugins.AddButton("", () => SelectGroup(PluginGroup.Official)); this.tabBtnOfficial = tabPanelPlugins.AddButton("", () => SelectGroup(PluginGroup.Official));
this.tabBtnCustom = tabPanelPlugins.AddButton("", () => SelectGroup(PluginGroup.Custom)); this.tabBtnCustom = tabPanelPlugins.AddButton("", () => SelectGroup(PluginGroup.Custom));
this.tabPanelPlugins.SelectTab(tabBtnOfficial);
this.pluginManager_Reloaded(pluginManager, null); this.pluginManager_Reloaded(pluginManager, null);
Shown += (sender, args) => { Shown += (sender, args) => {
Program.UserConfig.PluginsWindow.Restore(this, false); Program.UserConfig.PluginsWindow.Restore(this, false);
this.tabPanelPlugins.SelectTab(tabBtnOfficial);
}; };
FormClosed += (sender, args) => { FormClosed += (sender, args) => {
Program.UserConfig.PluginsWindow.Save(this); Program.UserConfig.PluginsWindow.Save(this);
Program.UserConfig.Save(); Program.UserConfig.Save();
}; };
Disposed += (sender, args) => this.pluginManager.Reloaded -= pluginManager_Reloaded;
} }
private void SelectGroup(PluginGroup group){ private void SelectGroup(PluginGroup group){
@@ -76,8 +78,8 @@ namespace TweetDck.Core.Other{
} }
} }
flowLayoutPlugins_Resize(flowLayoutPlugins, new EventArgs());
flowLayoutPlugins.ResumeLayout(true); flowLayoutPlugins.ResumeLayout(true);
flowLayoutPlugins_Resize(flowLayoutPlugins, new EventArgs());
} }
private void pluginManager_Reloaded(object sender, PluginLoadEventArgs e){ private void pluginManager_Reloaded(object sender, PluginLoadEventArgs e){

View File

@@ -44,7 +44,7 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
} }
private void btnOpenWiki_Click(object sender, EventArgs e){ private void btnOpenWiki_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki"); BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/wiki");
} }
private void btnApply_Click(object sender, EventArgs e){ private void btnApply_Click(object sender, EventArgs e){

View File

@@ -22,7 +22,7 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
} }
private void btnHelp_Click(object sender, EventArgs e){ private void btnHelp_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowser("http://peter.sh/experiments/chromium-command-line-switches/"); BrowserUtils.OpenExternalBrowserUnsafe("http://peter.sh/experiments/chromium-command-line-switches/");
} }
private void btnApply_Click(object sender, EventArgs e){ private void btnApply_Click(object sender, EventArgs e){

View File

@@ -9,10 +9,10 @@ using TweetDck.Core.Utils;
namespace TweetDck.Core.Other.Settings{ namespace TweetDck.Core.Other.Settings{
partial class TabSettingsNotifications : BaseTabSettings{ partial class TabSettingsNotifications : BaseTabSettings{
private readonly FormNotification notification; private readonly FormNotificationMain notification;
private readonly Point initCursorPosition; private readonly Point initCursorPosition;
public TabSettingsNotifications(FormNotification notification, bool ignoreAutoClick){ public TabSettingsNotifications(FormNotificationMain notification, bool ignoreAutoClick){
InitializeComponent(); InitializeComponent();
this.notification = notification; this.notification = notification;

View File

@@ -12,11 +12,11 @@ namespace TweetDck.Core.Other.Settings{
InitializeComponent(); InitializeComponent();
this.updates = updates; this.updates = updates;
this.updates.CheckFinished += updates_CheckFinished; this.updates.CheckFinished += updates_CheckFinished;
Disposed += (sender, args) => this.updates.CheckFinished -= updates_CheckFinished;
checkUpdateNotifications.Checked = Config.EnableUpdateCheck; checkUpdateNotifications.Checked = Config.EnableUpdateCheck;
Disposed += (sender, args) => this.updates.CheckFinished -= updates_CheckFinished;
} }
private void checkUpdateNotifications_CheckedChanged(object sender, EventArgs e){ private void checkUpdateNotifications_CheckedChanged(object sender, EventArgs e){

View File

@@ -27,7 +27,27 @@ namespace TweetDck.Core.Utils{
} }
} }
public static void OpenExternalBrowser(string url){ // TODO implement mailto public static bool IsValidUrl(string url){
Uri uri;
if (Uri.TryCreate(url, UriKind.Absolute, out uri)){
string scheme = uri.Scheme;
return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto;
}
return false;
}
public static void OpenExternalBrowser(string url){
if (IsValidUrl(url)){
OpenExternalBrowserUnsafe(url);
}
else{
MessageBox.Show("A potentially malicious URL was blocked from opening:"+Environment.NewLine+url, "Blocked URL", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
public static void OpenExternalBrowserUnsafe(string url){
using(Process.Start(url)){} using(Process.Start(url)){}
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Drawing;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows.Forms; using System.Windows.Forms;
@@ -45,6 +46,19 @@ namespace TweetDck.Core.Utils{
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO info); private static extern bool GetLastInputInfo(ref LASTINPUTINFO info);
[DllImport("user32.dll")]
private static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam); public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam);
@@ -106,5 +120,25 @@ namespace TweetDck.Core.Utils{
int seconds = (int)Math.Floor(TimeSpan.FromMilliseconds(ticks-info.dwTime).TotalSeconds); int seconds = (int)Math.Floor(TimeSpan.FromMilliseconds(ticks-info.dwTime).TotalSeconds);
return Math.Max(0, seconds); // ignore rollover after several weeks of uptime return Math.Max(0, seconds); // ignore rollover after several weeks of uptime
} }
public static IntPtr GetDeviceContext(Form form, bool includeBorder){
return includeBorder ? GetWindowDC(form.Handle) : GetDC(form.Handle);
}
public static void RenderSourceIntoBitmap(IntPtr source, Bitmap target){
using(Graphics graphics = Graphics.FromImage(target)){
IntPtr graphicsHandle = graphics.GetHdc();
try{
BitBlt(graphicsHandle, 0, 0, target.Width, target.Height, source, 0, 0, 0x00CC0020);
}finally{
graphics.ReleaseHdc(graphicsHandle);
}
}
}
public static void ReleaseDeviceContext(Form form, IntPtr ctx){
ReleaseDC(form.Handle, ctx);
}
} }
} }

View File

@@ -1,8 +1,11 @@
using System.Diagnostics; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using Timer = System.Windows.Forms.Timer;
namespace TweetDck.Core.Utils{ namespace TweetDck.Core.Utils{
static class WindowsUtils{ static class WindowsUtils{
@@ -45,6 +48,18 @@ namespace TweetDck.Core.Utils{
return timer; return timer;
} }
public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){
for(int waited = 0; waited < timeoutMillis; waited += timeStepMillis){
if (test()){
return true;
}
Thread.Sleep(timeStepMillis);
}
return false;
}
public static void ClipboardStripHtmlStyles(){ public static void ClipboardStripHtmlStyles(){
if (!Clipboard.ContainsText(TextDataFormat.Html)){ if (!Clipboard.ContainsText(TextDataFormat.Html)){
return; return;

View File

@@ -39,6 +39,10 @@ namespace TweetDck.Plugins{
} }
public void SetConfig(PluginConfig config){ public void SetConfig(PluginConfig config){
if (this.Config != null){
this.Config.InternalPluginChangedState -= Config_InternalPluginChangedState;
}
this.Config = config; this.Config = config;
this.Config.InternalPluginChangedState += Config_InternalPluginChangedState; this.Config.InternalPluginChangedState += Config_InternalPluginChangedState;
} }

View File

@@ -21,8 +21,8 @@ namespace TweetDck{
public const string BrandName = "TweetDuck"; public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com"; public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.6.4"; public const string VersionTag = "1.6.6";
public const string VersionFull = "1.6.4.0"; public const string VersionFull = "1.6.6.0";
public static readonly Version Version = new Version(VersionTag); public static readonly Version Version = new Version(VersionTag);
@@ -89,21 +89,26 @@ namespace TweetDck{
return; return;
} }
} }
else{ else Thread.Sleep(500);
Thread.Sleep(500);
}
} }
} }
else{ else{
LockManager.Result lockResult = LockManager.Lock(); LockManager.Result lockResult = LockManager.Lock();
if (lockResult == LockManager.Result.HasProcess){ if (lockResult == LockManager.Result.HasProcess){
if (LockManager.LockingProcess.MainWindowHandle == IntPtr.Zero && LockManager.LockingProcess.Responding){ // restore if the original process is in tray if (LockManager.LockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.SendMessage(NativeMethods.HWND_BROADCAST, WindowRestoreMessage, 0, IntPtr.Zero); NativeMethods.SendMessage(NativeMethods.HWND_BROADCAST, WindowRestoreMessage, LockManager.LockingProcess.Id, IntPtr.Zero);
return;
if (WindowsUtils.TrySleepUntil(() => {
LockManager.LockingProcess.Refresh();
return LockManager.LockingProcess.HasExited || (LockManager.LockingProcess.MainWindowHandle != IntPtr.Zero && LockManager.LockingProcess.Responding);
}, 2000, 250)){
return; // should trigger on first attempt if succeeded, but wait just in case
}
} }
else if (MessageBox.Show("Another instance of "+BrandName+" is already running.\r\nDo you want to close it?", BrandName+" is Already Running", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button2) == DialogResult.Yes){
if (!LockManager.CloseLockingProcess(30000)){ if (MessageBox.Show("Another instance of "+BrandName+" is already running.\r\nDo you want to close it?", BrandName+" is Already Running", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button2) == DialogResult.Yes){
if (!LockManager.CloseLockingProcess(10000, 5000)){
MessageBox.Show("Could not close the other process.", BrandName+" Has Failed :(", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show("Could not close the other process.", BrandName+" Has Failed :(", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; return;
} }
@@ -166,10 +171,11 @@ namespace TweetDck{
if (mainForm.UpdateInstallerPath != null){ if (mainForm.UpdateInstallerPath != null){
ExitCleanup(); ExitCleanup();
// ProgramPath has a trailing backslash
string updaterArgs = "/SP- /SILENT /CLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+GetArgsClean().ToString().Replace("\"", "^\"")+"\""+(IsPortable ? " /PORTABLE=1" : ""); string updaterArgs = "/SP- /SILENT /CLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+GetArgsClean().ToString().Replace("\"", "^\"")+"\""+(IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath); bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath);
WindowsUtils.StartProcess(mainForm.UpdateInstallerPath, updaterArgs, runElevated); // ProgramPath has a trailing backslash WindowsUtils.StartProcess(mainForm.UpdateInstallerPath, updaterArgs, runElevated);
Application.Exit(); Application.Exit();
} }
} }

View File

@@ -1,11 +1,11 @@
# Build Instructions # Build Instructions
The program was build using Visual Studio 2013. After opening the solution, make sure you have **CefSharp.WinForms** and **Microsoft.VC120.CRT.JetBrains** included - if not, download them using NuGet. For **CefSharp**, you will need version 53 or newer currently available as a pre-release. The program was build using Visual Studio 2013. After opening the solution, make sure you have **CefSharp.WinForms** and **Microsoft.VC120.CRT.JetBrains** included - if not, download them using NuGet.
``` ```
PM> Install-Package CefSharp.WinForms -Version 53.0.1 PM> Install-Package CefSharp.WinForms -Version 55.0.0
PM> Install-Package Microsoft.VC120.CRT.JetBrains PM> Install-Package Microsoft.VC120.CRT.JetBrains
``` ```
After building, run **_postbuild.bat** which deletes unnecessary files that CefSharp adds after post-build events >_> After building, run either `_postbuild.bat` if you want to package the files yourself, or `bld/RUN BUILD.bat` to generate installer files using Inno Setup. Do not run both files consecutively, otherwise the program will crash - if you want to do both, rebuild the project before running each file.
Built files are then available in **bin/x86** and/or **bin/x64**. Built files are then available in **bin/x86** and/or **bin/x64**, installer files are generated in **bld/Output**. If you decide to release a custom version publicly, please make it clear that it is not the original TweetDuck.

View File

@@ -1,14 +1,40 @@
enabled(){ enabled(){
this.emojiHTML = ""; this.selectedSkinTone = "";
this.skinToneList = [
"", "1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"
];
this.skinToneNonDefaultList = [
"1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"
];
this.skinToneData = [
[ "", "#FFDD67" ],
[ "1F3FB", "#FFE1BD" ],
[ "1F3FC", "#FED0AC" ],
[ "1F3FD", "#D6A57C" ],
[ "1F3FE", "#B47D56" ],
[ "1F3FF", "#8A6859" ],
];
this.emojiHTML1 = ""; // no skin tones, prepended
this.emojiHTML2 = {}; // contains emojis with skin tones
this.emojiHTML3 = ""; // no skin tones, appended
var me = this; var me = this;
// styles // styles
this.css = window.TDPF_createCustomStyle(this); this.css = window.TDPF_createCustomStyle(this);
this.css.insert(".emoji-keyboard { position: absolute; width: 15.35em; height: 11.2em; background-color: white; overflow-y: auto; padding: 0.1em; box-sizing: border-box; border-radius: 2px; font-size: 24px; z-index: 9999 }"); this.css.insert(".emoji-keyboard { position: absolute; width: 15.35em; background-color: white; border-radius: 2px 2px 3px 3px; font-size: 24px; z-index: 9999 }");
this.css.insert(".emoji-keyboard .separator { height: 26px; }"); this.css.insert(".emoji-keyboard-list { height: 10.14em; padding: 0.1em; box-sizing: border-box; overflow-y: auto }");
this.css.insert(".emoji-keyboard .emoji { padding: 0.1em !important; cursor: pointer }"); this.css.insert(".emoji-keyboard-list .separator { height: 26px }");
this.css.insert(".emoji-keyboard-list .emoji { padding: 0.1em !important; cursor: pointer }");
this.css.insert(".emoji-keyboard-skintones { height: 1.3em; text-align: center; background-color: #292f33; border-radius: 0 0 2px 2px }");
this.css.insert(".emoji-keyboard-skintones div { width: 0.8em; height: 0.8em; margin: 0.25em 0.1em; border-radius: 50%; display: inline-block; box-sizing: border-box; cursor: pointer }");
this.css.insert(".emoji-keyboard-skintones .sel { border: 2px solid rgba(0, 0, 0, 0.35); box-shadow: 0 0 2px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.4) inset }");
this.css.insert(".emoji-keyboard-skintones :hover { border: 2px solid rgba(0, 0, 0, 0.25); box-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.25) inset }");
// layout // layout
@@ -34,16 +60,29 @@ enabled(){
$(".emoji-keyboard-popup-btn").removeClass("is-selected"); $(".emoji-keyboard-popup-btn").removeClass("is-selected");
}; };
this.generateKeyboardFor = (input, left, top) => { var generateEmojiHTML = skinTone => {
var created = document.createElement("div"); return this.emojiHTML1+this.emojiHTML2[skinTone]+this.emojiHTML3;
document.body.appendChild(created); };
created.classList.add("emoji-keyboard"); var selectSkinTone = skinTone => {
created.style.left = left+"px"; let selectedEle = this.currentKeyboard.children[1].querySelector("[data-tone='"+this.selectedSkinTone+"']");
created.style.top = top+"px"; selectedEle && selectedEle.classList.remove("sel");
created.innerHTML = this.emojiHTML;
created.addEventListener("click", function(e){ this.selectedSkinTone = skinTone;
this.currentKeyboard.children[0].innerHTML = generateEmojiHTML(skinTone);
this.currentKeyboard.children[1].querySelector("[data-tone='"+this.selectedSkinTone+"']").classList.add("sel");
};
this.generateKeyboard = (input, left, top) => {
var outer = document.createElement("div");
outer.classList.add("emoji-keyboard");
outer.style.left = left+"px";
outer.style.top = top+"px";
var keyboard = document.createElement("div");
keyboard.classList.add("emoji-keyboard-list");
keyboard.addEventListener("click", function(e){
if (e.target.tagName === "IMG"){ if (e.target.tagName === "IMG"){
input.val(input.val()+e.target.getAttribute("alt")); input.val(input.val()+e.target.getAttribute("alt"));
input.trigger("change"); input.trigger("change");
@@ -53,7 +92,24 @@ enabled(){
e.stopPropagation(); e.stopPropagation();
}); });
return created; var skintones = document.createElement("div");
skintones.innerHTML = me.skinToneData.map(entry => "<div data-tone='"+entry[0]+"' style='background-color:"+entry[1]+"'></div>").join("");
skintones.classList.add("emoji-keyboard-skintones");
skintones.addEventListener("click", function(e){
if (e.target.hasAttribute("data-tone")){
selectSkinTone(e.target.getAttribute("data-tone") || "");
}
e.stopPropagation();
});
outer.appendChild(keyboard);
outer.appendChild(skintones);
document.body.appendChild(outer);
this.currentKeyboard = outer;
selectSkinTone(this.selectedSkinTone);
}; };
this.prevTryPasteImage = window.TDGF_tryPasteImage; this.prevTryPasteImage = window.TDGF_tryPasteImage;
@@ -75,7 +131,7 @@ enabled(){
} }
else{ else{
var pos = $(this).offset(); var pos = $(this).offset();
me.currentKeyboard = me.generateKeyboardFor($(".js-compose-text").first(), pos.left, pos.top+$(this).outerHeight()+8); me.generateKeyboard($(".js-compose-text").first(), pos.left, pos.top+$(this).outerHeight()+8);
$(this).addClass("is-selected"); $(this).addClass("is-selected");
} }
@@ -123,38 +179,93 @@ ready(){
}; };
$TDP.readFileRoot(this.$token, "emoji-ordering.txt").then(contents => { $TDP.readFileRoot(this.$token, "emoji-ordering.txt").then(contents => {
let generated = []; let generated1 = [];
let generated2 = {};
let generated3 = [];
let addDeclaration = decl => { for(let skinTone of this.skinToneList){
generated.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join("")); generated2[skinTone] = [];
}
// declaration inserters
let addDeclaration1 = decl => {
generated1.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join(""));
}; };
let skinTones = [ let addDeclaration2 = (tone, decl) => {
"1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF" let gen = decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join("");
];
if (tone === null){
for(let skinTone of this.skinToneList){
generated2[skinTone].push(gen);
}
}
else{
generated2[tone].push(gen);
}
};
let addDeclaration3 = decl => {
generated3.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join(""));
};
// line reading
let skinToneState = 0;
for(let line of contents.split("\n")){ for(let line of contents.split("\n")){
if (line[0] === '@'){ if (line[0] === '@'){
generated.push("___"); switch(skinToneState){
case 0: generated1.push("___"); break;
case 1: this.skinToneList.forEach(skinTone => generated2[skinTone].push("___")); break;
case 2: generated3.push("___"); break;
}
if (line[1] === '1'){
skinToneState = 1;
}
else if (line[1] === '2'){
skinToneState = 2;
}
} }
else{ else if (skinToneState === 1){
let decl = line.slice(0, line.indexOf(";")); let decl = line.slice(0, line.indexOf(';'));
let skinIndex = decl.indexOf('$'); let skinIndex = decl.indexOf('$');
if (skinIndex !== -1){ if (skinIndex !== -1){
let declPre = decl.slice(0, skinIndex); let declPre = decl.slice(0, skinIndex);
let declPost = decl.slice(skinIndex+1); let declPost = decl.slice(skinIndex+1);
skinTones.map(skinTone => declPre+skinTone+declPost).forEach(addDeclaration); for(let skinTone of this.skinToneNonDefaultList){
generated2[skinTone].pop();
addDeclaration2(skinTone, declPre+skinTone+declPost);
}
} }
else{ else{
addDeclaration(decl); addDeclaration2(null, decl);
} }
} }
else if (skinToneState === 2){
addDeclaration3(line.slice(0, line.indexOf(';')));
}
else if (skinToneState === 0){
addDeclaration1(line.slice(0, line.indexOf(';')));
}
} }
// final processing
let replaceSeparators = str => str.replace(/___/g, "<div class='separator'></div>");
let start = "<p style='font-size:13px;color:#444;margin:4px;text-align:center'>Please, note that most emoji will not show up properly in the text box above, but they will display in the tweet.</p>"; let start = "<p style='font-size:13px;color:#444;margin:4px;text-align:center'>Please, note that most emoji will not show up properly in the text box above, but they will display in the tweet.</p>";
this.emojiHTML = start+TD.util.cleanWithEmoji(generated.join("")).replace(/___/g, "<div class='separator'></div>"); this.emojiHTML1 = start+replaceSeparators(TD.util.cleanWithEmoji(generated1.join("")));
for(let skinTone of this.skinToneList){
this.emojiHTML2[skinTone] = replaceSeparators(TD.util.cleanWithEmoji(generated2[skinTone].join("")));
}
this.emojiHTML3 = replaceSeparators(TD.util.cleanWithEmoji(generated3.join("")));
}).catch(err => { }).catch(err => {
$TD.alert("error", "Problem loading emoji keyboard: "+err.message); $TD.alert("error", "Problem loading emoji keyboard: "+err.message);
}); });

View File

@@ -96,7 +96,7 @@
1F648; 🙈 see-no-evil monkey 1F648; 🙈 see-no-evil monkey
1F649; 🙉 hear-no-evil monkey 1F649; 🙉 hear-no-evil monkey
1F64A; 🙊 speak-no-evil monkey 1F64A; 🙊 speak-no-evil monkey
@ @1 enable skin tones
1F466; 👦 boy 1F466; 👦 boy
1F466 $; 👦🏻 boy 1F466 $; 👦🏻 boy
1F467; 👧 girl 1F467; 👧 girl
@@ -176,39 +176,27 @@
1F468 200D 1F692; 👨‍🚒 man firefighter 1F468 200D 1F692; 👨‍🚒 man firefighter
1F468 $ 200D 1F692; 👨🏻‍🚒 man firefighter 1F468 $ 200D 1F692; 👨🏻‍🚒 man firefighter
1F469 200D 1F692; 👩‍🚒 woman firefighter 1F469 200D 1F692; 👩‍🚒 woman firefighter
1F469 $ 200D 1F692; 👩🏻‍🚒 woman firefighter 1F469 $ 200D 1F692; 👩🏻‍🚒 woman firefighter !! deleted non-gendered duplicates below
1F46E; 👮 police officer
1F46E $; 👮🏻 police officer
1F46E 200D 2642 FE0F; 👮‍♂️ man police officer 1F46E 200D 2642 FE0F; 👮‍♂️ man police officer
1F46E $ 200D 2642 FE0F; 👮🏻‍♂️ man police officer 1F46E $ 200D 2642 FE0F; 👮🏻‍♂️ man police officer
1F46E 200D 2640 FE0F; 👮‍♀️ woman police officer 1F46E 200D 2640 FE0F; 👮‍♀️ woman police officer
1F46E $ 200D 2640 FE0F; 👮🏻‍♀️ woman police officer 1F46E $ 200D 2640 FE0F; 👮🏻‍♀️ woman police officer
1F575; 🕵 detective
1F575 $; 🕵🏻 detective
1F575 FE0F 200D 2642 FE0F; 🕵️‍♂️ man detective 1F575 FE0F 200D 2642 FE0F; 🕵️‍♂️ man detective
1F575 $ 200D 2642 FE0F; 🕵🏻‍♂️ man detective 1F575 $ 200D 2642 FE0F; 🕵🏻‍♂️ man detective
1F575 FE0F 200D 2640 FE0F; 🕵️‍♀️ woman detective 1F575 FE0F 200D 2640 FE0F; 🕵️‍♀️ woman detective
1F575 $ 200D 2640 FE0F; 🕵🏻‍♀️ woman detective 1F575 $ 200D 2640 FE0F; 🕵🏻‍♀️ woman detective
1F482; 💂 guard
1F482 $; 💂🏻 guard
1F482 200D 2642 FE0F; 💂‍♂️ man guard 1F482 200D 2642 FE0F; 💂‍♂️ man guard
1F482 $ 200D 2642 FE0F; 💂🏻‍♂️ man guard 1F482 $ 200D 2642 FE0F; 💂🏻‍♂️ man guard
1F482 200D 2640 FE0F; 💂‍♀️ woman guard 1F482 200D 2640 FE0F; 💂‍♀️ woman guard
1F482 $ 200D 2640 FE0F; 💂🏻‍♀️ woman guard 1F482 $ 200D 2640 FE0F; 💂🏻‍♀️ woman guard
1F477; 👷 construction worker
1F477 $; 👷🏻 construction worker
1F477 200D 2642 FE0F; 👷‍♂️ man construction worker 1F477 200D 2642 FE0F; 👷‍♂️ man construction worker
1F477 $ 200D 2642 FE0F; 👷🏻‍♂️ man construction worker 1F477 $ 200D 2642 FE0F; 👷🏻‍♂️ man construction worker
1F477 200D 2640 FE0F; 👷‍♀️ woman construction worker 1F477 200D 2640 FE0F; 👷‍♀️ woman construction worker
1F477 $ 200D 2640 FE0F; 👷🏻‍♀️ woman construction worker 1F477 $ 200D 2640 FE0F; 👷🏻‍♀️ woman construction worker
1F473; 👳 person wearing turban
1F473 $; 👳🏻 person wearing turban
1F473 200D 2642 FE0F; 👳‍♂️ man wearing turban 1F473 200D 2642 FE0F; 👳‍♂️ man wearing turban
1F473 $ 200D 2642 FE0F; 👳🏻‍♂️ man wearing turban 1F473 $ 200D 2642 FE0F; 👳🏻‍♂️ man wearing turban
1F473 200D 2640 FE0F; 👳‍♀️ woman wearing turban 1F473 200D 2640 FE0F; 👳‍♀️ woman wearing turban
1F473 $ 200D 2640 FE0F; 👳🏻‍♀️ woman wearing turban 1F473 $ 200D 2640 FE0F; 👳🏻‍♀️ woman wearing turban
1F471; 👱 blond-haired person
1F471 $; 👱🏻 blond-haired person
1F471 200D 2642 FE0F; 👱‍♂️ blond-haired man 1F471 200D 2642 FE0F; 👱‍♂️ blond-haired man
1F471 $ 200D 2642 FE0F; 👱🏻‍♂️ blond-haired man 1F471 $ 200D 2642 FE0F; 👱🏻‍♂️ blond-haired man
1F471 200D 2640 FE0F; 👱‍♀️ blond-haired woman 1F471 200D 2640 FE0F; 👱‍♀️ blond-haired woman
@@ -229,80 +217,54 @@
1F930 $; 🤰🏻 pregnant woman 1F930 $; 🤰🏻 pregnant woman
1F472; 👲 man with Chinese cap 1F472; 👲 man with Chinese cap
1F472 $; 👲🏻 man with Chinese cap 1F472 $; 👲🏻 man with Chinese cap
1F64D; 🙍 person frowning
1F64D $; 🙍🏻 person frowning
1F64D 200D 2642 FE0F; 🙍‍♂️ man frowning 1F64D 200D 2642 FE0F; 🙍‍♂️ man frowning
1F64D $ 200D 2642 FE0F; 🙍🏻‍♂️ man frowning 1F64D $ 200D 2642 FE0F; 🙍🏻‍♂️ man frowning
1F64D 200D 2640 FE0F; 🙍‍♀️ woman frowning 1F64D 200D 2640 FE0F; 🙍‍♀️ woman frowning
1F64D $ 200D 2640 FE0F; 🙍🏻‍♀️ woman frowning 1F64D $ 200D 2640 FE0F; 🙍🏻‍♀️ woman frowning
1F64E; 🙎 person pouting
1F64E $; 🙎🏻 person pouting
1F64E 200D 2642 FE0F; 🙎‍♂️ man pouting 1F64E 200D 2642 FE0F; 🙎‍♂️ man pouting
1F64E $ 200D 2642 FE0F; 🙎🏻‍♂️ man pouting 1F64E $ 200D 2642 FE0F; 🙎🏻‍♂️ man pouting
1F64E 200D 2640 FE0F; 🙎‍♀️ woman pouting 1F64E 200D 2640 FE0F; 🙎‍♀️ woman pouting
1F64E $ 200D 2640 FE0F; 🙎🏻‍♀️ woman pouting 1F64E $ 200D 2640 FE0F; 🙎🏻‍♀️ woman pouting
1F645; 🙅 person gesturing NO
1F645 $; 🙅🏻 person gesturing NO
1F645 200D 2642 FE0F; 🙅‍♂️ man gesturing NO 1F645 200D 2642 FE0F; 🙅‍♂️ man gesturing NO
1F645 $ 200D 2642 FE0F; 🙅🏻‍♂️ man gesturing NO 1F645 $ 200D 2642 FE0F; 🙅🏻‍♂️ man gesturing NO
1F645 200D 2640 FE0F; 🙅‍♀️ woman gesturing NO 1F645 200D 2640 FE0F; 🙅‍♀️ woman gesturing NO
1F645 $ 200D 2640 FE0F; 🙅🏻‍♀️ woman gesturing NO 1F645 $ 200D 2640 FE0F; 🙅🏻‍♀️ woman gesturing NO
1F646; 🙆 person gesturing OK
1F646 $; 🙆🏻 person gesturing OK
1F646 200D 2642 FE0F; 🙆‍♂️ man gesturing OK 1F646 200D 2642 FE0F; 🙆‍♂️ man gesturing OK
1F646 $ 200D 2642 FE0F; 🙆🏻‍♂️ man gesturing OK 1F646 $ 200D 2642 FE0F; 🙆🏻‍♂️ man gesturing OK
1F646 200D 2640 FE0F; 🙆‍♀️ woman gesturing OK 1F646 200D 2640 FE0F; 🙆‍♀️ woman gesturing OK
1F646 $ 200D 2640 FE0F; 🙆🏻‍♀️ woman gesturing OK 1F646 $ 200D 2640 FE0F; 🙆🏻‍♀️ woman gesturing OK
1F481; 💁 person tipping hand
1F481 $; 💁🏻 person tipping hand
1F481 200D 2642 FE0F; 💁‍♂️ man tipping hand 1F481 200D 2642 FE0F; 💁‍♂️ man tipping hand
1F481 $ 200D 2642 FE0F; 💁🏻‍♂️ man tipping hand 1F481 $ 200D 2642 FE0F; 💁🏻‍♂️ man tipping hand
1F481 200D 2640 FE0F; 💁‍♀️ woman tipping hand 1F481 200D 2640 FE0F; 💁‍♀️ woman tipping hand
1F481 $ 200D 2640 FE0F; 💁🏻‍♀️ woman tipping hand 1F481 $ 200D 2640 FE0F; 💁🏻‍♀️ woman tipping hand
1F64B; 🙋 person raising hand
1F64B $; 🙋🏻 person raising hand
1F64B 200D 2642 FE0F; 🙋‍♂️ man raising hand 1F64B 200D 2642 FE0F; 🙋‍♂️ man raising hand
1F64B $ 200D 2642 FE0F; 🙋🏻‍♂️ man raising hand 1F64B $ 200D 2642 FE0F; 🙋🏻‍♂️ man raising hand
1F64B 200D 2640 FE0F; 🙋‍♀️ woman raising hand 1F64B 200D 2640 FE0F; 🙋‍♀️ woman raising hand
1F64B $ 200D 2640 FE0F; 🙋🏻‍♀️ woman raising hand 1F64B $ 200D 2640 FE0F; 🙋🏻‍♀️ woman raising hand
1F647; 🙇 person bowing
1F647 $; 🙇🏻 person bowing
1F647 200D 2642 FE0F; 🙇‍♂️ man bowing 1F647 200D 2642 FE0F; 🙇‍♂️ man bowing
1F647 $ 200D 2642 FE0F; 🙇🏻‍♂️ man bowing 1F647 $ 200D 2642 FE0F; 🙇🏻‍♂️ man bowing
1F647 200D 2640 FE0F; 🙇‍♀️ woman bowing 1F647 200D 2640 FE0F; 🙇‍♀️ woman bowing
1F647 $ 200D 2640 FE0F; 🙇🏻‍♀️ woman bowing 1F647 $ 200D 2640 FE0F; 🙇🏻‍♀️ woman bowing
1F926; 🤦 person facepalming
1F926 $; 🤦🏻 person facepalming
1F926 200D 2642 FE0F; 🤦‍♂️ man facepalming 1F926 200D 2642 FE0F; 🤦‍♂️ man facepalming
1F926 $ 200D 2642 FE0F; 🤦🏻‍♂️ man facepalming 1F926 $ 200D 2642 FE0F; 🤦🏻‍♂️ man facepalming
1F926 200D 2640 FE0F; 🤦‍♀️ woman facepalming 1F926 200D 2640 FE0F; 🤦‍♀️ woman facepalming
1F926 $ 200D 2640 FE0F; 🤦🏻‍♀️ woman facepalming 1F926 $ 200D 2640 FE0F; 🤦🏻‍♀️ woman facepalming
1F937; 🤷 person shrugging
1F937 $; 🤷🏻 person shrugging
1F937 200D 2642 FE0F; 🤷‍♂️ man shrugging 1F937 200D 2642 FE0F; 🤷‍♂️ man shrugging
1F937 $ 200D 2642 FE0F; 🤷🏻‍♂️ man shrugging 1F937 $ 200D 2642 FE0F; 🤷🏻‍♂️ man shrugging
1F937 200D 2640 FE0F; 🤷‍♀️ woman shrugging 1F937 200D 2640 FE0F; 🤷‍♀️ woman shrugging
1F937 $ 200D 2640 FE0F; 🤷🏻‍♀️ woman shrugging 1F937 $ 200D 2640 FE0F; 🤷🏻‍♀️ woman shrugging
1F486; 💆 person getting massage
1F486 $; 💆🏻 person getting massage
1F486 200D 2642 FE0F; 💆‍♂️ man getting massage 1F486 200D 2642 FE0F; 💆‍♂️ man getting massage
1F486 $ 200D 2642 FE0F; 💆🏻‍♂️ man getting massage 1F486 $ 200D 2642 FE0F; 💆🏻‍♂️ man getting massage
1F486 200D 2640 FE0F; 💆‍♀️ woman getting massage 1F486 200D 2640 FE0F; 💆‍♀️ woman getting massage
1F486 $ 200D 2640 FE0F; 💆🏻‍♀️ woman getting massage 1F486 $ 200D 2640 FE0F; 💆🏻‍♀️ woman getting massage
1F487; 💇 person getting haircut
1F487 $; 💇🏻 person getting haircut
1F487 200D 2642 FE0F; 💇‍♂️ man getting haircut 1F487 200D 2642 FE0F; 💇‍♂️ man getting haircut
1F487 $ 200D 2642 FE0F; 💇🏻‍♂️ man getting haircut 1F487 $ 200D 2642 FE0F; 💇🏻‍♂️ man getting haircut
1F487 200D 2640 FE0F; 💇‍♀️ woman getting haircut 1F487 200D 2640 FE0F; 💇‍♀️ woman getting haircut
1F487 $ 200D 2640 FE0F; 💇🏻‍♀️ woman getting haircut 1F487 $ 200D 2640 FE0F; 💇🏻‍♀️ woman getting haircut
1F6B6; 🚶 person walking
1F6B6 $; 🚶🏻 person walking
1F6B6 200D 2642 FE0F; 🚶‍♂️ man walking 1F6B6 200D 2642 FE0F; 🚶‍♂️ man walking
1F6B6 $ 200D 2642 FE0F; 🚶🏻‍♂️ man walking 1F6B6 $ 200D 2642 FE0F; 🚶🏻‍♂️ man walking
1F6B6 200D 2640 FE0F; 🚶‍♀️ woman walking 1F6B6 200D 2640 FE0F; 🚶‍♀️ woman walking
1F6B6 $ 200D 2640 FE0F; 🚶🏻‍♀️ woman walking 1F6B6 $ 200D 2640 FE0F; 🚶🏻‍♀️ woman walking
1F3C3; 🏃 person running
1F3C3 $; 🏃🏻 person running
1F3C3 200D 2642 FE0F; 🏃‍♂️ man running 1F3C3 200D 2642 FE0F; 🏃‍♂️ man running
1F3C3 $ 200D 2642 FE0F; 🏃🏻‍♂️ man running 1F3C3 $ 200D 2642 FE0F; 🏃🏻‍♂️ man running
1F3C3 200D 2640 FE0F; 🏃‍♀️ woman running 1F3C3 200D 2640 FE0F; 🏃‍♀️ woman running
@@ -311,7 +273,6 @@
1F483 $; 💃🏻 woman dancing 1F483 $; 💃🏻 woman dancing
1F57A; 🕺 man dancing 1F57A; 🕺 man dancing
1F57A $; 🕺🏻 man dancing 1F57A $; 🕺🏻 man dancing
1F46F; 👯 people with bunny ears partying
1F46F 200D 2642 FE0F; 👯‍♂️ men with bunny ears partying 1F46F 200D 2642 FE0F; 👯‍♂️ men with bunny ears partying
1F46F 200D 2640 FE0F; 👯‍♀️ women with bunny ears partying 1F46F 200D 2640 FE0F; 👯‍♀️ women with bunny ears partying
1F574; 🕴 man in business suit levitating 1F574; 🕴 man in business suit levitating
@@ -325,83 +286,62 @@
26F7; ⛷ skier 26F7; ⛷ skier
1F3C2; 🏂 snowboarder 1F3C2; 🏂 snowboarder
1F3C2 $; 🏂🏻 snowboarder 1F3C2 $; 🏂🏻 snowboarder
1F3CC; 🏌 person golfing
1F3CC $; 🏌🏻 person golfing
1F3CC FE0F 200D 2642 FE0F; 🏌️‍♂️ man golfing 1F3CC FE0F 200D 2642 FE0F; 🏌️‍♂️ man golfing
1F3CC $ 200D 2642 FE0F; 🏌🏻‍♂️ man golfing 1F3CC $ 200D 2642 FE0F; 🏌🏻‍♂️ man golfing
1F3CC FE0F 200D 2640 FE0F; 🏌️‍♀️ woman golfing 1F3CC FE0F 200D 2640 FE0F; 🏌️‍♀️ woman golfing
1F3CC $ 200D 2640 FE0F; 🏌🏻‍♀️ woman golfing 1F3CC $ 200D 2640 FE0F; 🏌🏻‍♀️ woman golfing
1F3C4; 🏄 person surfing
1F3C4 $; 🏄🏻 person surfing
1F3C4 200D 2642 FE0F; 🏄‍♂️ man surfing 1F3C4 200D 2642 FE0F; 🏄‍♂️ man surfing
1F3C4 $ 200D 2642 FE0F; 🏄🏻‍♂️ man surfing 1F3C4 $ 200D 2642 FE0F; 🏄🏻‍♂️ man surfing
1F3C4 200D 2640 FE0F; 🏄‍♀️ woman surfing 1F3C4 200D 2640 FE0F; 🏄‍♀️ woman surfing
1F3C4 $ 200D 2640 FE0F; 🏄🏻‍♀️ woman surfing 1F3C4 $ 200D 2640 FE0F; 🏄🏻‍♀️ woman surfing
1F6A3; 🚣 person rowing boat
1F6A3 $; 🚣🏻 person rowing boat
1F6A3 200D 2642 FE0F; 🚣‍♂️ man rowing boat 1F6A3 200D 2642 FE0F; 🚣‍♂️ man rowing boat
1F6A3 $ 200D 2642 FE0F; 🚣🏻‍♂️ man rowing boat 1F6A3 $ 200D 2642 FE0F; 🚣🏻‍♂️ man rowing boat
1F6A3 200D 2640 FE0F; 🚣‍♀️ woman rowing boat 1F6A3 200D 2640 FE0F; 🚣‍♀️ woman rowing boat
1F6A3 $ 200D 2640 FE0F; 🚣🏻‍♀️ woman rowing boat 1F6A3 $ 200D 2640 FE0F; 🚣🏻‍♀️ woman rowing boat
1F3CA; 🏊 person swimming
1F3CA $; 🏊🏻 person swimming
1F3CA 200D 2642 FE0F; 🏊‍♂️ man swimming 1F3CA 200D 2642 FE0F; 🏊‍♂️ man swimming
1F3CA $ 200D 2642 FE0F; 🏊🏻‍♂️ man swimming 1F3CA $ 200D 2642 FE0F; 🏊🏻‍♂️ man swimming
1F3CA 200D 2640 FE0F; 🏊‍♀️ woman swimming 1F3CA 200D 2640 FE0F; 🏊‍♀️ woman swimming
1F3CA $ 200D 2640 FE0F; 🏊🏻‍♀️ woman swimming 1F3CA $ 200D 2640 FE0F; 🏊🏻‍♀️ woman swimming
26F9; ⛹ person bouncing ball
26F9 $; ⛹🏻 person bouncing ball
26F9 FE0F 200D 2642 FE0F; ⛹️‍♂️ man bouncing ball 26F9 FE0F 200D 2642 FE0F; ⛹️‍♂️ man bouncing ball
26F9 $ 200D 2642 FE0F; ⛹🏻‍♂️ man bouncing ball 26F9 $ 200D 2642 FE0F; ⛹🏻‍♂️ man bouncing ball
26F9 FE0F 200D 2640 FE0F; ⛹️‍♀️ woman bouncing ball 26F9 FE0F 200D 2640 FE0F; ⛹️‍♀️ woman bouncing ball
26F9 $ 200D 2640 FE0F; ⛹🏻‍♀️ woman bouncing ball 26F9 $ 200D 2640 FE0F; ⛹🏻‍♀️ woman bouncing ball
1F3CB; 🏋 person lifting weights
1F3CB $; 🏋🏻 person lifting weights
1F3CB FE0F 200D 2642 FE0F; 🏋️‍♂️ man lifting weights 1F3CB FE0F 200D 2642 FE0F; 🏋️‍♂️ man lifting weights
1F3CB $ 200D 2642 FE0F; 🏋🏻‍♂️ man lifting weights 1F3CB $ 200D 2642 FE0F; 🏋🏻‍♂️ man lifting weights
1F3CB FE0F 200D 2640 FE0F; 🏋️‍♀️ woman lifting weights 1F3CB FE0F 200D 2640 FE0F; 🏋️‍♀️ woman lifting weights
1F3CB $ 200D 2640 FE0F; 🏋🏻‍♀️ woman lifting weights 1F3CB $ 200D 2640 FE0F; 🏋🏻‍♀️ woman lifting weights
1F6B4; 🚴 person biking
1F6B4 $; 🚴🏻 person biking
1F6B4 200D 2642 FE0F; 🚴‍♂️ man biking 1F6B4 200D 2642 FE0F; 🚴‍♂️ man biking
1F6B4 $ 200D 2642 FE0F; 🚴🏻‍♂️ man biking 1F6B4 $ 200D 2642 FE0F; 🚴🏻‍♂️ man biking
1F6B4 200D 2640 FE0F; 🚴‍♀️ woman biking 1F6B4 200D 2640 FE0F; 🚴‍♀️ woman biking
1F6B4 $ 200D 2640 FE0F; 🚴🏻‍♀️ woman biking 1F6B4 $ 200D 2640 FE0F; 🚴🏻‍♀️ woman biking
1F6B5; 🚵 person mountain biking
1F6B5 $; 🚵🏻 person mountain biking
1F6B5 200D 2642 FE0F; 🚵‍♂️ man mountain biking 1F6B5 200D 2642 FE0F; 🚵‍♂️ man mountain biking
1F6B5 $ 200D 2642 FE0F; 🚵🏻‍♂️ man mountain biking 1F6B5 $ 200D 2642 FE0F; 🚵🏻‍♂️ man mountain biking
1F6B5 200D 2640 FE0F; 🚵‍♀️ woman mountain biking 1F6B5 200D 2640 FE0F; 🚵‍♀️ woman mountain biking
1F6B5 $ 200D 2640 FE0F; 🚵🏻‍♀️ woman mountain biking 1F6B5 $ 200D 2640 FE0F; 🚵🏻‍♀️ woman mountain biking
1F3CE; 🏎 racing car 1F3CE; 🏎 racing car
1F3CD; 🏍 motorcycle 1F3CD; 🏍 motorcycle
1F938; 🤸 person cartwheeling
1F938 $; 🤸🏻 person cartwheeling
1F938 200D 2642 FE0F; 🤸‍♂️ man cartwheeling 1F938 200D 2642 FE0F; 🤸‍♂️ man cartwheeling
1F938 $ 200D 2642 FE0F; 🤸🏻‍♂️ man cartwheeling 1F938 $ 200D 2642 FE0F; 🤸🏻‍♂️ man cartwheeling
1F938 200D 2640 FE0F; 🤸‍♀️ woman cartwheeling 1F938 200D 2640 FE0F; 🤸‍♀️ woman cartwheeling
1F938 $ 200D 2640 FE0F; 🤸🏻‍♀️ woman cartwheeling 1F938 $ 200D 2640 FE0F; 🤸🏻‍♀️ woman cartwheeling
1F93C; 🤼 people wrestling
1F93C 200D 2642 FE0F; 🤼‍♂️ men wrestling 1F93C 200D 2642 FE0F; 🤼‍♂️ men wrestling
1F93C 200D 2640 FE0F; 🤼‍♀️ women wrestling 1F93C 200D 2640 FE0F; 🤼‍♀️ women wrestling
1F93D; 🤽 person playing water polo
1F93D $; 🤽🏻 person playing water polo
1F93D 200D 2642 FE0F; 🤽‍♂️ man playing water polo 1F93D 200D 2642 FE0F; 🤽‍♂️ man playing water polo
1F93D $ 200D 2642 FE0F; 🤽🏻‍♂️ man playing water polo 1F93D $ 200D 2642 FE0F; 🤽🏻‍♂️ man playing water polo
1F93D 200D 2640 FE0F; 🤽‍♀️ woman playing water polo 1F93D 200D 2640 FE0F; 🤽‍♀️ woman playing water polo
1F93D $ 200D 2640 FE0F; 🤽🏻‍♀️ woman playing water polo 1F93D $ 200D 2640 FE0F; 🤽🏻‍♀️ woman playing water polo
1F93E; 🤾 person playing handball
1F93E $; 🤾🏻 person playing handball
1F93E 200D 2642 FE0F; 🤾‍♂️ man playing handball 1F93E 200D 2642 FE0F; 🤾‍♂️ man playing handball
1F93E $ 200D 2642 FE0F; 🤾🏻‍♂️ man playing handball 1F93E $ 200D 2642 FE0F; 🤾🏻‍♂️ man playing handball
1F93E 200D 2640 FE0F; 🤾‍♀️ woman playing handball 1F93E 200D 2640 FE0F; 🤾‍♀️ woman playing handball
1F93E $ 200D 2640 FE0F; 🤾🏻‍♀️ woman playing handball 1F93E $ 200D 2640 FE0F; 🤾🏻‍♀️ woman playing handball
1F939; 🤹 person juggling
1F939 $; 🤹🏻 person juggling
1F939 200D 2642 FE0F; 🤹‍♂️ man juggling 1F939 200D 2642 FE0F; 🤹‍♂️ man juggling
1F939 $ 200D 2642 FE0F; 🤹🏻‍♂️ man juggling 1F939 $ 200D 2642 FE0F; 🤹🏻‍♂️ man juggling
1F939 200D 2640 FE0F; 🤹‍♀️ woman juggling 1F939 200D 2640 FE0F; 🤹‍♀️ woman juggling
1F939 $ 200D 2640 FE0F; 🤹🏻‍♀️ woman juggling 1F939 $ 200D 2640 FE0F; 🤹🏻‍♀️ woman juggling
1F6CC; 🛌 person in bed !! moved
1F6CC $; 🛌🏻 person in bed !! moved
1F6C0; 🛀 person taking bath !! moved
1F6C0 $; 🛀🏻 person taking bath !! moved
1F46B; 👫 man and woman holding hands 1F46B; 👫 man and woman holding hands
1F46C; 👬 two men holding hands 1F46C; 👬 two men holding hands
1F46D; 👭 two women holding hands 1F46D; 👭 two women holding hands
@@ -439,7 +379,7 @@
1F469 200D 1F467; 👩‍👧 family 1F469 200D 1F467; 👩‍👧 family
1F469 200D 1F467 200D 1F466; 👩‍👧‍👦 family 1F469 200D 1F467 200D 1F466; 👩‍👧‍👦 family
1F469 200D 1F467 200D 1F467; 👩‍👧‍👧 family 1F469 200D 1F467 200D 1F467; 👩‍👧‍👧 family
@ !! removed skin tone modifiers @
1F4AA; 💪 flexed biceps 1F4AA; 💪 flexed biceps
1F4AA $; 💪🏻 flexed biceps 1F4AA $; 💪🏻 flexed biceps
1F933; 🤳 selfie 1F933; 🤳 selfie
@@ -505,7 +445,6 @@
1F443; 👃 nose 1F443; 👃 nose
1F443 $; 👃🏻 nose 1F443 $; 👃🏻 nose
1F91D; 🤝 handshake !! moved 1F91D; 🤝 handshake !! moved
@
1F463; 👣 footprints 1F463; 👣 footprints
1F440; 👀 eyes 1F440; 👀 eyes
1F441; 👁 eye 1F441; 👁 eye
@@ -570,7 +509,7 @@
1F484; 💄 lipstick 1F484; 💄 lipstick
1F48D; 💍 ring 1F48D; 💍 ring
1F48E; 💎 gem stone 1F48E; 💎 gem stone
@ @2 no more skin tones beyond this point
1F435; 🐵 monkey face 1F435; 🐵 monkey face
1F412; 🐒 monkey 1F412; 🐒 monkey
1F98D; 🦍 gorilla 1F98D; 🦍 gorilla
@@ -895,14 +834,10 @@
1F6F0; 🛰 satellite 1F6F0; 🛰 satellite
1F6CE; 🛎 bellhop bell 1F6CE; 🛎 bellhop bell
1F6AA; 🚪 door 1F6AA; 🚪 door
1F6CC; 🛌 person in bed
1F6CC $; 🛌🏻 person in bed
1F6CF; 🛏 bed 1F6CF; 🛏 bed
1F6CB; 🛋 couch and lamp 1F6CB; 🛋 couch and lamp
1F6BD; 🚽 toilet 1F6BD; 🚽 toilet
1F6BF; 🚿 shower 1F6BF; 🚿 shower
1F6C0; 🛀 person taking bath
1F6C0 $; 🛀🏻 person taking bath
1F6C1; 🛁 bathtub 1F6C1; 🛁 bathtub
@ @
231B; ⌛ hourglass 231B; ⌛ hourglass

View File

@@ -8,7 +8,7 @@ Custom reply account
chylex chylex
[version] [version]
1.2 1.2.1
[website] [website]
https://tweetduck.chylex.com https://tweetduck.chylex.com

View File

@@ -22,10 +22,19 @@ enabled(){
var section = data.element.closest("section.column"); var section = data.element.closest("section.column");
var column = TD.controller.columnManager.get(section.attr("data-column")); var column = TD.controller.columnManager.get(section.attr("data-column"));
var header = $("h1.column-title", section); var header = $(".column-title", section);
var title = header.children(".column-head-title");
var columnTitle = header.children(".column-head-title").text(); var columnTitle, columnAccount;
var columnAccount = header.children(".attribution").text();
if (title.length){
columnTitle = title.text();
columnAccount = header.children(".attribution").text();
}
else{
columnTitle = header.children(".column-title-edit-box").val();
columnAccount = "";
}
try{ try{
query = configuration.customSelector(column.getColumnType(), columnTitle, columnAccount, column); query = configuration.customSelector(column.getColumnType(), columnTitle, columnAccount, column);

View File

@@ -304,8 +304,8 @@
if (isDetail){ if (isDetail){
setImportantProperty(selectedTweet.find(".js-tweet-media"), "margin-bottom", "0"); setImportantProperty(selectedTweet.find(".js-tweet-media"), "margin-bottom", "0");
selectedTweet.find(".js-translate-call-to-action").first().remove(); selectedTweet.find(".js-translate-call-to-action").first().remove();
selectedTweet.find(".js-cards-container").first().nextAll().remove(); selectedTweet.find(".js-tweet").first().nextAll().remove();
selectedTweet.find(".js-detail-view-inline").first().remove(); selectedTweet.find("footer").last().prev().addBack().remove(); // footer & date
} }
else{ else{
selectedTweet.find("footer").last().remove(); selectedTweet.find("footer").last().remove();
@@ -344,6 +344,11 @@
}; };
var clickUpload = function(){ var clickUpload = function(){
$(document).one("uiFilesAdded", function(){
getScroller().scrollTop(prevScrollTop);
$(".js-drawer").find(".js-compose-text").first()[0].focus();
});
var button = $(".js-add-image-button").first(); var button = $(".js-add-image-button").first();
var scroller = getScroller(); var scroller = getScroller();
@@ -395,13 +400,6 @@
lastPasteElement = null; lastPasteElement = null;
} }
}; };
window.TDGF_tryPasteImageFinish = function(){
setTimeout(function(){
getScroller().scrollTop(prevScrollTop);
$(".js-drawer").find(".js-compose-text").first()[0].focus();
}, 10);
};
})(); })();
// //

View File

@@ -19,6 +19,11 @@
// //
const updateCheckUrlAll = "https://api.github.com/repos/chylex/TweetDuck/releases"; const updateCheckUrlAll = "https://api.github.com/repos/chylex/TweetDuck/releases";
//
// Constant: Fallback url in case the update installer file is missing.
//
const updateDownloadFallback = "https://tweetduck.chylex.com/#download";
// //
// 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.
// //
@@ -28,7 +33,7 @@
var ele = $("#tweetduck-update"); var ele = $("#tweetduck-update");
var existed = ele.length > 0; var existed = ele.length > 0;
if (existed > 0){ if (existed){
ele.remove(); ele.remove();
} }
@@ -106,7 +111,13 @@
buttonDiv.children(".tdu-btn-download").click(function(){ buttonDiv.children(".tdu-btn-download").click(function(){
ele.remove(); ele.remove();
$TDU.onUpdateAccepted(version, download);
if (download){
$TDU.onUpdateAccepted(version, download);
}
else{
$TDU.openBrowser(updateDownloadFallback);
}
}); });
buttonDiv.children(".tdu-btn-unsupported").click(function(){ buttonDiv.children(".tdu-btn-unsupported").click(function(){
@@ -125,12 +136,21 @@
return ele; return ele;
}; };
//
// Function: Returns milliseconds until the start of the next hour, with an extra offset in seconds that can skip an hour if the clock would roll over too soon.
//
var getTimeUntilNextHour = function(extra){
var now = new Date();
var offset = new Date(+now+extra*1000);
return new Date(offset.getFullYear(), offset.getMonth(), offset.getDate(), offset.getHours()+1, 0, 0)-now;
};
// //
// Function: Runs an update check and updates all DOM elements appropriately. // Function: Runs an update check and updates all DOM elements appropriately.
// //
var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){ var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){
clearTimeout(updateCheckTimeoutID); clearTimeout(updateCheckTimeoutID);
updateCheckTimeoutID = setTimeout($TDU.triggerUpdateCheck, 1000*60*60); // 1 hour updateCheckTimeoutID = setTimeout($TDU.triggerUpdateCheck, getTimeUntilNextHour(60*30)); // 30 minute offset
$.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){ $.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){
var release = allowPre ? response[0] : response; var release = allowPre ? response[0] : response;
@@ -139,7 +159,7 @@
var hasUpdate = tagName !== versionTag && tagName !== dismissedVersionTag && release.assets.length > 0; var hasUpdate = tagName !== versionTag && tagName !== dismissedVersionTag && release.assets.length > 0;
if (hasUpdate){ if (hasUpdate){
var obj = release.assets.find(asset => asset.name === updateFileName) || release.assets[0]; var obj = release.assets.find(asset => asset.name === updateFileName) || { browser_download_url: "" };
displayNotification(tagName, obj.browser_download_url); displayNotification(tagName, obj.browser_download_url);
} }

View File

@@ -1,7 +1,7 @@
<?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.53.0.1\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props')" /> <Import Project="packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props')" /> <Import Project="packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.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>
@@ -10,11 +10,10 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetDck</RootNamespace> <RootNamespace>TweetDck</RootNamespace>
<AssemblyName Condition=" '$(Configuration)' == 'Debug' ">TweetDick</AssemblyName> <AssemblyName>TweetDuck</AssemblyName>
<AssemblyName Condition=" '$(Configuration)' == 'Release' ">TweetDuck</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>783c0e30</NuGetPackageImportStamp> <NuGetPackageImportStamp>886d3074</NuGetPackageImportStamp>
<TargetFrameworkProfile> <TargetFrameworkProfile>
</TargetFrameworkProfile> </TargetFrameworkProfile>
<PublishUrl>publish\</PublishUrl> <PublishUrl>publish\</PublishUrl>
@@ -59,9 +58,6 @@
</DefineConstants> </DefineConstants>
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<AssemblyName>TweetDuck</AssemblyName>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@@ -103,16 +99,25 @@
<Compile Include="Core\FormBrowser.Designer.cs"> <Compile Include="Core\FormBrowser.Designer.cs">
<DependentUpon>FormBrowser.cs</DependentUpon> <DependentUpon>FormBrowser.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Core\FormNotification.cs"> <Compile Include="Core\Notification\FormNotificationMain.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Include="Core\FormNotification.Designer.cs"> <Compile Include="Core\Notification\FormNotificationMain.Designer.cs">
<DependentUpon>FormNotification.cs</DependentUpon> <DependentUpon>FormNotificationMain.cs</DependentUpon>
</Compile>
<Compile Include="Core\Notification\FormNotificationBase.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Core\Notification\FormNotificationBase.Designer.cs">
<DependentUpon>FormNotificationBase.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Core\Handling\ContextMenuNotification.cs" /> <Compile Include="Core\Handling\ContextMenuNotification.cs" />
<Compile Include="Core\Handling\FileDialogHandler.cs" /> <Compile Include="Core\Handling\FileDialogHandler.cs" />
<Compile Include="Core\Handling\JavaScriptDialogHandler.cs" /> <Compile Include="Core\Handling\JavaScriptDialogHandler.cs" />
<Compile Include="Core\Handling\LifeSpanHandler.cs" /> <Compile Include="Core\Handling\LifeSpanHandler.cs" />
<Compile Include="Core\Notification\FormNotificationTweet.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Core\Notification\SoundNotification.cs" /> <Compile Include="Core\Notification\SoundNotification.cs" />
<Compile Include="Core\Notification\TweetNotification.cs" /> <Compile Include="Core\Notification\TweetNotification.cs" />
<Compile Include="Core\Other\FormAbout.cs"> <Compile Include="Core\Other\FormAbout.cs">
@@ -256,7 +261,6 @@
<Compile Include="Resources\ScriptLoader.cs" /> <Compile Include="Resources\ScriptLoader.cs" />
<Compile Include="Updates\UpdaterSettings.cs" /> <Compile Include="Updates\UpdaterSettings.cs" />
<None Include="Configuration\app.config" /> <None Include="Configuration\app.config" />
<None Include="Configuration\packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client"> <BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
@@ -308,6 +312,7 @@
</ContentWithTargetPath> </ContentWithTargetPath>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Configuration\packages.config" />
<None Include="Resources\icon-small.ico" /> <None Include="Resources\icon-small.ico" />
<None Include="Resources\icon-tray-new.ico" /> <None Include="Resources\icon-tray-new.ico" />
<None Include="Resources\icon-tray.ico" /> <None Include="Resources\icon-tray.ico" />
@@ -329,12 +334,12 @@
<PropertyGroup> <PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable 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. Enable 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.x86.3.2785.1486\build\cef.redist.x86.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets'))" /> <Error Condition="!Exists('packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets'))" />
<Error Condition="!Exists('packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets'))" /> <Error Condition="!Exists('packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets'))" />
<Error Condition="!Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props'))" /> <Error Condition="!Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets'))" /> <Error Condition="!Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props'))" /> <Error Condition="!Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets'))" /> <Error Condition="!Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets'))" />
</Target> </Target>
<PropertyGroup> <PropertyGroup>
<PostBuildEvent>del "$(TargetPath).config" <PostBuildEvent>del "$(TargetPath).config"
@@ -363,10 +368,10 @@ if $(ConfigurationName) == Debug (
xcopy "$(ProjectDir)Resources\Plugins\.debug\*" "$(TargetDir)plugins\user\.debug\" /E /Y xcopy "$(ProjectDir)Resources\Plugins\.debug\*" "$(TargetDir)plugins\user\.debug\" /E /Y
)</PostBuildEvent> )</PostBuildEvent>
</PropertyGroup> </PropertyGroup>
<Import Project="packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets')" /> <Import Project="packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets')" />
<Import Project="packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets')" /> <Import Project="packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets')" />
<Import Project="packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets')" /> <Import Project="packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets')" />
<Import Project="packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets')" /> <Import Project="packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Windows.Forms; using System.Windows.Forms;
@@ -35,13 +34,13 @@ namespace TweetDck.Updates{
this.updateInfo = info; this.updateInfo = info;
this.UpdateStatus = Status.Waiting; this.UpdateStatus = Status.Waiting;
Disposed += (sender, args) => webClient.Dispose();
webClient.DownloadProgressChanged += webClient_DownloadProgressChanged; webClient.DownloadProgressChanged += webClient_DownloadProgressChanged;
webClient.DownloadFileCompleted += webClient_DownloadFileCompleted; webClient.DownloadFileCompleted += webClient_DownloadFileCompleted;
Text = "Updating "+Program.BrandName; Text = "Updating "+Program.BrandName;
labelDescription.Text = "Downloading version "+info.VersionTag+"..."; labelDescription.Text = "Downloading version "+info.VersionTag+"...";
Disposed += (sender, args) => this.webClient.Dispose();
} }
private void FormUpdateDownload_Shown(object sender, EventArgs e){ private void FormUpdateDownload_Shown(object sender, EventArgs e){
@@ -69,7 +68,7 @@ namespace TweetDck.Updates{
progressDownload.SetValueInstant(1000); progressDownload.SetValueInstant(1000);
} }
labelStatus.Text = (e.BytesReceived/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" kB"; labelStatus.Text = (long)(e.BytesReceived/BytesToKB)+" kB";
} }
else{ else{
if (progressDownload.Style != ProgressBarStyle.Continuous){ if (progressDownload.Style != ProgressBarStyle.Continuous){
@@ -77,7 +76,7 @@ namespace TweetDck.Updates{
} }
progressDownload.SetValueInstant(e.ProgressPercentage*10); progressDownload.SetValueInstant(e.ProgressPercentage*10);
labelStatus.Text = (e.BytesReceived/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" / "+(e.TotalBytesToReceive/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" kB"; labelStatus.Text = (long)(e.BytesReceived/BytesToKB)+" / "+(long)(e.TotalBytesToReceive/BytesToKB)+" kB";
} }
}); });
} }
@@ -93,7 +92,7 @@ namespace TweetDck.Updates{
Program.Reporter.Log(e.Error.ToString()); Program.Reporter.Log(e.Error.ToString());
if (MessageBox.Show("Could not download the update: "+e.Error.Message+"\r\n\r\nDo you want to open the website and try downloading the update manually?", "Update Has Failed", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1) == DialogResult.Yes){ if (MessageBox.Show("Could not download the update: "+e.Error.Message+"\r\n\r\nDo you want to open the website and try downloading the update manually?", "Update Has Failed", MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1) == DialogResult.Yes){
BrowserUtils.OpenExternalBrowser(Program.Website); BrowserUtils.OpenExternalBrowserUnsafe(Program.Website);
UpdateStatus = Status.Manual; UpdateStatus = Status.Manual;
} }
else{ else{

View File

@@ -67,12 +67,12 @@ namespace TweetDck.Updates{
private void TriggerUpdateAcceptedEvent(UpdateAcceptedEventArgs args){ private void TriggerUpdateAcceptedEvent(UpdateAcceptedEventArgs args){
if (UpdateAccepted != null){ if (UpdateAccepted != null){
form.InvokeSafe(() => UpdateAccepted(this, args)); form.InvokeAsyncSafe(() => UpdateAccepted(this, args));
} }
} }
private void TriggerUpdateDismissedEvent(UpdateDismissedEventArgs args){ private void TriggerUpdateDismissedEvent(UpdateDismissedEventArgs args){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
settings.DismissedUpdate = args.VersionTag; settings.DismissedUpdate = args.VersionTag;
if (UpdateDismissed != null){ if (UpdateDismissed != null){
@@ -83,7 +83,7 @@ namespace TweetDck.Updates{
private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){ private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){
if (CheckFinished != null){ if (CheckFinished != null){
form.InvokeSafe(() => CheckFinished(this, args)); form.InvokeAsyncSafe(() => CheckFinished(this, args));
} }
} }

View File

@@ -1,4 +1,5 @@
del "bin\x86\Release\*.xml" del "bin\x86\Release\*.xml"
del "bin\x86\Release\*.pdb"
del "bin\x86\Release\devtools_resources.pak" del "bin\x86\Release\devtools_resources.pak"
del "bin\x86\Release\d3dcompiler_43.dll" del "bin\x86\Release\d3dcompiler_43.dll"
del "bin\x86\Release\widevinecdmadapter.dll" del "bin\x86\Release\widevinecdmadapter.dll"

View File

@@ -39,7 +39,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
[Files] [Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,devtools_resources.pak,d3dcompiler_43.dll,widevinecdmadapter.dll,debug.js" Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.pdb,devtools_resources.pak,d3dcompiler_43.dll,widevinecdmadapter.dll,debug.js"
[Icons] [Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable

View File

@@ -36,17 +36,21 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Files] [Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,devtools_resources.pak,d3dcompiler_43.dll,widevinecdmadapter.dll,debug.js" Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.pdb,devtools_resources.pak,d3dcompiler_43.dll,widevinecdmadapter.dll,debug.js"
[Run] [Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
[Code] [Code]
var UpdatePath: String;
function TDGetNetFrameworkVersion: Cardinal; forward; function TDGetNetFrameworkVersion: Cardinal; forward;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. } { Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. }
function InitializeSetup: Boolean; function InitializeSetup: Boolean;
begin begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
if TDGetNetFrameworkVersion() >= 379893 then if TDGetNetFrameworkVersion() >= 379893 then
begin begin
Result := True; Result := True;
@@ -62,6 +66,21 @@ begin
Result := True; Result := True;
end; end;
{ Set the installation path if updating. }
procedure InitializeWizard();
begin
if (UpdatePath <> '') then
begin
WizardForm.DirEdit.Text := UpdatePath;
end;
end;
{ Skip the install path selection page if running from an update installer. }
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := (PageID = wpSelectDir) and (UpdatePath <> '')
end;
{ Return DWORD value containing the build version of .NET Framework. } { Return DWORD value containing the build version of .NET Framework. }
function TDGetNetFrameworkVersion: Cardinal; function TDGetNetFrameworkVersion: Cardinal;
var FrameworkVersion: Cardinal; var FrameworkVersion: Cardinal;

View File

@@ -8,7 +8,7 @@
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06" #define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe") #define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
#define CefVersion "3.2785.1486.0" #define CefVersion "3.2883.1552.0"
[Setup] [Setup]
AppId={{{#MyAppID}} AppId={{{#MyAppID}}
@@ -41,7 +41,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Files] [Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.dll,*.pak,*.bin,*.dat,debug.js" Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.pdb,*.dll,*.pak,*.bin,*.dat,debug.js"
[Icons] [Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
@@ -72,6 +72,7 @@ function TDIsUninstallable: Boolean; forward;
function TDFindUpdatePath: String; forward; function TDFindUpdatePath: String; forward;
function TDGetNetFrameworkVersion: Cardinal; forward; function TDGetNetFrameworkVersion: Cardinal; forward;
function TDGetAppVersionClean: String; forward; function TDGetAppVersionClean: String; forward;
function TDGetFullDownloadFileName: String; forward;
function TDIsMatchingCEFVersion: Boolean; forward; function TDIsMatchingCEFVersion: Boolean; forward;
procedure TDExecuteFullDownload; forward; procedure TDExecuteFullDownload; forward;
@@ -93,7 +94,7 @@ begin
if not TDIsMatchingCEFVersion() then if not TDIsMatchingCEFVersion() then
begin begin
idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/{#MyAppName}.exe', ExpandConstant('{tmp}\{#MyAppName}.Full.exe')); idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe'));
end; end;
if TDGetNetFrameworkVersion() >= 379893 then if TDGetNetFrameworkVersion() >= 379893 then
@@ -166,14 +167,20 @@ end;
{ Returns a validated installation path (including trailing backslash) using the /UPDATEPATH parameter or installation info in registry. Returns empty string on failure. } { Returns a validated installation path (including trailing backslash) using the /UPDATEPATH parameter or installation info in registry. Returns empty string on failure. }
function TDFindUpdatePath: String; function TDFindUpdatePath: String;
var Path: String; var Path: String;
var RegistryKey: String;
begin begin
Path := ExpandConstant('{param:UPDATEPATH}') Path := ExpandConstant('{param:UPDATEPATH}')
if (Path = '') and not IsPortable and not RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppID}}_is1', 'InstallLocation', Path) then if (Path = '') and not IsPortable then
begin begin
Result := '' RegistryKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppID}}_is1'
Exit
if not (RegQueryStringValue(HKEY_CURRENT_USER, RegistryKey, 'InstallLocation', Path) or RegQueryStringValue(HKEY_LOCAL_MACHINE, RegistryKey, 'InstallLocation', Path)) then
begin
Result := ''
Exit
end;
end; end;
if not FileExists(Path+'{#MyAppExeName}') then if not FileExists(Path+'{#MyAppExeName}') then
@@ -199,6 +206,12 @@ begin
Result := 0; Result := 0;
end; end;
{ Return the name of the full installer file to download from GitHub. }
function TDGetFullDownloadFileName: String;
begin
if IsPortable then Result := '{#MyAppName}.Portable.exe' else Result := '{#MyAppName}.exe';
end;
{ Return whether the version of the installed libcef.dll library matches internal one. } { Return whether the version of the installed libcef.dll library matches internal one. }
function TDIsMatchingCEFVersion: Boolean; function TDIsMatchingCEFVersion: Boolean;
var CEFVersion: String; var CEFVersion: String;

View File

@@ -4,6 +4,37 @@ using TweetDck.Core.Utils;
namespace UnitTests.Core.Utils{ namespace UnitTests.Core.Utils{
[TestClass] [TestClass]
public class TestBrowserUtils{ public class TestBrowserUtils{
[TestMethod]
public void TestIsValidUrl(){
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com")); // base
Assert.IsTrue(BrowserUtils.IsValidUrl("http://www.google.com")); // www.
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.co.uk")); // co.uk
Assert.IsTrue(BrowserUtils.IsValidUrl("https://google.com")); // https
Assert.IsTrue(BrowserUtils.IsValidUrl("ftp://google.com")); // ftp
Assert.IsTrue(BrowserUtils.IsValidUrl("mailto:someone@google.com")); // mailto
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/")); // trailing slash
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/?")); // trailing question mark
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/?a=5&b=x")); // parameters
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/#hash")); // parameters + hash
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/?a=5&b=x#hash")); // parameters + hash
foreach(string tld in new string[]{ "accountants", "blackfriday", "cancerresearch", "coffee", "cool", "foo", "travelersinsurance" }){
Assert.IsTrue(BrowserUtils.IsValidUrl("http://test."+tld)); // long and unusual TLDs
}
Assert.IsFalse(BrowserUtils.IsValidUrl("explorer")); // file
Assert.IsFalse(BrowserUtils.IsValidUrl("explorer.exe")); // file
Assert.IsFalse(BrowserUtils.IsValidUrl("://explorer.exe")); // file-sorta
Assert.IsFalse(BrowserUtils.IsValidUrl("file://explorer.exe")); // file-proper
Assert.IsFalse(BrowserUtils.IsValidUrl("")); // empty
Assert.IsFalse(BrowserUtils.IsValidUrl("lol")); // random
Assert.IsFalse(BrowserUtils.IsValidUrl("gopher://nobody.cares")); // lmao rekt
}
[TestMethod] [TestMethod]
public void TestGetFileNameFromUrl(){ public void TestGetFileNameFromUrl(){
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html")); Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html"));