mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 10:32:10 +02:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
6387ab41b3 | |||
4df16b7f15 | |||
ed387a2873 | |||
9e225530a6 | |||
7b23686dc6 | |||
4de31453fd | |||
4c59526e39 | |||
9ec1764194 | |||
47afa32902 | |||
2a09487b55 | |||
563c856dd3 | |||
69ea242408 | |||
d6e0e0726f | |||
73d460d40a | |||
1f27d96ac9 | |||
93e9f28d69 | |||
ec2e26752a | |||
fadd95f3e6 | |||
00acc677e6 | |||
1a799881e8 | |||
f75677593a | |||
19e3bd19f0 | |||
85701b0a3c | |||
014cb18dcb | |||
e71e1c853f | |||
ee9d9196f5 | |||
53c8272e01 | |||
7f7b6b1e2a | |||
405777e0f5 | |||
df2b624cb5 | |||
8a48d5c2f9 | |||
c55ee71442 | |||
3f82745f5b | |||
404187a1ae | |||
2b7b3f586b | |||
04959a3493 | |||
97cf4932ae | |||
b0d88a0a37 | |||
67a2e40622 |
@@ -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){
|
||||||
|
@@ -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>
|
@@ -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){
|
||||||
|
@@ -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()");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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:
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
66
Core/Notification/FormNotificationBase.Designer.cs
generated
Normal file
66
Core/Notification/FormNotificationBase.Designer.cs
generated
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
206
Core/Notification/FormNotificationBase.cs
Normal file
206
Core/Notification/FormNotificationBase.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
243
Core/Notification/FormNotificationMain.cs
Normal file
243
Core/Notification/FormNotificationMain.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
Core/Notification/FormNotificationTweet.cs
Normal file
85
Core/Notification/FormNotificationTweet.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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){
|
||||||
|
@@ -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){
|
||||||
|
@@ -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){
|
||||||
|
@@ -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;
|
||||||
|
@@ -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){
|
||||||
|
@@ -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)){}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
28
Program.cs
28
Program.cs
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
||||||
|
@@ -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);
|
};
|
||||||
|
|
||||||
|
var selectSkinTone = skinTone => {
|
||||||
|
let selectedEle = this.currentKeyboard.children[1].querySelector("[data-tone='"+this.selectedSkinTone+"']");
|
||||||
|
selectedEle && selectedEle.classList.remove("sel");
|
||||||
|
|
||||||
created.classList.add("emoji-keyboard");
|
this.selectedSkinTone = skinTone;
|
||||||
created.style.left = left+"px";
|
this.currentKeyboard.children[0].innerHTML = generateEmojiHTML(skinTone);
|
||||||
created.style.top = top+"px";
|
this.currentKeyboard.children[1].querySelector("[data-tone='"+this.selectedSkinTone+"']").classList.add("sel");
|
||||||
created.innerHTML = this.emojiHTML;
|
};
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
created.addEventListener("click", function(e){
|
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);
|
||||||
});
|
});
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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);
|
||||||
@@ -134,4 +143,4 @@ disabled(){
|
|||||||
$(document).off("uiInlineComposeTweet", this.uiComposeTweetEvent);
|
$(document).off("uiInlineComposeTweet", this.uiComposeTweetEvent);
|
||||||
$(document).off("uiDockedComposeTweet", this.uiComposeTweetEvent);
|
$(document).off("uiDockedComposeTweet", this.uiComposeTweetEvent);
|
||||||
$(document).off("click", ".js-account-list .js-account-item", this.onSelectedAccountChanged);
|
$(document).off("click", ".js-account-list .js-account-item", this.onSelectedAccountChanged);
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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">
|
||||||
|
@@ -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{
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
@@ -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"));
|
||||||
|
Reference in New Issue
Block a user