mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 19:32:10 +02:00
Compare commits
65 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 | |||
3a28556c7f | |||
9ecc92b9a5 | |||
ca023be98a | |||
11a1423f76 | |||
79f6df121b | |||
71eade7e86 | |||
5f81d29036 | |||
ec1cb5dc5f | |||
fd969e2d55 | |||
37e33b77ff | |||
f7ed7703b4 | |||
4bb35295ca | |||
1e4f673f9e | |||
7cadb1c403 | |||
37148f5093 | |||
f6bc26789f | |||
b3f5a88525 | |||
1e538d2b28 | |||
7d7bfb7b01 | |||
41d86ba440 | |||
3df474a8a5 | |||
a50d6e8f47 | |||
6081e5b9c1 | |||
66ccea920c | |||
470d63093f | |||
eae0507831 |
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using TweetDck.Core.Utils;
|
||||
|
||||
namespace TweetDck.Configuration{
|
||||
sealed class LockManager{
|
||||
@@ -114,25 +115,45 @@ namespace TweetDck.Configuration{
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool CloseLockingProcess(int timeout){
|
||||
public bool CloseLockingProcess(int closeTimeout, int killTimeout){
|
||||
if (LockingProcess != null){
|
||||
LockingProcess.CloseMainWindow();
|
||||
try{
|
||||
if (LockingProcess.CloseMainWindow()){
|
||||
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, closeTimeout, 250);
|
||||
}
|
||||
|
||||
for(int waited = 0; waited < timeout && !LockingProcess.HasExited; waited += 250){
|
||||
LockingProcess.Refresh();
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
if (!LockingProcess.HasExited){
|
||||
LockingProcess.Kill();
|
||||
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, killTimeout, 250);
|
||||
}
|
||||
|
||||
if (LockingProcess.HasExited){
|
||||
LockingProcess.Dispose();
|
||||
LockingProcess = null;
|
||||
return true;
|
||||
if (LockingProcess.HasExited){
|
||||
LockingProcess.Dispose();
|
||||
LockingProcess = null;
|
||||
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;
|
||||
}
|
||||
|
||||
private bool CheckLockingProcessExited(){
|
||||
LockingProcess.Refresh();
|
||||
return LockingProcess.HasExited;
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
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">
|
||||
<package id="cef.redist.x64" version="3.2785.1486" targetFramework="net452" />
|
||||
<package id="cef.redist.x86" version="3.2785.1486" targetFramework="net452" />
|
||||
<package id="CefSharp.Common" version="53.0.1" targetFramework="net452" />
|
||||
<package id="CefSharp.WinForms" version="53.0.1" targetFramework="net452" />
|
||||
<package id="Microsoft.VC120.CRT.JetBrains" version="12.0.21005.2" targetFramework="net452" />
|
||||
<package id="cef.redist.x64" version="3.2883.1552" targetFramework="net452" xmlns="" />
|
||||
<package id="cef.redist.x86" version="3.2883.1552" targetFramework="net452" xmlns="" />
|
||||
<package id="CefSharp.Common" version="55.0.0" targetFramework="net452" xmlns="" />
|
||||
<package id="CefSharp.WinForms" version="55.0.0" targetFramework="net452" xmlns="" />
|
||||
<package id="Microsoft.VC120.CRT.JetBrains" version="12.0.21005.2" targetFramework="net452" xmlns="" />
|
||||
</packages>
|
@@ -19,57 +19,57 @@ namespace TweetDck.Core.Bridge{
|
||||
}
|
||||
|
||||
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.notification = notification;
|
||||
}
|
||||
|
||||
public void LoadFontSizeClass(string fsClass){
|
||||
form.InvokeSafe(() => {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
TweetNotification.SetFontSizeClass(fsClass);
|
||||
});
|
||||
}
|
||||
|
||||
public void LoadNotificationHeadContents(string headContents){
|
||||
form.InvokeSafe(() => {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
TweetNotification.SetHeadTag(headContents);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetLastRightClickedLink(string link){
|
||||
form.InvokeSafe(() => LastRightClickedLink = link);
|
||||
form.InvokeAsyncSafe(() => LastRightClickedLink = link);
|
||||
}
|
||||
|
||||
public void SetLastHighlightedTweet(string link, string quotedLink){
|
||||
form.InvokeSafe(() => {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
LastHighlightedTweet = link;
|
||||
LastHighlightedQuotedTweet = quotedLink;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetNotificationQuotedTweet(string link){
|
||||
notification.InvokeSafe(() => notification.CurrentQuotedTweetUrl = link);
|
||||
notification.InvokeAsyncSafe(() => notification.CurrentQuotedTweetUrl = link);
|
||||
}
|
||||
|
||||
public void OpenSettingsMenu(){
|
||||
form.InvokeSafe(form.OpenSettings);
|
||||
form.InvokeAsyncSafe(form.OpenSettings);
|
||||
}
|
||||
|
||||
public void OpenPluginsMenu(){
|
||||
form.InvokeSafe(form.OpenPlugins);
|
||||
form.InvokeAsyncSafe(form.OpenPlugins);
|
||||
}
|
||||
|
||||
public void OnTweetPopup(string tweetHtml, string tweetUrl, int tweetCharacters){
|
||||
notification.InvokeSafe(() => {
|
||||
notification.InvokeAsyncSafe(() => {
|
||||
form.OnTweetNotification();
|
||||
notification.ShowNotification(new TweetNotification(tweetHtml, tweetUrl, tweetCharacters));
|
||||
});
|
||||
}
|
||||
|
||||
public void OnTweetSound(){
|
||||
form.InvokeSafe(() => {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
form.OnTweetNotification();
|
||||
form.PlayNotificationSound();
|
||||
});
|
||||
@@ -77,15 +77,15 @@ namespace TweetDck.Core.Bridge{
|
||||
|
||||
public void DisplayTooltip(string text, bool showInNotification){
|
||||
if (showInNotification){
|
||||
notification.InvokeSafe(() => notification.DisplayTooltip(text));
|
||||
notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
|
||||
}
|
||||
else{
|
||||
form.InvokeSafe(() => form.DisplayTooltip(text));
|
||||
form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadNextNotification(){
|
||||
notification.InvokeSafe(notification.FinishCurrentTweet);
|
||||
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
|
||||
}
|
||||
|
||||
public void TryPasteImage(){
|
||||
@@ -109,23 +109,15 @@ namespace TweetDck.Core.Bridge{
|
||||
}
|
||||
|
||||
public void ClickUploadImage(int offsetX, int offsetY){
|
||||
form.InvokeSafe(() => {
|
||||
Point prevPos = Cursor.Position;
|
||||
|
||||
Cursor.Position = form.PointToScreen(new Point(offsetX, offsetY));
|
||||
NativeMethods.SimulateMouseClick(NativeMethods.MouseButton.Left);
|
||||
Cursor.Position = prevPos;
|
||||
|
||||
form.OnImagePastedFinish();
|
||||
});
|
||||
form.InvokeAsyncSafe(() => form.TriggerImageUpload(offsetX, offsetY));
|
||||
}
|
||||
|
||||
public void ScreenshotTweet(string html, int width, int height){
|
||||
form.InvokeSafe(() => form.OnTweetScreenshotReady(html, width, height));
|
||||
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
|
||||
}
|
||||
|
||||
public void FixClipboard(){
|
||||
form.InvokeSafe(WindowsUtils.ClipboardStripHtmlStyles);
|
||||
form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
|
||||
}
|
||||
|
||||
public void OpenBrowser(string url){
|
||||
|
@@ -16,6 +16,10 @@ namespace TweetDck.Core.Controls{
|
||||
}
|
||||
}
|
||||
|
||||
public static void InvokeAsyncSafe(this Control control, Action func){
|
||||
control.BeginInvoke(func);
|
||||
}
|
||||
|
||||
public static void MoveToCenter(this Form targetForm, Form parentForm){
|
||||
targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2);
|
||||
}
|
||||
|
@@ -13,12 +13,11 @@ using TweetDck.Updates;
|
||||
using TweetDck.Plugins;
|
||||
using TweetDck.Plugins.Enums;
|
||||
using TweetDck.Plugins.Events;
|
||||
using System.Media;
|
||||
using TweetDck.Core.Bridge;
|
||||
using TweetDck.Core.Notification;
|
||||
using TweetDck.Core.Notification.Screenshot;
|
||||
using TweetDck.Updates.Events;
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TweetDck.Core{
|
||||
sealed partial class FormBrowser : Form{
|
||||
@@ -33,7 +32,7 @@ namespace TweetDck.Core{
|
||||
private readonly ChromiumWebBrowser browser;
|
||||
private readonly PluginManager plugins;
|
||||
private readonly UpdateHandler updates;
|
||||
private readonly FormNotification notification;
|
||||
private readonly FormNotificationTweet notification;
|
||||
|
||||
private FormSettings currentFormSettings;
|
||||
private FormAbout currentFormAbout;
|
||||
@@ -43,8 +42,7 @@ namespace TweetDck.Core{
|
||||
private FormWindowState prevState;
|
||||
|
||||
private TweetScreenshotManager notificationScreenshotManager;
|
||||
private SoundPlayer notificationSound;
|
||||
private bool ignoreNotificationSoundError;
|
||||
private SoundNotification soundNotification;
|
||||
|
||||
public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
|
||||
InitializeComponent();
|
||||
@@ -55,12 +53,14 @@ namespace TweetDck.Core{
|
||||
this.plugins.Reloaded += plugins_Reloaded;
|
||||
this.plugins.PluginChangedState += plugins_PluginChangedState;
|
||||
|
||||
this.notification = CreateNotificationForm(NotificationFlags.AutoHide | NotificationFlags.TopMost);
|
||||
#if DEBUG
|
||||
this.notification.CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt;
|
||||
#else
|
||||
this.notification.CanMoveWindow = () => false;
|
||||
#endif
|
||||
this.notification = new FormNotificationTweet(this, plugins, NotificationFlags.TopMost){
|
||||
#if DEBUG
|
||||
CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt
|
||||
#else
|
||||
CanMoveWindow = () => false
|
||||
#endif
|
||||
};
|
||||
|
||||
this.notification.Show();
|
||||
|
||||
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
|
||||
@@ -90,8 +90,8 @@ namespace TweetDck.Core{
|
||||
notificationScreenshotManager.Dispose();
|
||||
}
|
||||
|
||||
if (notificationSound != null){
|
||||
notificationSound.Dispose();
|
||||
if (soundNotification != null){
|
||||
soundNotification.Dispose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace TweetDck.Core{
|
||||
}
|
||||
|
||||
private void plugins_Reloaded(object sender, PluginLoadEventArgs e){
|
||||
ReloadBrowser();
|
||||
browser.GetBrowser().Reload();
|
||||
}
|
||||
|
||||
private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
|
||||
@@ -246,6 +246,7 @@ namespace TweetDck.Core{
|
||||
FormUpdateDownload downloadForm = new FormUpdateDownload(e.UpdateInfo);
|
||||
downloadForm.MoveToCenter(this);
|
||||
downloadForm.ShowDialog();
|
||||
downloadForm.Dispose();
|
||||
|
||||
if (downloadForm.UpdateStatus == FormUpdateDownload.Status.Succeeded){
|
||||
UpdateInstallerPath = downloadForm.InstallerPath;
|
||||
@@ -266,7 +267,12 @@ namespace TweetDck.Core{
|
||||
|
||||
protected override void WndProc(ref Message m){
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -280,8 +286,8 @@ namespace TweetDck.Core{
|
||||
|
||||
// notification helpers
|
||||
|
||||
public FormNotification CreateNotificationForm(NotificationFlags flags){
|
||||
return new FormNotification(this, plugins, flags);
|
||||
public FormNotificationMain CreateNotificationForm(NotificationFlags flags){
|
||||
return new FormNotificationMain(this, plugins, flags);
|
||||
}
|
||||
|
||||
public void PauseNotification(){
|
||||
@@ -305,13 +311,17 @@ namespace TweetDck.Core{
|
||||
// callback handlers
|
||||
|
||||
public void OpenSettings(){
|
||||
OpenSettings(0);
|
||||
}
|
||||
|
||||
public void OpenSettings(int tabIndex){
|
||||
if (currentFormSettings != null){
|
||||
currentFormSettings.BringToFront();
|
||||
}
|
||||
else{
|
||||
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
|
||||
|
||||
currentFormSettings = new FormSettings(this, plugins, updates);
|
||||
currentFormSettings = new FormSettings(this, plugins, updates, tabIndex);
|
||||
|
||||
currentFormSettings.FormClosed += (sender, args) => {
|
||||
currentFormSettings = null;
|
||||
@@ -368,45 +378,11 @@ namespace TweetDck.Core{
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationSound == null){
|
||||
notificationSound = new SoundPlayer{
|
||||
LoadTimeout = 5000
|
||||
};
|
||||
if (soundNotification == null){
|
||||
soundNotification = new SoundNotification(this);
|
||||
}
|
||||
|
||||
if (notificationSound.SoundLocation != Config.NotificationSoundPath){
|
||||
notificationSound.SoundLocation = Config.NotificationSoundPath;
|
||||
ignoreNotificationSoundError = false;
|
||||
}
|
||||
|
||||
try{
|
||||
notificationSound.Play();
|
||||
}catch(FileNotFoundException e){
|
||||
OnNotificationSoundError("File not found: "+e.FileName);
|
||||
}catch(InvalidOperationException){
|
||||
OnNotificationSoundError("File is not a valid sound file.");
|
||||
}catch(TimeoutException){
|
||||
OnNotificationSoundError("File took too long to load.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNotificationSoundError(string message){
|
||||
if (!ignoreNotificationSoundError){
|
||||
ignoreNotificationSoundError = true;
|
||||
|
||||
using(FormMessage form = new FormMessage("Notification Sound Error", "Could not play custom notification sound."+Environment.NewLine+message, MessageBoxIcon.Error)){
|
||||
form.AddButton("Ignore");
|
||||
|
||||
Button btnOpenSettings = form.AddButton("Open Settings");
|
||||
btnOpenSettings.Width += 16;
|
||||
btnOpenSettings.Location = new Point(btnOpenSettings.Location.X-16, btnOpenSettings.Location.Y);
|
||||
|
||||
if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnOpenSettings){
|
||||
OpenSettings();
|
||||
currentFormSettings.SelectTab(FormSettings.TabIndexNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
soundNotification.Play(Config.NotificationSoundPath);
|
||||
}
|
||||
|
||||
public void OnTweetScreenshotReady(string html, int width, int height){
|
||||
@@ -432,16 +408,14 @@ namespace TweetDck.Core{
|
||||
browser.ExecuteScriptAsync("TDGF_tryPasteImage()");
|
||||
}
|
||||
|
||||
public void OnImagePastedFinish(){
|
||||
browser.ExecuteScriptAsync("TDGF_tryPasteImageFinish()");
|
||||
public void TriggerImageUpload(int offsetX, int offsetY){
|
||||
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(){
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -101,7 +101,7 @@ namespace TweetDck.Core.Handling{
|
||||
}
|
||||
|
||||
protected void SetClipboardText(string text){
|
||||
form.InvokeSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
|
||||
form.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
|
||||
}
|
||||
|
||||
protected static void RemoveSeparatorIfLast(IMenuModel model){
|
||||
|
@@ -95,19 +95,19 @@ namespace TweetDck.Core.Handling{
|
||||
return true;
|
||||
|
||||
case MenuSettings:
|
||||
form.InvokeSafe(form.OpenSettings);
|
||||
form.InvokeAsyncSafe(form.OpenSettings);
|
||||
return true;
|
||||
|
||||
case MenuAbout:
|
||||
form.InvokeSafe(form.OpenAbout);
|
||||
form.InvokeAsyncSafe(form.OpenAbout);
|
||||
return true;
|
||||
|
||||
case MenuPlugins:
|
||||
form.InvokeSafe(form.OpenPlugins);
|
||||
form.InvokeAsyncSafe(form.OpenPlugins);
|
||||
return true;
|
||||
|
||||
case MenuMute:
|
||||
form.InvokeSafe(() => {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
Program.UserConfig.MuteNotifications = !Program.UserConfig.MuteNotifications;
|
||||
Program.UserConfig.Save();
|
||||
});
|
||||
@@ -123,7 +123,7 @@ namespace TweetDck.Core.Handling{
|
||||
return true;
|
||||
|
||||
case MenuScreenshotTweet:
|
||||
form.InvokeSafe(form.TriggerTweetScreenshot);
|
||||
form.InvokeAsyncSafe(form.TriggerTweetScreenshot);
|
||||
return true;
|
||||
|
||||
case MenuOpenQuotedTweetUrl:
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using CefSharp;
|
||||
using TweetDck.Core.Controls;
|
||||
using TweetDck.Core.Notification;
|
||||
|
||||
namespace TweetDck.Core.Handling{
|
||||
class ContextMenuNotification : ContextMenuBase{
|
||||
@@ -8,10 +9,10 @@ namespace TweetDck.Core.Handling{
|
||||
private const int MenuCopyTweetUrl = 26602;
|
||||
private const int MenuCopyQuotedTweetUrl = 26603;
|
||||
|
||||
private readonly FormNotification form;
|
||||
private readonly FormNotificationBase form;
|
||||
private readonly bool enableCustomMenu;
|
||||
|
||||
public ContextMenuNotification(FormNotification form, bool enableCustomMenu) : base(form){
|
||||
public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu) : base(form){
|
||||
this.form = form;
|
||||
this.enableCustomMenu = enableCustomMenu;
|
||||
}
|
||||
@@ -49,7 +50,7 @@ namespace TweetDck.Core.Handling{
|
||||
|
||||
RemoveSeparatorIfLast(model);
|
||||
|
||||
form.InvokeSafe(() => form.ContextMenuOpen = true);
|
||||
form.InvokeAsyncSafe(() => form.ContextMenuOpen = true);
|
||||
}
|
||||
|
||||
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
|
||||
@@ -59,11 +60,11 @@ namespace TweetDck.Core.Handling{
|
||||
|
||||
switch((int)commandId){
|
||||
case MenuSkipTweet:
|
||||
form.InvokeSafe(form.FinishCurrentTweet);
|
||||
form.InvokeAsyncSafe(form.FinishCurrentNotification);
|
||||
return true;
|
||||
|
||||
case MenuFreeze:
|
||||
form.InvokeSafe(() => form.FreezeTimer = !form.FreezeTimer);
|
||||
form.InvokeAsyncSafe(() => form.FreezeTimer = !form.FreezeTimer);
|
||||
return true;
|
||||
|
||||
case MenuCopyTweetUrl:
|
||||
@@ -80,7 +81,7 @@ namespace TweetDck.Core.Handling{
|
||||
|
||||
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
|
||||
base.OnContextMenuDismissed(browserControl, browser, frame);
|
||||
form.InvokeSafe(() => form.ContextMenuOpen = false);
|
||||
form.InvokeAsyncSafe(() => form.ContextMenuOpen = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,10 +7,11 @@ namespace TweetDck.Core.Handling{
|
||||
newBrowser = null;
|
||||
|
||||
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.NewForegroundTab:
|
||||
case WindowOpenDisposition.NewPopup:
|
||||
case WindowOpenDisposition.NewWindow:
|
||||
// TODO case WindowOpenDisposition.NewWindow:
|
||||
BrowserUtils.OpenExternalBrowser(targetUrl);
|
||||
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 {
|
||||
partial class FormNotification {
|
||||
namespace TweetDck.Core.Notification {
|
||||
partial class FormNotificationMain {
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
@@ -26,23 +24,15 @@ namespace TweetDck.Core {
|
||||
/// </summary>
|
||||
private void InitializeComponent() {
|
||||
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.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();
|
||||
//
|
||||
// panelBrowser
|
||||
// timerDisplayDelay
|
||||
//
|
||||
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, 118);
|
||||
this.panelBrowser.TabIndex = 0;
|
||||
this.timerDisplayDelay.Interval = 17;
|
||||
this.timerDisplayDelay.Tick += new System.EventHandler(this.timerDisplayDelay_Tick);
|
||||
//
|
||||
// timerProgress
|
||||
//
|
||||
@@ -62,11 +52,6 @@ namespace TweetDck.Core {
|
||||
this.progressBarTimer.Size = new System.Drawing.Size(284, 4);
|
||||
this.progressBarTimer.TabIndex = 1;
|
||||
//
|
||||
// timerDisplayDelay
|
||||
//
|
||||
this.timerDisplayDelay.Interval = 17;
|
||||
this.timerDisplayDelay.Tick += new System.EventHandler(this.timerDisplayDelay_Tick);
|
||||
//
|
||||
// FormNotification
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
@@ -74,15 +59,6 @@ namespace TweetDck.Core {
|
||||
this.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.ClientSize = new System.Drawing.Size(284, 122);
|
||||
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.ResumeLayout(false);
|
||||
|
||||
@@ -90,10 +66,8 @@ namespace TweetDck.Core {
|
||||
|
||||
#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;
|
||||
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]
|
||||
public enum NotificationFlags{
|
||||
None = 0,
|
||||
AutoHide = 1,
|
||||
DisableScripts = 2,
|
||||
DisableContextMenu = 4,
|
||||
TopMost = 8,
|
||||
ManualDisplay = 16
|
||||
DisableContextMenu = 1,
|
||||
TopMost = 2
|
||||
}
|
||||
}
|
||||
|
@@ -4,10 +4,13 @@ using CefSharp;
|
||||
using TweetDck.Core.Bridge;
|
||||
using TweetDck.Core.Controls;
|
||||
using TweetDck.Resources;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using TweetDck.Core.Utils;
|
||||
|
||||
namespace TweetDck.Core.Notification.Screenshot{
|
||||
sealed class FormNotificationScreenshotable : FormNotification{
|
||||
public FormNotificationScreenshotable(Action callback, FormBrowser owner, NotificationFlags flags) : base(owner, null, flags){
|
||||
sealed class FormNotificationScreenshotable : FormNotificationBase{
|
||||
public FormNotificationScreenshotable(Action callback, Form owner, NotificationFlags flags) : base(owner, flags){
|
||||
browser.RegisterAsyncJsObject("$TD_NotificationScreenshot", new CallbackBridge(this, callback));
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
public void LoadNotificationForScreenshot(TweetNotification tweet, int width, int height){
|
||||
browser.LoadHtml(tweet.GenerateHtml(enableCustomCSS: false), "http://tweetdeck.twitter.com/?"+DateTime.Now.Ticks);
|
||||
|
||||
Location = ControlExtensions.InvisibleLocation;
|
||||
FormBorderStyle = Program.UserConfig.ShowScreenshotBorder ? FormBorderStyle.FixedToolWindow : FormBorderStyle.None;
|
||||
|
||||
SetNotificationSize(width, height, false);
|
||||
SetNotificationSize(width, height);
|
||||
}
|
||||
|
||||
public void TakeScreenshotAndHide(){
|
||||
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();
|
||||
}
|
||||
|
||||
|
@@ -4,14 +4,11 @@ using TweetDck.Core.Utils;
|
||||
|
||||
namespace TweetDck.Core.Notification.Screenshot{
|
||||
sealed class TweetScreenshotManager : IDisposable{
|
||||
private readonly FormBrowser browser;
|
||||
private readonly FormNotificationScreenshotable screenshot;
|
||||
private readonly Timer timeout;
|
||||
|
||||
public TweetScreenshotManager(FormBrowser browser){
|
||||
this.browser = browser;
|
||||
|
||||
this.screenshot = new FormNotificationScreenshotable(Callback, browser, NotificationFlags.DisableScripts | NotificationFlags.DisableContextMenu | NotificationFlags.TopMost | NotificationFlags.ManualDisplay){
|
||||
public TweetScreenshotManager(Form owner){
|
||||
this.screenshot = new FormNotificationScreenshotable(Callback, owner, NotificationFlags.DisableContextMenu | NotificationFlags.TopMost){
|
||||
CanMoveWindow = () => false
|
||||
};
|
||||
|
||||
@@ -31,15 +28,12 @@ namespace TweetDck.Core.Notification.Screenshot{
|
||||
}
|
||||
|
||||
timeout.Stop();
|
||||
|
||||
browser.PauseNotification();
|
||||
screenshot.TakeScreenshotAndHide();
|
||||
browser.ResumeNotification();
|
||||
}
|
||||
|
||||
public void Dispose(){
|
||||
timeout.Dispose();
|
||||
screenshot.Dispose();
|
||||
timeout.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
Core/Notification/SoundNotification.cs
Normal file
66
Core/Notification/SoundNotification.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Media;
|
||||
using System.Windows.Forms;
|
||||
using TweetDck.Core.Other;
|
||||
|
||||
namespace TweetDck.Core.Notification{
|
||||
class SoundNotification : IDisposable{
|
||||
private readonly FormBrowser browserForm;
|
||||
|
||||
private SoundPlayer notificationSound;
|
||||
private bool ignoreNotificationSoundError;
|
||||
|
||||
public SoundNotification(FormBrowser browserForm){
|
||||
this.browserForm = browserForm;
|
||||
}
|
||||
|
||||
public void Play(string file){
|
||||
if (notificationSound == null){
|
||||
notificationSound = new SoundPlayer{
|
||||
LoadTimeout = 5000
|
||||
};
|
||||
}
|
||||
|
||||
if (notificationSound.SoundLocation != file){
|
||||
notificationSound.SoundLocation = file;
|
||||
ignoreNotificationSoundError = false;
|
||||
}
|
||||
|
||||
try{
|
||||
notificationSound.Play();
|
||||
}catch(FileNotFoundException e){
|
||||
OnNotificationSoundError("File not found: "+e.FileName);
|
||||
}catch(InvalidOperationException){
|
||||
OnNotificationSoundError("File is not a valid sound file.");
|
||||
}catch(TimeoutException){
|
||||
OnNotificationSoundError("File took too long to load.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNotificationSoundError(string message){
|
||||
if (!ignoreNotificationSoundError){
|
||||
ignoreNotificationSoundError = true;
|
||||
|
||||
using(FormMessage form = new FormMessage("Notification Sound Error", "Could not play custom notification sound."+Environment.NewLine+message, MessageBoxIcon.Error)){
|
||||
form.AddButton("Ignore");
|
||||
|
||||
Button btnOpenSettings = form.AddButton("Open Settings");
|
||||
btnOpenSettings.Width += 16;
|
||||
btnOpenSettings.Location = new Point(btnOpenSettings.Location.X-16, btnOpenSettings.Location.Y);
|
||||
|
||||
if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnOpenSettings){
|
||||
browserForm.OpenSettings(FormSettings.TabIndexNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose(){
|
||||
if (notificationSound != null){
|
||||
notificationSound.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@ namespace TweetDck.Core.Other{
|
||||
}
|
||||
|
||||
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.tabBtnCustom = tabPanelPlugins.AddButton("", () => SelectGroup(PluginGroup.Custom));
|
||||
|
||||
this.tabPanelPlugins.SelectTab(tabBtnOfficial);
|
||||
this.pluginManager_Reloaded(pluginManager, null);
|
||||
|
||||
Shown += (sender, args) => {
|
||||
Program.UserConfig.PluginsWindow.Restore(this, false);
|
||||
this.tabPanelPlugins.SelectTab(tabBtnOfficial);
|
||||
};
|
||||
|
||||
FormClosed += (sender, args) => {
|
||||
Program.UserConfig.PluginsWindow.Save(this);
|
||||
Program.UserConfig.Save();
|
||||
};
|
||||
|
||||
Disposed += (sender, args) => this.pluginManager.Reloaded -= pluginManager_Reloaded;
|
||||
}
|
||||
|
||||
private void SelectGroup(PluginGroup group){
|
||||
@@ -76,8 +78,8 @@ namespace TweetDck.Core.Other{
|
||||
}
|
||||
}
|
||||
|
||||
flowLayoutPlugins_Resize(flowLayoutPlugins, new EventArgs());
|
||||
flowLayoutPlugins.ResumeLayout(true);
|
||||
flowLayoutPlugins_Resize(flowLayoutPlugins, new EventArgs());
|
||||
}
|
||||
|
||||
private void pluginManager_Reloaded(object sender, PluginLoadEventArgs e){
|
||||
|
@@ -13,10 +13,9 @@ namespace TweetDck.Core.Other{
|
||||
|
||||
private readonly FormBrowser browser;
|
||||
private readonly Dictionary<Type, BaseTabSettings> tabs = new Dictionary<Type, BaseTabSettings>(4);
|
||||
private readonly bool hasFinishedLoading;
|
||||
|
||||
private bool wasTabSelectedAutomatically;
|
||||
|
||||
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates){
|
||||
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, int startTabIndex = 0){
|
||||
InitializeComponent();
|
||||
|
||||
Text = Program.BrandName+" Settings";
|
||||
@@ -26,16 +25,12 @@ namespace TweetDck.Core.Other{
|
||||
|
||||
this.tabPanel.SetupTabPanel(100);
|
||||
this.tabPanel.AddButton("General", SelectTab<TabSettingsGeneral>);
|
||||
this.tabPanel.AddButton("Notifications", () => SelectTab(() => new TabSettingsNotifications(browser.CreateNotificationForm(NotificationFlags.DisableContextMenu), wasTabSelectedAutomatically)));
|
||||
this.tabPanel.AddButton("Notifications", () => SelectTab(() => new TabSettingsNotifications(browser.CreateNotificationForm(NotificationFlags.DisableContextMenu), !hasFinishedLoading)));
|
||||
this.tabPanel.AddButton("Updates", () => SelectTab(() => new TabSettingsUpdates(updates)));
|
||||
this.tabPanel.AddButton("Advanced", () => SelectTab(() => new TabSettingsAdvanced(browser.ReinjectCustomCSS, plugins)));
|
||||
this.tabPanel.SelectTab(tabPanel.Buttons.First());
|
||||
}
|
||||
|
||||
public void SelectTab(int index){
|
||||
wasTabSelectedAutomatically = true;
|
||||
this.tabPanel.SelectTab(tabPanel.Buttons.ElementAt(index));
|
||||
wasTabSelectedAutomatically = false;
|
||||
this.tabPanel.SelectTab(tabPanel.Buttons.ElementAt(startTabIndex));
|
||||
hasFinishedLoading = true;
|
||||
}
|
||||
|
||||
private void SelectTab<T>() where T : BaseTabSettings, new(){
|
||||
|
@@ -44,7 +44,7 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
|
||||
}
|
||||
|
||||
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){
|
||||
|
@@ -22,7 +22,7 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
|
||||
}
|
||||
|
||||
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){
|
||||
|
@@ -104,6 +104,7 @@
|
||||
this.Controls.Add(this.cbConfig);
|
||||
this.Controls.Add(this.btnApply);
|
||||
this.Controls.Add(this.btnCancel);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
||||
this.MinimumSize = new System.Drawing.Size(200, 170);
|
||||
this.Name = "DialogSettingsExport";
|
||||
this.ShowIcon = false;
|
||||
|
@@ -9,10 +9,10 @@ using TweetDck.Core.Utils;
|
||||
|
||||
namespace TweetDck.Core.Other.Settings{
|
||||
partial class TabSettingsNotifications : BaseTabSettings{
|
||||
private readonly FormNotification notification;
|
||||
private readonly FormNotificationMain notification;
|
||||
private readonly Point initCursorPosition;
|
||||
|
||||
public TabSettingsNotifications(FormNotification notification, bool ignoreAutoClick){
|
||||
public TabSettingsNotifications(FormNotificationMain notification, bool ignoreAutoClick){
|
||||
InitializeComponent();
|
||||
|
||||
this.notification = notification;
|
||||
|
@@ -12,11 +12,11 @@ namespace TweetDck.Core.Other.Settings{
|
||||
InitializeComponent();
|
||||
|
||||
this.updates = updates;
|
||||
|
||||
this.updates.CheckFinished += updates_CheckFinished;
|
||||
Disposed += (sender, args) => this.updates.CheckFinished -= updates_CheckFinished;
|
||||
|
||||
checkUpdateNotifications.Checked = Config.EnableUpdateCheck;
|
||||
|
||||
Disposed += (sender, args) => this.updates.CheckFinished -= updates_CheckFinished;
|
||||
}
|
||||
|
||||
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)){}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -45,6 +46,19 @@ namespace TweetDck.Core.Utils{
|
||||
[DllImport("user32.dll")]
|
||||
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")]
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
100
Core/Utils/TwoKeyDictionary.cs
Normal file
100
Core/Utils/TwoKeyDictionary.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace TweetDck.Core.Utils{
|
||||
class TwoKeyDictionary<K1, K2, V>{
|
||||
private readonly Dictionary<K1, Dictionary<K2, V>> dict;
|
||||
private readonly int innerCapacity;
|
||||
|
||||
public TwoKeyDictionary() : this(16, 16){}
|
||||
|
||||
public TwoKeyDictionary(int outerCapacity, int innerCapacity){
|
||||
this.dict = new Dictionary<K1, Dictionary<K2, V>>(outerCapacity);
|
||||
this.innerCapacity = innerCapacity;
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
public V this[K1 outerKey, K2 innerKey]{
|
||||
get{ // throws on missing key
|
||||
return dict[outerKey][innerKey];
|
||||
}
|
||||
|
||||
set{
|
||||
Dictionary<K2, V> innerDict;
|
||||
|
||||
if (!dict.TryGetValue(outerKey, out innerDict)){
|
||||
dict.Add(outerKey, innerDict = new Dictionary<K2, V>(innerCapacity));
|
||||
}
|
||||
|
||||
innerDict[innerKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Members
|
||||
|
||||
public void Add(K1 outerKey, K2 innerKey, V value){ // throws on duplicate
|
||||
Dictionary<K2, V> innerDict;
|
||||
|
||||
if (!dict.TryGetValue(outerKey, out innerDict)){
|
||||
dict.Add(outerKey, innerDict = new Dictionary<K2, V>(innerCapacity));
|
||||
}
|
||||
|
||||
innerDict.Add(innerKey, value);
|
||||
}
|
||||
|
||||
public void Clear(){
|
||||
this.dict.Clear();
|
||||
}
|
||||
|
||||
public void Clear(K1 outerKey){ // throws on missing key, but keeps the key unlike Remove(K1)
|
||||
dict[outerKey].Clear();
|
||||
}
|
||||
|
||||
public bool Contains(K1 outerKey){
|
||||
return dict.ContainsKey(outerKey);
|
||||
}
|
||||
|
||||
public bool Contains(K1 outerKey, K2 innerKey){
|
||||
Dictionary<K2, V> innerDict;
|
||||
return dict.TryGetValue(outerKey, out innerDict) && innerDict.ContainsKey(innerKey);
|
||||
}
|
||||
|
||||
public int Count(){
|
||||
return dict.Values.Sum(d => d.Count);
|
||||
}
|
||||
|
||||
public int Count(K1 outerKey){ // throws on missing key
|
||||
return dict[outerKey].Count;
|
||||
}
|
||||
|
||||
public bool Remove(K1 outerKey){
|
||||
return dict.Remove(outerKey);
|
||||
}
|
||||
|
||||
public bool Remove(K1 outerKey, K2 innerKey){
|
||||
Dictionary<K2, V> innerDict;
|
||||
|
||||
if (dict.TryGetValue(outerKey, out innerDict) && innerDict.Remove(innerKey)){
|
||||
if (innerDict.Count == 0){
|
||||
dict.Remove(outerKey);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
public bool TryGetValue(K1 outerKey, K2 innerKey, out V value){
|
||||
Dictionary<K2, V> innerDict;
|
||||
|
||||
if (dict.TryGetValue(outerKey, out innerDict)){
|
||||
return innerDict.TryGetValue(innerKey, out value);
|
||||
}
|
||||
else{
|
||||
value = default(V);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,11 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Timer = System.Windows.Forms.Timer;
|
||||
|
||||
namespace TweetDck.Core.Utils{
|
||||
static class WindowsUtils{
|
||||
@@ -45,6 +48,18 @@ namespace TweetDck.Core.Utils{
|
||||
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(){
|
||||
if (!Clipboard.ContainsText(TextDataFormat.Html)){
|
||||
return;
|
||||
|
@@ -1,24 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using TweetDck.Core.Utils;
|
||||
using TweetDck.Plugins.Enums;
|
||||
using TweetDck.Plugins.Events;
|
||||
|
||||
namespace TweetDck.Plugins{
|
||||
class PluginBridge{
|
||||
private static string SanitizeCacheKey(string key){
|
||||
return key.Replace('\\', '/').Trim();
|
||||
}
|
||||
|
||||
private readonly PluginManager manager;
|
||||
private readonly Dictionary<string, string> fileCache = new Dictionary<string, string>(2);
|
||||
private readonly TwoKeyDictionary<int, string, string> fileCache = new TwoKeyDictionary<int, string, string>(4, 2);
|
||||
|
||||
public PluginBridge(PluginManager manager){
|
||||
this.manager = manager;
|
||||
this.manager.Reloaded += manager_Reloaded;
|
||||
this.manager.PluginChangedState += manager_PluginChangedState;
|
||||
}
|
||||
|
||||
private void manager_Reloaded(object sender, PluginLoadEventArgs e){
|
||||
fileCache.Clear();
|
||||
}
|
||||
|
||||
private void manager_PluginChangedState(object sender, PluginChangedStateEventArgs e){
|
||||
if (!e.IsEnabled){
|
||||
fileCache.Remove(manager.GetTokenFromPlugin(e.Plugin));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
|
||||
Plugin plugin = manager.GetPluginFromToken(token);
|
||||
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(folder, path);
|
||||
@@ -35,15 +46,17 @@ namespace TweetDck.Plugins{
|
||||
}
|
||||
}
|
||||
|
||||
private string ReadFileUnsafe(string fullPath, bool readCached){
|
||||
private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool readCached){
|
||||
cacheKey = SanitizeCacheKey(cacheKey);
|
||||
|
||||
string cachedContents;
|
||||
|
||||
if (readCached && fileCache.TryGetValue(fullPath, out cachedContents)){
|
||||
if (readCached && fileCache.TryGetValue(token, cacheKey, out cachedContents)){
|
||||
return cachedContents;
|
||||
}
|
||||
|
||||
try{
|
||||
return fileCache[fullPath] = File.ReadAllText(fullPath, Encoding.UTF8);
|
||||
return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
|
||||
}catch(FileNotFoundException){
|
||||
throw new Exception("File not found.");
|
||||
}catch(DirectoryNotFoundException){
|
||||
@@ -60,17 +73,17 @@ namespace TweetDck.Plugins{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
|
||||
|
||||
File.WriteAllText(fullPath, contents, Encoding.UTF8);
|
||||
fileCache[fullPath] = contents;
|
||||
fileCache[token, SanitizeCacheKey(path)] = contents;
|
||||
}
|
||||
|
||||
public string ReadFile(int token, string path, bool cache){
|
||||
return ReadFileUnsafe(GetFullPathOrThrow(token, PluginFolder.Data, path), cache);
|
||||
return ReadFileUnsafe(token, path, GetFullPathOrThrow(token, PluginFolder.Data, path), cache);
|
||||
}
|
||||
|
||||
public void DeleteFile(int token, string path){
|
||||
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
|
||||
|
||||
fileCache.Remove(fullPath);
|
||||
fileCache.Remove(token, SanitizeCacheKey(path));
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
|
||||
@@ -79,7 +92,7 @@ namespace TweetDck.Plugins{
|
||||
}
|
||||
|
||||
public string ReadFileRoot(int token, string path){
|
||||
return ReadFileUnsafe(GetFullPathOrThrow(token, PluginFolder.Root, path), true);
|
||||
return ReadFileUnsafe(token, "root*"+path, GetFullPathOrThrow(token, PluginFolder.Root, path), true);
|
||||
}
|
||||
|
||||
public bool CheckFileExistsRoot(int token, string path){
|
||||
|
@@ -13,6 +13,8 @@ namespace TweetDck.Plugins{
|
||||
public const string PluginNotificationScriptFile = "plugins.notification.js";
|
||||
public const string PluginGlobalScriptFile = "plugins.js";
|
||||
|
||||
private const int InvalidToken = 0;
|
||||
|
||||
public string PathOfficialPlugins { get { return Path.Combine(rootPath, "official"); } }
|
||||
public string PathCustomPlugins { get { return Path.Combine(rootPath, "user"); } }
|
||||
|
||||
@@ -37,6 +39,10 @@ namespace TweetDck.Plugins{
|
||||
}
|
||||
|
||||
public void SetConfig(PluginConfig config){
|
||||
if (this.Config != null){
|
||||
this.Config.InternalPluginChangedState -= Config_InternalPluginChangedState;
|
||||
}
|
||||
|
||||
this.Config = config;
|
||||
this.Config.InternalPluginChangedState += Config_InternalPluginChangedState;
|
||||
}
|
||||
@@ -63,6 +69,16 @@ namespace TweetDck.Plugins{
|
||||
return plugins.Any(plugin => plugin.Environments.HasFlag(environment));
|
||||
}
|
||||
|
||||
public int GetTokenFromPlugin(Plugin plugin){
|
||||
foreach(KeyValuePair<int, Plugin> kvp in tokens){
|
||||
if (kvp.Value.Equals(plugin)){
|
||||
return kvp.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return InvalidToken;
|
||||
}
|
||||
|
||||
public Plugin GetPluginFromToken(int token){
|
||||
Plugin plugin;
|
||||
return tokens.TryGetValue(token, out plugin) ? plugin : null;
|
||||
@@ -108,7 +124,7 @@ namespace TweetDck.Plugins{
|
||||
int token;
|
||||
|
||||
if (tokens.ContainsValue(plugin)){
|
||||
token = tokens.First(kvp => kvp.Value.Equals(plugin)).Key;
|
||||
token = GetTokenFromPlugin(plugin);
|
||||
}
|
||||
else{
|
||||
token = GenerateToken();
|
||||
@@ -141,7 +157,7 @@ namespace TweetDck.Plugins{
|
||||
for(int attempt = 0; attempt < 1000; attempt++){
|
||||
int token = rand.Next();
|
||||
|
||||
if (!tokens.ContainsKey(token)){
|
||||
if (!tokens.ContainsKey(token) && token != InvalidToken){
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
28
Program.cs
28
Program.cs
@@ -21,8 +21,8 @@ namespace TweetDck{
|
||||
public const string BrandName = "TweetDuck";
|
||||
public const string Website = "https://tweetduck.chylex.com";
|
||||
|
||||
public const string VersionTag = "1.6.3";
|
||||
public const string VersionFull = "1.6.3.0";
|
||||
public const string VersionTag = "1.6.6";
|
||||
public const string VersionFull = "1.6.6.0";
|
||||
|
||||
public static readonly Version Version = new Version(VersionTag);
|
||||
|
||||
@@ -89,21 +89,26 @@ namespace TweetDck{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else{
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
else Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
else{
|
||||
LockManager.Result lockResult = LockManager.Lock();
|
||||
|
||||
if (lockResult == LockManager.Result.HasProcess){
|
||||
if (LockManager.LockingProcess.MainWindowHandle == IntPtr.Zero && LockManager.LockingProcess.Responding){ // restore if the original process is in tray
|
||||
NativeMethods.SendMessage(NativeMethods.HWND_BROADCAST, WindowRestoreMessage, 0, IntPtr.Zero);
|
||||
return;
|
||||
if (LockManager.LockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
|
||||
NativeMethods.SendMessage(NativeMethods.HWND_BROADCAST, WindowRestoreMessage, LockManager.LockingProcess.Id, IntPtr.Zero);
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@@ -166,10 +171,11 @@ namespace TweetDck{
|
||||
if (mainForm.UpdateInstallerPath != null){
|
||||
ExitCleanup();
|
||||
|
||||
// ProgramPath has a trailing backslash
|
||||
string updaterArgs = "/SP- /SILENT /CLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+GetArgsClean().ToString().Replace("\"", "^\"")+"\""+(IsPortable ? " /PORTABLE=1" : "");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
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.
|
||||
|
14
Resources/Plugins/.debug/.meta
Normal file
14
Resources/Plugins/.debug/.meta
Normal file
@@ -0,0 +1,14 @@
|
||||
[name]
|
||||
Debug plugin
|
||||
|
||||
[description]
|
||||
- Runs several tests on startup, only included in debug configuration
|
||||
|
||||
[author]
|
||||
chylex
|
||||
|
||||
[version]
|
||||
1.0
|
||||
|
||||
[website]
|
||||
https://tweetduck.chylex.com
|
3
Resources/Plugins/.debug/browser.js
Normal file
3
Resources/Plugins/.debug/browser.js
Normal file
@@ -0,0 +1,3 @@
|
||||
enabled(){
|
||||
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
Revert TweetDeck design changes
|
||||
|
||||
[description]
|
||||
- Individually configurable options to revert and tweak TweetDeck design changes
|
||||
- Moves action menu to the right and hides it by default
|
||||
- Reverts interactive texts around tweets (such as 'Details' or 'Conversation')
|
||||
|
||||
@@ -9,10 +10,16 @@ Revert TweetDeck design changes
|
||||
chylex
|
||||
|
||||
[version]
|
||||
1.1
|
||||
1.2
|
||||
|
||||
[website]
|
||||
https://tweetduck.chylex.com
|
||||
|
||||
[configfile]
|
||||
configuration.js
|
||||
|
||||
[configdefault]
|
||||
configuration.default.js
|
||||
|
||||
[requires]
|
||||
1.4.1
|
@@ -1,20 +1,36 @@
|
||||
enabled(){
|
||||
// add a stylesheet to change tweet actions
|
||||
this.css = window.TDPF_createCustomStyle(this);
|
||||
this.css.insert(".tweet-actions { float: right !important; width: auto !important; }");
|
||||
this.css.insert(".tweet-action { opacity: 0; }");
|
||||
this.css.insert(".is-favorite .tweet-action, .is-retweet .tweet-action { opacity: 0.5; visibility: visible !important; }");
|
||||
this.css.insert(".tweet:hover .tweet-action, .is-favorite .tweet-action[rel='favorite'], .is-retweet .tweet-action[rel='retweet'] { opacity: 1; visibility: visible !important; }");
|
||||
this.css.insert(".tweet-actions > li:nth-child(4) { margin-right: 2px !important; }");
|
||||
|
||||
// revert small links around the tweet
|
||||
this.prevFooterMustache = TD.mustaches["status/tweet_single_footer.mustache"];
|
||||
|
||||
var footerLayout = TD.mustaches["status/tweet_single_footer.mustache"];
|
||||
footerLayout = footerLayout.replace('txt-mute txt-size--12', 'txt-mute txt-small');
|
||||
footerLayout = footerLayout.replace('{{#inReplyToID}}', '{{^inReplyToID}} <a class="pull-left margin-txs txt-mute txt-small is-vishidden-narrow" href="#" rel="viewDetails">{{_i}}Details{{/i}}</a> <a class="pull-left margin-txs txt-mute txt-small is-vishidden is-visshown-narrow" href="#" rel="viewDetails">{{_i}}Open{{/i}}</a> {{/inReplyToID}} {{#inReplyToID}}');
|
||||
footerLayout = footerLayout.replace('<span class="link-complex-target"> {{_i}}View Conversation{{/i}}', '<i class="icon icon-conversation icon-small-context"></i> <span class="link-complex-target"> <span class="is-vishidden-wide is-vishidden-narrow">{{_i}}View{{/i}}</span> <span class="is-vishidden is-visshown-wide">{{_i}}Conversation{{/i}}</span>');
|
||||
TD.mustaches["status/tweet_single_footer.mustache"] = footerLayout;
|
||||
// load configuration
|
||||
window.TDPF_loadConfigurationFile(this, "configuration.js", "configuration.default.js", config => {
|
||||
if (config.hideTweetActions){
|
||||
this.css.insert(".tweet-action { opacity: 0; }");
|
||||
this.css.insert(".is-favorite .tweet-action, .is-retweet .tweet-action { opacity: 0.5; visibility: visible !important; }");
|
||||
this.css.insert(".tweet:hover .tweet-action, .is-favorite .tweet-action[rel='favorite'], .is-retweet .tweet-action[rel='retweet'] { opacity: 1; visibility: visible !important; }");
|
||||
}
|
||||
|
||||
if (config.moveTweetActionsToRight){
|
||||
this.css.insert(".tweet-actions { float: right !important; width: auto !important; }");
|
||||
this.css.insert(".tweet-actions > li:nth-child(4) { margin-right: 2px !important; }");
|
||||
}
|
||||
|
||||
if (config.smallComposeTextSize){
|
||||
this.css.insert(".compose-text { font-size: 12px !important; height: 120px !important; }");
|
||||
}
|
||||
|
||||
if (config.revertConversationLinks){
|
||||
var footer = TD.mustaches["status/tweet_single_footer.mustache"];
|
||||
footer = footer.replace('txt-mute txt-size--12', 'txt-mute txt-small');
|
||||
footer = footer.replace('{{#inReplyToID}}', '{{^inReplyToID}} <a class="pull-left margin-txs txt-mute txt-small is-vishidden-narrow" href="#" rel="viewDetails">{{_i}}Details{{/i}}</a> <a class="pull-left margin-txs txt-mute txt-small is-vishidden is-visshown-narrow" href="#" rel="viewDetails">{{_i}}Open{{/i}}</a> {{/inReplyToID}} {{#inReplyToID}}');
|
||||
footer = footer.replace('<span class="link-complex-target"> {{_i}}View Conversation{{/i}}', '<i class="icon icon-conversation icon-small-context"></i> <span class="link-complex-target"> <span class="is-vishidden-wide is-vishidden-narrow">{{_i}}View{{/i}}</span> <span class="is-vishidden is-visshown-wide">{{_i}}Conversation{{/i}}</span>');
|
||||
TD.mustaches["status/tweet_single_footer.mustache"] = footer;
|
||||
}
|
||||
|
||||
if (config.moveTweetActionsToRight){
|
||||
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// fix layout for right-aligned actions menu
|
||||
this.uiShowActionsMenuEvent = function(){
|
||||
@@ -22,13 +38,9 @@ enabled(){
|
||||
};
|
||||
}
|
||||
|
||||
ready(){
|
||||
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
|
||||
}
|
||||
|
||||
disabled(){
|
||||
this.css.remove();
|
||||
TD.mustaches["status/tweet_single_footer.mustache"] = this.prevFooterMustache;
|
||||
|
||||
$(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent);
|
||||
TD.mustaches["status/tweet_single_footer.mustache"] = this.prevFooterMustache;
|
||||
}
|
22
Resources/Plugins/design-revert/configuration.default.js
Normal file
22
Resources/Plugins/design-revert/configuration.default.js
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
/*
|
||||
* Hides the action bar below each tweet.
|
||||
* The action bar fully appears when the mouse is over the tweet, or at half the opacity when the tweet is liked/retweeted.
|
||||
*/
|
||||
hideTweetActions: true,
|
||||
|
||||
/*
|
||||
* Moves the action bar to the right side of the tweet.
|
||||
*/
|
||||
moveTweetActionsToRight: true,
|
||||
|
||||
/*
|
||||
* Reverts New Tweet font size to a smaller one.
|
||||
*/
|
||||
smallComposeTextSize: false,
|
||||
|
||||
/*
|
||||
* Reverts design changes to the 'View Conversation' and 'Details' links.
|
||||
*/
|
||||
revertConversationLinks: true
|
||||
}
|
18
Resources/Plugins/emoji-keyboard/.meta
Normal file
18
Resources/Plugins/emoji-keyboard/.meta
Normal file
@@ -0,0 +1,18 @@
|
||||
[name]
|
||||
Emoji keyboard
|
||||
|
||||
[description]
|
||||
- Adds an emoji keyboard when writing tweets
|
||||
- Emoji list provided by http://unicode.org/emoji/charts/emoji-ordering.html
|
||||
|
||||
[author]
|
||||
chylex
|
||||
|
||||
[version]
|
||||
1.0
|
||||
|
||||
[website]
|
||||
https://tweetduck.chylex.com
|
||||
|
||||
[requires]
|
||||
1.5.3
|
289
Resources/Plugins/emoji-keyboard/browser.js
Normal file
289
Resources/Plugins/emoji-keyboard/browser.js
Normal file
@@ -0,0 +1,289 @@
|
||||
enabled(){
|
||||
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;
|
||||
|
||||
// styles
|
||||
|
||||
this.css = window.TDPF_createCustomStyle(this);
|
||||
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-list { height: 10.14em; padding: 0.1em; box-sizing: border-box; overflow-y: auto }");
|
||||
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
|
||||
|
||||
var buttonHTML = '<button class="needsclick btn btn-on-blue txt-left padding-v--9 emoji-keyboard-popup-btn"><i class="icon icon-heart"></i></button>';
|
||||
|
||||
this.prevComposeMustache = TD.mustaches["compose/docked_compose.mustache"];
|
||||
TD.mustaches["compose/docked_compose.mustache"] = TD.mustaches["compose/docked_compose.mustache"].replace('<div class="cf margin-t--12 margin-b--30">', '<div class="cf margin-t--12 margin-b--30">'+buttonHTML);
|
||||
|
||||
var dockedComposePanel = $(".js-docked-compose");
|
||||
|
||||
if (dockedComposePanel.length){
|
||||
dockedComposePanel.find(".cf.margin-t--12.margin-b--30").first().append(buttonHTML);
|
||||
}
|
||||
|
||||
// keyboard generation
|
||||
|
||||
this.currentKeyboard = null;
|
||||
|
||||
var hideKeyboard = () => {
|
||||
$(this.currentKeyboard).remove();
|
||||
this.currentKeyboard = null;
|
||||
|
||||
$(".emoji-keyboard-popup-btn").removeClass("is-selected");
|
||||
};
|
||||
|
||||
var generateEmojiHTML = skinTone => {
|
||||
return this.emojiHTML1+this.emojiHTML2[skinTone]+this.emojiHTML3;
|
||||
};
|
||||
|
||||
var selectSkinTone = skinTone => {
|
||||
let selectedEle = this.currentKeyboard.children[1].querySelector("[data-tone='"+this.selectedSkinTone+"']");
|
||||
selectedEle && selectedEle.classList.remove("sel");
|
||||
|
||||
this.selectedSkinTone = skinTone;
|
||||
this.currentKeyboard.children[0].innerHTML = generateEmojiHTML(skinTone);
|
||||
this.currentKeyboard.children[1].querySelector("[data-tone='"+this.selectedSkinTone+"']").classList.add("sel");
|
||||
};
|
||||
|
||||
this.generateKeyboard = (input, left, top) => {
|
||||
var outer = document.createElement("div");
|
||||
outer.classList.add("emoji-keyboard");
|
||||
outer.style.left = left+"px";
|
||||
outer.style.top = top+"px";
|
||||
|
||||
var keyboard = document.createElement("div");
|
||||
keyboard.classList.add("emoji-keyboard-list");
|
||||
|
||||
keyboard.addEventListener("click", function(e){
|
||||
if (e.target.tagName === "IMG"){
|
||||
input.val(input.val()+e.target.getAttribute("alt"));
|
||||
input.trigger("change");
|
||||
input.focus();
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
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;
|
||||
var prevTryPasteImageF = this.prevTryPasteImage;
|
||||
|
||||
window.TDGF_tryPasteImage = function(){
|
||||
if (me.currentKeyboard){
|
||||
hideKeyboard();
|
||||
}
|
||||
|
||||
return prevTryPasteImageF.apply(this, arguments);
|
||||
};
|
||||
|
||||
// event handlers
|
||||
|
||||
this.emojiKeyboardButtonClickEvent = function(e){
|
||||
if (me.currentKeyboard){
|
||||
hideKeyboard();
|
||||
}
|
||||
else{
|
||||
var pos = $(this).offset();
|
||||
me.generateKeyboard($(".js-compose-text").first(), pos.left, pos.top+$(this).outerHeight()+8);
|
||||
|
||||
$(this).addClass("is-selected");
|
||||
}
|
||||
|
||||
$(this).blur();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
this.documentClickEvent = function(e){
|
||||
if (me.currentKeyboard && !e.target.classList.contains("js-compose-text")){
|
||||
hideKeyboard();
|
||||
}
|
||||
};
|
||||
|
||||
this.documentKeyEvent = function(e){
|
||||
if (me.currentKeyboard && e.keyCode === 27){ // escape
|
||||
hideKeyboard();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* TODO
|
||||
* ----
|
||||
* add emoji search if I can be bothered
|
||||
* lazy emoji loading
|
||||
*/
|
||||
}
|
||||
|
||||
ready(){
|
||||
$(".emoji-keyboard-popup-btn").on("click", this.emojiKeyboardButtonClickEvent);
|
||||
$(document).on("click", this.documentClickEvent);
|
||||
$(document).on("keydown", this.documentKeyEvent);
|
||||
|
||||
// HTML generation
|
||||
|
||||
var convUnicode = function(codePt){
|
||||
if (codePt > 0xFFFF){
|
||||
codePt -= 0x10000;
|
||||
return String.fromCharCode(0xD800+(codePt>>10), 0xDC00+(codePt&0x3FF));
|
||||
}
|
||||
else{
|
||||
return String.fromCharCode(codePt);
|
||||
}
|
||||
};
|
||||
|
||||
$TDP.readFileRoot(this.$token, "emoji-ordering.txt").then(contents => {
|
||||
let generated1 = [];
|
||||
let generated2 = {};
|
||||
let generated3 = [];
|
||||
|
||||
for(let skinTone of this.skinToneList){
|
||||
generated2[skinTone] = [];
|
||||
}
|
||||
|
||||
// declaration inserters
|
||||
|
||||
let addDeclaration1 = decl => {
|
||||
generated1.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join(""));
|
||||
};
|
||||
|
||||
let addDeclaration2 = (tone, decl) => {
|
||||
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")){
|
||||
if (line[0] === '@'){
|
||||
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 if (skinToneState === 1){
|
||||
let decl = line.slice(0, line.indexOf(';'));
|
||||
let skinIndex = decl.indexOf('$');
|
||||
|
||||
if (skinIndex !== -1){
|
||||
let declPre = decl.slice(0, skinIndex);
|
||||
let declPost = decl.slice(skinIndex+1);
|
||||
|
||||
for(let skinTone of this.skinToneNonDefaultList){
|
||||
generated2[skinTone].pop();
|
||||
addDeclaration2(skinTone, declPre+skinTone+declPost);
|
||||
}
|
||||
}
|
||||
else{
|
||||
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>";
|
||||
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 => {
|
||||
$TD.alert("error", "Problem loading emoji keyboard: "+err.message);
|
||||
});
|
||||
}
|
||||
|
||||
disabled(){
|
||||
this.css.remove();
|
||||
|
||||
if (this.currentKeyboard){
|
||||
$(this.currentKeyboard).remove();
|
||||
}
|
||||
|
||||
window.TDGF_tryPasteImage = this.prevTryPasteImage;
|
||||
|
||||
$(".emoji-keyboard-popup-btn").off("click", this.emojiKeyboardButtonClickEvent);
|
||||
$(".emoji-keyboard-popup-btn").remove();
|
||||
|
||||
$(document).off("click", this.documentClickEvent);
|
||||
$(document).off("keydown", this.documentKeyEvent);
|
||||
TD.mustaches["compose/docked_compose.mustache"] = this.prevComposeMustache;
|
||||
}
|
1606
Resources/Plugins/emoji-keyboard/emoji-ordering.txt
Normal file
1606
Resources/Plugins/emoji-keyboard/emoji-ordering.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ Custom reply account
|
||||
chylex
|
||||
|
||||
[version]
|
||||
1.2
|
||||
1.2.1
|
||||
|
||||
[website]
|
||||
https://tweetduck.chylex.com
|
||||
|
@@ -22,10 +22,19 @@ enabled(){
|
||||
var section = data.element.closest("section.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 columnAccount = header.children(".attribution").text();
|
||||
var columnTitle, columnAccount;
|
||||
|
||||
if (title.length){
|
||||
columnTitle = title.text();
|
||||
columnAccount = header.children(".attribution").text();
|
||||
}
|
||||
else{
|
||||
columnTitle = header.children(".column-title-edit-box").val();
|
||||
columnAccount = "";
|
||||
}
|
||||
|
||||
try{
|
||||
query = configuration.customSelector(column.getColumnType(), columnTitle, columnAccount, column);
|
||||
|
@@ -10,71 +10,14 @@
|
||||
var highlightedTweetEle;
|
||||
|
||||
//
|
||||
// Function: Initializes TweetD*ck events. Called after the website app is loaded.
|
||||
// Variable: Array of functions called after the website app is loaded.
|
||||
//
|
||||
var initializeTweetDck = function(){
|
||||
// Settings button hook
|
||||
$("[data-action='settings-menu']").click(function(){
|
||||
setTimeout(function(){
|
||||
var menu = $(".js-dropdown-content").children("ul").first();
|
||||
if (menu.length === 0)return;
|
||||
var onAppReady = [];
|
||||
|
||||
menu.children(".drp-h-divider").last().after([
|
||||
'<li class="is-selectable" data-std><a href="#" data-action="td-settings">TweetDuck settings</a></li>',
|
||||
'<li class="is-selectable" data-std><a href="#" data-action="td-plugins">TweetDuck plugins</a></li>',
|
||||
'<li class="drp-h-divider"></li>'
|
||||
].join(""));
|
||||
|
||||
var buttons = menu.children("[data-std]");
|
||||
|
||||
buttons.on("click", "a", function(){
|
||||
var action = $(this).attr("data-action");
|
||||
|
||||
if (action === "td-settings"){
|
||||
$TD.openSettingsMenu();
|
||||
}
|
||||
else if (action === "td-plugins"){
|
||||
$TD.openPluginsMenu();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.hover(function(){
|
||||
$(this).addClass("is-selected");
|
||||
}, function(){
|
||||
$(this).removeClass("is-selected");
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Notification handling
|
||||
$.subscribe("/notifications/new", function(obj){
|
||||
for(let index = obj.items.length-1; index >= 0; index--){
|
||||
onNewTweet(obj.column, obj.items[index]);
|
||||
}
|
||||
});
|
||||
|
||||
// Setup video element replacement and fix missing target in user links
|
||||
new MutationObserver(function(){
|
||||
$("video").each(function(){
|
||||
$(this).parent().replaceWith("<a href='"+$(this).attr("src")+"' rel='url' target='_blank' style='display:block; border:1px solid #555; padding:3px 6px'>► Open video in browser</a>");
|
||||
});
|
||||
|
||||
$("a[rel='user']").attr("target", "_blank");
|
||||
}).observe($(".js-app-columns")[0], {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Finish init and load plugins
|
||||
$TD.loadFontSizeClass(TD.settings.getFontSize());
|
||||
$TD.loadNotificationHeadContents(getNotificationHeadContents());
|
||||
|
||||
window.TD_APP_READY = true;
|
||||
|
||||
if (window.TD_PLUGINS){
|
||||
window.TD_PLUGINS.onReady();
|
||||
}
|
||||
};
|
||||
//
|
||||
// Variable: DOM object containing the main app element.
|
||||
//
|
||||
var app = $(document.body).children(".js-app");
|
||||
|
||||
//
|
||||
// Function: Prepends code at the beginning of a function. If the prepended function returns true, execution of the original function is cancelled.
|
||||
@@ -137,23 +80,6 @@
|
||||
return tags.join("");
|
||||
};
|
||||
|
||||
//
|
||||
// Block: Observe the app <div> element and initialize TweetD*ck whenever possible.
|
||||
//
|
||||
var app = $("body").children(".js-app");
|
||||
|
||||
new MutationObserver(function(){
|
||||
if (window.TD_APP_READY && app.hasClass("is-hidden")){
|
||||
window.TD_APP_READY = false;
|
||||
}
|
||||
else if (!window.TD_APP_READY && !app.hasClass("is-hidden")){
|
||||
initializeTweetDck();
|
||||
}
|
||||
}).observe(app[0], {
|
||||
attributes: true,
|
||||
attributeFilter: [ "class" ]
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Hook into settings object to detect when the settings change.
|
||||
//
|
||||
@@ -168,7 +94,7 @@
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Force popup notification settings.
|
||||
// Block: Enable popup notifications.
|
||||
//
|
||||
TD.controller.notifications.hasNotifications = function(){
|
||||
return true;
|
||||
@@ -178,6 +104,49 @@
|
||||
return true;
|
||||
};
|
||||
|
||||
$.subscribe("/notifications/new", function(obj){
|
||||
for(let index = obj.items.length-1; index >= 0; index--){
|
||||
onNewTweet(obj.column, obj.items[index]);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Add TweetDuck buttons to the settings menu.
|
||||
//
|
||||
onAppReady.push(function(){
|
||||
$("[data-action='settings-menu']").click(function(){
|
||||
setTimeout(function(){
|
||||
var menu = $(".js-dropdown-content").children("ul").first();
|
||||
if (menu.length === 0)return;
|
||||
|
||||
menu.children(".drp-h-divider").last().after([
|
||||
'<li class="is-selectable" data-std><a href="#" data-action="td-settings">TweetDuck settings</a></li>',
|
||||
'<li class="is-selectable" data-std><a href="#" data-action="td-plugins">TweetDuck plugins</a></li>',
|
||||
'<li class="drp-h-divider"></li>'
|
||||
].join(""));
|
||||
|
||||
var buttons = menu.children("[data-std]");
|
||||
|
||||
buttons.on("click", "a", function(){
|
||||
var action = $(this).attr("data-action");
|
||||
|
||||
if (action === "td-settings"){
|
||||
$TD.openSettingsMenu();
|
||||
}
|
||||
else if (action === "td-plugins"){
|
||||
$TD.openPluginsMenu();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.hover(function(){
|
||||
$(this).addClass("is-selected");
|
||||
}, function(){
|
||||
$(this).removeClass("is-selected");
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Expand shortened links on hover or display tooltip.
|
||||
//
|
||||
@@ -335,8 +304,8 @@
|
||||
if (isDetail){
|
||||
setImportantProperty(selectedTweet.find(".js-tweet-media"), "margin-bottom", "0");
|
||||
selectedTweet.find(".js-translate-call-to-action").first().remove();
|
||||
selectedTweet.find(".js-cards-container").first().nextAll().remove();
|
||||
selectedTweet.find(".js-detail-view-inline").first().remove();
|
||||
selectedTweet.find(".js-tweet").first().nextAll().remove();
|
||||
selectedTweet.find("footer").last().prev().addBack().remove(); // footer & date
|
||||
}
|
||||
else{
|
||||
selectedTweet.find("footer").last().remove();
|
||||
@@ -375,6 +344,11 @@
|
||||
};
|
||||
|
||||
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 scroller = getScroller();
|
||||
@@ -387,7 +361,7 @@
|
||||
$TD.clickUploadImage(Math.floor(buttonPos.left), Math.floor(buttonPos.top));
|
||||
};
|
||||
|
||||
$(".js-app").delegate(".js-compose-text,.js-reply-tweetbox", "paste", function(){
|
||||
app.delegate(".js-compose-text,.js-reply-tweetbox", "paste", function(){
|
||||
lastPasteElement = $(this);
|
||||
$TD.tryPasteImage();
|
||||
});
|
||||
@@ -426,13 +400,6 @@
|
||||
lastPasteElement = null;
|
||||
}
|
||||
};
|
||||
|
||||
window.TDGF_tryPasteImageFinish = function(){
|
||||
setTimeout(function(){
|
||||
getScroller().scrollTop(prevScrollTop);
|
||||
$(".js-drawer").find(".js-compose-text").first()[0].focus();
|
||||
}, 10);
|
||||
};
|
||||
})();
|
||||
|
||||
//
|
||||
@@ -525,6 +492,25 @@
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
// Block: Swap shift key functionality for selecting accounts.
|
||||
//
|
||||
onAppReady.push(function(){
|
||||
$(".js-drawer[data-drawer='compose']").delegate(".js-account-list > .js-account-item", "click", function(e){
|
||||
e.shiftKey = !e.shiftKey;
|
||||
});
|
||||
|
||||
TD.components.AccountSelector.prototype.refreshPostingAccounts = appendToFunction(TD.components.AccountSelector.prototype.refreshPostingAccounts, function(){
|
||||
if (!this.$node.attr("td-account-selector-hook")){
|
||||
this.$node.attr("td-account-selector-hook", "1");
|
||||
|
||||
this.$node.delegate(".js-account-item", "click", function(e){
|
||||
e.shiftKey = !e.shiftKey;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Work around clipboard HTML formatting.
|
||||
//
|
||||
@@ -544,6 +530,9 @@
|
||||
styleOfficial.sheet.insertRule(".txt-base-smallest .badge-verified:before { height: 13px !important; }", 0); // fix cut off badge icon
|
||||
styleOfficial.sheet.insertRule(".keyboard-shortcut-list { vertical-align: top; }", 0); // fix keyboard navigation alignment
|
||||
|
||||
styleOfficial.sheet.insertRule(".is-video a:not([href*='youtu']), .is-gif .js-media-gif-container { cursor: alias; }", 0); // change cursor on unsupported videos
|
||||
styleOfficial.sheet.insertRule(".is-video a:not([href*='youtu']) .icon-bg-dot, .is-gif .icon-bg-dot { color: #bd3d37; }", 0); // change play icon color on unsupported videos
|
||||
|
||||
TD.services.TwitterActionRetweetedRetweet.prototype.iconClass = "icon-retweet icon-retweet-color txt-base-medium"; // fix retweet icon mismatch
|
||||
|
||||
window.TDGF_reinjectCustomCSS = function(styles){
|
||||
@@ -554,4 +543,66 @@
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
//
|
||||
// Block: Setup unsupported video element hook.
|
||||
//
|
||||
(function(){
|
||||
var cancelModal = false;
|
||||
|
||||
TD.components.MediaGallery.prototype._loadTweet = appendToFunction(TD.components.MediaGallery.prototype._loadTweet, function(){
|
||||
var media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
|
||||
|
||||
if (media && media.isVideo && media.service !== "youtube"){
|
||||
$TD.openBrowser(this.clickedLink);
|
||||
cancelModal = true;
|
||||
}
|
||||
});
|
||||
|
||||
TD.components.BaseModal.prototype.setAndShowContainer = prependToFunction(TD.components.BaseModal.prototype.setAndShowContainer, function(){
|
||||
if (cancelModal){
|
||||
cancelModal = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
TD.ui.Column.prototype.playGifIfNotManuallyPaused = function(){};
|
||||
TD.mustaches["status/media_thumb.mustache"] = TD.mustaches["status/media_thumb.mustache"].replace("is-gif", "is-gif is-paused");
|
||||
|
||||
app.delegate(".js-gif-play", "click", function(e){
|
||||
var parent = $(e.target).closest(".js-tweet").first();
|
||||
var link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
|
||||
|
||||
$TD.openBrowser(link.attr("href"));
|
||||
e.stopPropagation();
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
// Block: Finish initialization and load plugins.
|
||||
//
|
||||
onAppReady.push(function(){
|
||||
$TD.loadFontSizeClass(TD.settings.getFontSize());
|
||||
$TD.loadNotificationHeadContents(getNotificationHeadContents());
|
||||
|
||||
if (window.TD_PLUGINS){
|
||||
window.TD_PLUGINS.onReady();
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Block: Observe the main app element and call the ready event whenever the contents are loaded.
|
||||
//
|
||||
new MutationObserver(function(){
|
||||
if (window.TD_APP_READY && app.hasClass("is-hidden")){
|
||||
window.TD_APP_READY = false;
|
||||
}
|
||||
else if (!window.TD_APP_READY && !app.hasClass("is-hidden")){
|
||||
onAppReady.forEach(func => func());
|
||||
window.TD_APP_READY = true;
|
||||
}
|
||||
}).observe(app[0], {
|
||||
attributes: true,
|
||||
attributeFilter: [ "class" ]
|
||||
});
|
||||
})($, $TD, $TDX, TD);
|
||||
|
@@ -19,6 +19,11 @@
|
||||
//
|
||||
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.
|
||||
//
|
||||
@@ -28,7 +33,7 @@
|
||||
var ele = $("#tweetduck-update");
|
||||
var existed = ele.length > 0;
|
||||
|
||||
if (existed > 0){
|
||||
if (existed){
|
||||
ele.remove();
|
||||
}
|
||||
|
||||
@@ -106,7 +111,13 @@
|
||||
|
||||
buttonDiv.children(".tdu-btn-download").click(function(){
|
||||
ele.remove();
|
||||
$TDU.onUpdateAccepted(version, download);
|
||||
|
||||
if (download){
|
||||
$TDU.onUpdateAccepted(version, download);
|
||||
}
|
||||
else{
|
||||
$TDU.openBrowser(updateDownloadFallback);
|
||||
}
|
||||
});
|
||||
|
||||
buttonDiv.children(".tdu-btn-unsupported").click(function(){
|
||||
@@ -125,12 +136,21 @@
|
||||
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.
|
||||
//
|
||||
var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){
|
||||
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){
|
||||
var release = allowPre ? response[0] : response;
|
||||
@@ -139,7 +159,7 @@
|
||||
var hasUpdate = tagName !== versionTag && tagName !== dismissedVersionTag && release.assets.length > 0;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.Common.53.0.1\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.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.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')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
@@ -10,11 +10,10 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>TweetDck</RootNamespace>
|
||||
<AssemblyName Condition=" '$(Configuration)' == 'Debug' ">TweetDick</AssemblyName>
|
||||
<AssemblyName Condition=" '$(Configuration)' == 'Release' ">TweetDuck</AssemblyName>
|
||||
<AssemblyName>TweetDuck</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<NuGetPackageImportStamp>783c0e30</NuGetPackageImportStamp>
|
||||
<NuGetPackageImportStamp>886d3074</NuGetPackageImportStamp>
|
||||
<TargetFrameworkProfile>
|
||||
</TargetFrameworkProfile>
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
@@ -59,9 +58,6 @@
|
||||
</DefineConstants>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AssemblyName>TweetDuck</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
@@ -103,16 +99,26 @@
|
||||
<Compile Include="Core\FormBrowser.Designer.cs">
|
||||
<DependentUpon>FormBrowser.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Core\FormNotification.cs">
|
||||
<Compile Include="Core\Notification\FormNotificationMain.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Core\FormNotification.Designer.cs">
|
||||
<DependentUpon>FormNotification.cs</DependentUpon>
|
||||
<Compile Include="Core\Notification\FormNotificationMain.Designer.cs">
|
||||
<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 Include="Core\Handling\ContextMenuNotification.cs" />
|
||||
<Compile Include="Core\Handling\FileDialogHandler.cs" />
|
||||
<Compile Include="Core\Handling\JavaScriptDialogHandler.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\TweetNotification.cs" />
|
||||
<Compile Include="Core\Other\FormAbout.cs">
|
||||
<SubType>Form</SubType>
|
||||
@@ -191,6 +197,7 @@
|
||||
</Compile>
|
||||
<Compile Include="Core\Notification\NotificationFlags.cs" />
|
||||
<Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
|
||||
<Compile Include="Core\Utils\TwoKeyDictionary.cs" />
|
||||
<Compile Include="Core\Utils\WindowState.cs" />
|
||||
<Compile Include="Core\Utils\WindowsUtils.cs" />
|
||||
<Compile Include="Core\Bridge\TweetDeckBridge.cs" />
|
||||
@@ -254,7 +261,6 @@
|
||||
<Compile Include="Resources\ScriptLoader.cs" />
|
||||
<Compile Include="Updates\UpdaterSettings.cs" />
|
||||
<None Include="Configuration\app.config" />
|
||||
<None Include="Configuration\packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
|
||||
@@ -306,6 +312,7 @@
|
||||
</ContentWithTargetPath>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Configuration\packages.config" />
|
||||
<None Include="Resources\icon-small.ico" />
|
||||
<None Include="Resources\icon-tray-new.ico" />
|
||||
<None Include="Resources\icon-tray.ico" />
|
||||
@@ -327,12 +334,12 @@
|
||||
<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>
|
||||
</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.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\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.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.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.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\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.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.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.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.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.55.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets'))" />
|
||||
</Target>
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>del "$(TargetPath).config"
|
||||
@@ -351,12 +358,20 @@ mkdir "$(TargetDir)plugins\official"
|
||||
mkdir "$(TargetDir)plugins\user"
|
||||
xcopy "$(ProjectDir)Resources\Plugins\*" "$(TargetDir)plugins\official\" /E /Y
|
||||
rmdir "$(ProjectDir)\bin\Debug"
|
||||
rmdir "$(ProjectDir)\bin\Release"</PostBuildEvent>
|
||||
rmdir "$(ProjectDir)\bin\Release"
|
||||
|
||||
rmdir "$(TargetDir)plugins\official\.debug" /S /Q
|
||||
|
||||
if $(ConfigurationName) == Debug (
|
||||
rmdir "$(TargetDir)plugins\official\.debug" /S /Q
|
||||
mkdir "$(TargetDir)plugins\user\.debug"
|
||||
xcopy "$(ProjectDir)Resources\Plugins\.debug\*" "$(TargetDir)plugins\user\.debug\" /E /Y
|
||||
)</PostBuildEvent>
|
||||
</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.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\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.WinForms.53.0.1\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.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.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.55.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.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.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
|
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Windows.Forms;
|
||||
@@ -35,13 +34,13 @@ namespace TweetDck.Updates{
|
||||
this.updateInfo = info;
|
||||
this.UpdateStatus = Status.Waiting;
|
||||
|
||||
Disposed += (sender, args) => webClient.Dispose();
|
||||
|
||||
webClient.DownloadProgressChanged += webClient_DownloadProgressChanged;
|
||||
webClient.DownloadFileCompleted += webClient_DownloadFileCompleted;
|
||||
|
||||
Text = "Updating "+Program.BrandName;
|
||||
labelDescription.Text = "Downloading version "+info.VersionTag+"...";
|
||||
|
||||
Disposed += (sender, args) => this.webClient.Dispose();
|
||||
}
|
||||
|
||||
private void FormUpdateDownload_Shown(object sender, EventArgs e){
|
||||
@@ -69,7 +68,7 @@ namespace TweetDck.Updates{
|
||||
progressDownload.SetValueInstant(1000);
|
||||
}
|
||||
|
||||
labelStatus.Text = (e.BytesReceived/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" kB";
|
||||
labelStatus.Text = (long)(e.BytesReceived/BytesToKB)+" kB";
|
||||
}
|
||||
else{
|
||||
if (progressDownload.Style != ProgressBarStyle.Continuous){
|
||||
@@ -77,7 +76,7 @@ namespace TweetDck.Updates{
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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;
|
||||
}
|
||||
else{
|
||||
|
@@ -67,12 +67,12 @@ namespace TweetDck.Updates{
|
||||
|
||||
private void TriggerUpdateAcceptedEvent(UpdateAcceptedEventArgs args){
|
||||
if (UpdateAccepted != null){
|
||||
form.InvokeSafe(() => UpdateAccepted(this, args));
|
||||
form.InvokeAsyncSafe(() => UpdateAccepted(this, args));
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerUpdateDismissedEvent(UpdateDismissedEventArgs args){
|
||||
form.InvokeSafe(() => {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
settings.DismissedUpdate = args.VersionTag;
|
||||
|
||||
if (UpdateDismissed != null){
|
||||
@@ -83,7 +83,7 @@ namespace TweetDck.Updates{
|
||||
|
||||
private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){
|
||||
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\*.pdb"
|
||||
del "bin\x86\Release\devtools_resources.pak"
|
||||
del "bin\x86\Release\d3dcompiler_43.dll"
|
||||
del "bin\x86\Release\widevinecdmadapter.dll"
|
||||
|
@@ -39,7 +39,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
|
||||
|
||||
[Files]
|
||||
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]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||
|
@@ -17,7 +17,7 @@ AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={pf}\{#MyAppName}
|
||||
DefaultDirName={sd}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
OutputBaseFilename={#MyAppName}.Portable
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
@@ -36,17 +36,21 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
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]
|
||||
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]
|
||||
var UpdatePath: String;
|
||||
|
||||
function TDGetNetFrameworkVersion: Cardinal; forward;
|
||||
|
||||
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. }
|
||||
function InitializeSetup: Boolean;
|
||||
begin
|
||||
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
|
||||
|
||||
if TDGetNetFrameworkVersion() >= 379893 then
|
||||
begin
|
||||
Result := True;
|
||||
@@ -62,6 +66,21 @@ begin
|
||||
Result := True;
|
||||
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. }
|
||||
function TDGetNetFrameworkVersion: Cardinal;
|
||||
var FrameworkVersion: Cardinal;
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
||||
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
|
||||
#define CefVersion "3.2785.1486.0"
|
||||
#define CefVersion "3.2883.1552.0"
|
||||
|
||||
[Setup]
|
||||
AppId={{{#MyAppID}}
|
||||
@@ -41,7 +41,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
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]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||
@@ -72,6 +72,7 @@ function TDIsUninstallable: Boolean; forward;
|
||||
function TDFindUpdatePath: String; forward;
|
||||
function TDGetNetFrameworkVersion: Cardinal; forward;
|
||||
function TDGetAppVersionClean: String; forward;
|
||||
function TDGetFullDownloadFileName: String; forward;
|
||||
function TDIsMatchingCEFVersion: Boolean; forward;
|
||||
procedure TDExecuteFullDownload; forward;
|
||||
|
||||
@@ -93,7 +94,7 @@ begin
|
||||
|
||||
if not TDIsMatchingCEFVersion() then
|
||||
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;
|
||||
|
||||
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. }
|
||||
function TDFindUpdatePath: String;
|
||||
var Path: String;
|
||||
var RegistryKey: String;
|
||||
|
||||
begin
|
||||
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
|
||||
Result := ''
|
||||
Exit
|
||||
RegistryKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppID}}_is1'
|
||||
|
||||
if not (RegQueryStringValue(HKEY_CURRENT_USER, RegistryKey, 'InstallLocation', Path) or RegQueryStringValue(HKEY_LOCAL_MACHINE, RegistryKey, 'InstallLocation', Path)) then
|
||||
begin
|
||||
Result := ''
|
||||
Exit
|
||||
end;
|
||||
end;
|
||||
|
||||
if not FileExists(Path+'{#MyAppExeName}') then
|
||||
@@ -199,6 +206,12 @@ begin
|
||||
Result := 0;
|
||||
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. }
|
||||
function TDIsMatchingCEFVersion: Boolean;
|
||||
var CEFVersion: String;
|
||||
|
@@ -4,6 +4,37 @@ using TweetDck.Core.Utils;
|
||||
namespace UnitTests.Core.Utils{
|
||||
[TestClass]
|
||||
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]
|
||||
public void TestGetFileNameFromUrl(){
|
||||
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html"));
|
||||
|
201
tests/Core/Utils/TestTwoKeyDictionary.cs
Normal file
201
tests/Core/Utils/TestTwoKeyDictionary.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using TweetDck.Core.Utils;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnitTests.Core.Utils{
|
||||
[TestClass]
|
||||
public class TestTwoKeyDictionary{
|
||||
private static TwoKeyDictionary<string, int, string> CreateDict(){
|
||||
TwoKeyDictionary<string, int, string> dict = new TwoKeyDictionary<string, int, string>();
|
||||
|
||||
dict.Add("aaa", 0, "x");
|
||||
dict.Add("aaa", 1, "y");
|
||||
dict.Add("aaa", 2, "z");
|
||||
|
||||
dict.Add("bbb", 0, "test 1");
|
||||
dict.Add("bbb", 10, "test 2");
|
||||
dict.Add("bbb", 20, "test 3");
|
||||
dict.Add("bbb", 30, "test 4");
|
||||
|
||||
dict.Add("ccc", -5, "");
|
||||
dict.Add("", 0, "");
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestAdd(){
|
||||
CreateDict();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void TestAddDuplicate(){
|
||||
var dict = new TwoKeyDictionary<string, int, string>();
|
||||
|
||||
dict.Add("aaa", 0, "test");
|
||||
dict.Add("aaa", 0, "oops");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestAccessor(){
|
||||
var dict = CreateDict();
|
||||
|
||||
// get
|
||||
|
||||
Assert.AreEqual("x", dict["aaa", 0]);
|
||||
Assert.AreEqual("y", dict["aaa", 1]);
|
||||
Assert.AreEqual("z", dict["aaa", 2]);
|
||||
|
||||
Assert.AreEqual("test 3", dict["bbb", 20]);
|
||||
|
||||
Assert.AreEqual("", dict["ccc", -5]);
|
||||
Assert.AreEqual("", dict["", 0]);
|
||||
|
||||
// set
|
||||
|
||||
dict["aaa", 0] = "replaced entry";
|
||||
Assert.AreEqual("replaced entry", dict["aaa", 0]);
|
||||
|
||||
dict["aaa", 3] = "new entry";
|
||||
Assert.AreEqual("new entry", dict["aaa", 3]);
|
||||
|
||||
dict["xxxxx", 150] = "new key and entry";
|
||||
Assert.AreEqual("new key and entry", dict["xxxxx", 150]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(KeyNotFoundException))]
|
||||
public void TestAccessorMissingKey1(){
|
||||
var _ = CreateDict()["missing", 0];
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(KeyNotFoundException))]
|
||||
public void TestAccessorMissingKey2(){
|
||||
var _ = CreateDict()["aaa", 3];
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestClear(){
|
||||
var dict = CreateDict();
|
||||
|
||||
Assert.IsTrue(dict.Contains("bbb"));
|
||||
dict.Clear("bbb");
|
||||
Assert.IsTrue(dict.Contains("bbb"));
|
||||
|
||||
Assert.IsTrue(dict.Contains(""));
|
||||
dict.Clear("");
|
||||
Assert.IsTrue(dict.Contains(""));
|
||||
|
||||
Assert.IsTrue(dict.Contains("aaa"));
|
||||
Assert.IsTrue(dict.Contains("ccc"));
|
||||
dict.Clear();
|
||||
Assert.IsFalse(dict.Contains("aaa"));
|
||||
Assert.IsFalse(dict.Contains("ccc"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(KeyNotFoundException))]
|
||||
public void TestClearMissingKey(){
|
||||
CreateDict().Clear("missing");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestContains(){
|
||||
var dict = CreateDict();
|
||||
|
||||
// positive
|
||||
|
||||
Assert.IsTrue(dict.Contains("aaa"));
|
||||
Assert.IsTrue(dict.Contains("aaa", 0));
|
||||
Assert.IsTrue(dict.Contains("aaa", 1));
|
||||
Assert.IsTrue(dict.Contains("aaa", 2));
|
||||
|
||||
Assert.IsTrue(dict.Contains("ccc"));
|
||||
Assert.IsTrue(dict.Contains("ccc", -5));
|
||||
|
||||
Assert.IsTrue(dict.Contains(""));
|
||||
Assert.IsTrue(dict.Contains("", 0));
|
||||
|
||||
// negative
|
||||
|
||||
Assert.IsFalse(dict.Contains("missing"));
|
||||
Assert.IsFalse(dict.Contains("missing", 999));
|
||||
|
||||
Assert.IsFalse(dict.Contains("aaa", 3));
|
||||
Assert.IsFalse(dict.Contains("", -1));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestCount(){
|
||||
var dict = CreateDict();
|
||||
|
||||
Assert.AreEqual(9, dict.Count());
|
||||
Assert.AreEqual(3, dict.Count("aaa"));
|
||||
Assert.AreEqual(4, dict.Count("bbb"));
|
||||
Assert.AreEqual(1, dict.Count("ccc"));
|
||||
Assert.AreEqual(1, dict.Count(""));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(KeyNotFoundException))]
|
||||
public void TestCountMissingKey(){
|
||||
CreateDict().Count("missing");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestRemove(){
|
||||
var dict = CreateDict();
|
||||
|
||||
// negative
|
||||
Assert.IsFalse(dict.Remove("missing"));
|
||||
Assert.IsFalse(dict.Remove("aaa", 3));
|
||||
|
||||
// positive
|
||||
|
||||
Assert.IsTrue(dict.Contains("aaa"));
|
||||
Assert.IsTrue(dict.Remove("aaa"));
|
||||
Assert.IsFalse(dict.Contains("aaa"));
|
||||
|
||||
Assert.IsTrue(dict.Contains("bbb", 10));
|
||||
Assert.IsTrue(dict.Remove("bbb", 10));
|
||||
Assert.IsFalse(dict.Contains("bbb", 10));
|
||||
Assert.IsTrue(dict.Contains("bbb"));
|
||||
Assert.IsTrue(dict.Contains("bbb", 20));
|
||||
|
||||
Assert.IsTrue(dict.Remove("bbb", 0));
|
||||
Assert.IsTrue(dict.Remove("bbb", 20));
|
||||
Assert.IsTrue(dict.Remove("bbb", 30));
|
||||
Assert.IsFalse(dict.Contains("bbb"));
|
||||
|
||||
Assert.IsTrue(dict.Contains(""));
|
||||
Assert.IsTrue(dict.Remove("", 0));
|
||||
Assert.IsFalse(dict.Contains(""));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTryGetValue(){
|
||||
var dict = CreateDict();
|
||||
string val;
|
||||
|
||||
// positive
|
||||
|
||||
Assert.IsTrue(dict.TryGetValue("bbb", 10, out val));
|
||||
Assert.AreEqual("test 2", val);
|
||||
|
||||
Assert.IsTrue(dict.TryGetValue("ccc", -5, out val));
|
||||
Assert.AreEqual("", val);
|
||||
|
||||
Assert.IsTrue(dict.TryGetValue("", 0, out val));
|
||||
Assert.AreEqual("", val);
|
||||
|
||||
// negative
|
||||
|
||||
Assert.IsFalse(dict.TryGetValue("ccc", -50, out val));
|
||||
Assert.IsFalse(dict.TryGetValue("", 1, out val));
|
||||
Assert.IsFalse(dict.TryGetValue("missing", 0, out val));
|
||||
}
|
||||
}
|
||||
}
|
@@ -51,6 +51,7 @@
|
||||
<Compile Include="Core\Utils\TestBrowserUtils.cs" />
|
||||
<Compile Include="Core\Utils\TestCommandLineArgs.cs" />
|
||||
<Compile Include="Core\Utils\TestCommandLineArgsParser.cs" />
|
||||
<Compile Include="Core\Utils\TestTwoKeyDictionary.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="TestUtils.cs" />
|
||||
</ItemGroup>
|
||||
|
Reference in New Issue
Block a user