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

Compare commits

..

86 Commits
1.6.1 ... 1.6.5

Author SHA1 Message Date
00acc677e6 Release 1.6.5 2017-03-10 14:34:16 +01:00
1a799881e8 Protect against accessing MainWindowHandle on locking process when already existed 2017-03-10 14:07:09 +01:00
f75677593a Update build tools to remove/ignore .pdb files 2017-03-10 11:37:22 +01:00
19e3bd19f0 Update build guide in readme 2017-03-10 11:23:51 +01:00
85701b0a3c Update CefSharp to 55 2017-03-10 10:59:01 +01:00
014cb18dcb Remove unused 'using' statement from FormUpdateDownload 2017-03-09 20:40:21 +01:00
e71e1c853f Refactor FormBrowser.ReloadBrowser 2017-03-09 20:39:12 +01:00
ee9d9196f5 Rewrite image paste click simulation to use CEF events instead of WinAPI 2017-03-09 19:46:12 +01:00
53c8272e01 Remove decimal point in update download label 2017-03-09 19:13:17 +01:00
7f7b6b1e2a Minor code changes, including InvokeAsyncSafe in a couple more places 2017-03-09 19:08:33 +01:00
405777e0f5 Fix tray restoration code to no longer restore windows of all existing TweetDuck processes
Closes #108
2017-03-09 13:59:37 +01:00
df2b624cb5 Update Program to use TrySleepUntil 2017-03-09 13:47:47 +01:00
8a48d5c2f9 Update LockManager to use TrySleepUntil 2017-03-09 13:35:18 +01:00
c55ee71442 Add WindowsUtils.TrySleepUntil to make timeoutable waiting easier 2017-03-09 13:23:13 +01:00
3f82745f5b Improve main window detection and skip kill if already exited in LockManager 2017-03-09 03:06:47 +01:00
404187a1ae Rewrite tray restoration code to detect deadlocked process and allow killing it 2017-03-09 02:54:19 +01:00
2b7b3f586b Allow LockManager to forcibly kill the process if the attempt to close it times out 2017-03-09 02:52:04 +01:00
04959a3493 Make the update check run at the beginning of each hour instead of each hour after startup 2017-03-09 01:17:03 +01:00
97cf4932ae Move a comment in Program.cs 2017-03-09 00:56:36 +01:00
b0d88a0a37 Add a safeguard to updater to open browser if the update installer is missing 2017-03-09 00:52:12 +01:00
67a2e40622 Ninja fix deadlock when exiting after update 2017-03-08 22:10:06 +01:00
3a28556c7f Release 1.6.4 2017-03-08 21:33:39 +01:00
9ecc92b9a5 Fix emoji keyboard separators only working for the first case 2017-03-08 21:18:58 +01:00
ca023be98a Change default installation directory in portable installer 2017-03-08 21:16:36 +01:00
11a1423f76 Make sure the app is loaded before hooking account selectors 2017-03-08 13:06:50 +01:00
79f6df121b Swap shift key functionality in drawer and retweet account selectors 2017-03-08 13:01:48 +01:00
71eade7e86 Fix unsupported video tweaks for actual embedded video elements 2017-03-07 22:47:54 +01:00
5f81d29036 Finish basic emoji keyboard (enable/disable functionality, layout fix, screenshot pasting fix)
Closes #102
2017-03-07 20:32:30 +01:00
ec1cb5dc5f Final optimizations for emoji keyboard 2017-03-07 20:05:40 +01:00
fd969e2d55 Further cut down size of emoji-ordering.txt by wildcarding emojis with skin tones 2017-03-07 18:54:51 +01:00
37e33b77ff Cut down size of emoji-ordering.txt file 2017-03-07 18:36:07 +01:00
f7ed7703b4 Rewrite plugin cache to use tokens and local paths as multikeys 2017-03-07 18:31:58 +01:00
4bb35295ca Add a debug plugin to unit test plugin features 2017-03-07 18:11:13 +01:00
1e4f673f9e Add a TwoKeyDictionary collection with unit tests 2017-03-07 17:45:13 +01:00
7cadb1c403 Add an option (disabled by default) to revert New Tweet font size in design-revert plugin 2017-03-07 16:39:06 +01:00
37148f5093 Make design-revert plugin features configurable
Closes #107
2017-03-07 16:32:08 +01:00
f6bc26789f Rework emoji keyboard using official ordering, fix loading, add separators, tweak styles 2017-03-07 15:32:34 +01:00
b3f5a88525 Set red play button on unsupported videos instead of replacing them
Closes #104
2017-03-07 01:15:33 +01:00
1e538d2b28 Move sound notification code to a separate class 2017-03-05 14:27:47 +01:00
7d7bfb7b01 Refactor FormSettings to take initial tab index in constructor and remove public SelectTab 2017-03-05 14:27:35 +01:00
41d86ba440 Remove (hopefully) unnecessary user link target fix 2017-03-04 13:11:33 +01:00
3df474a8a5 Refactor ready state handling in code.js 2017-03-04 13:03:30 +01:00
a50d6e8f47 Disable resizing for the settings export dialog 2017-02-25 19:07:21 +01:00
6081e5b9c1 Add & use ControlExtensions.InvokeAsyncSafe for improved performance 2017-02-20 13:02:24 +01:00
66ccea920c Hide emoji keyboard on escape or click outside 2017-01-30 16:54:50 +01:00
470d63093f Add combined emoji to the emoji keyboard plugin 2017-01-30 16:21:09 +01:00
eae0507831 Add a WIP emoji keyboard plugin 2017-01-30 15:32:28 +01:00
92af85d3bb Release 1.6.3 2017-01-28 18:49:23 +01:00
7635af5730 Add an AppName suffix for portable and update installers 2017-01-28 18:49:14 +01:00
a838e89695 Fix custom sound notification textbox not setting color when the control is created 2017-01-28 18:12:30 +01:00
b22289a8b9 Work around Alt freezing the app since W10 Anniversary Update
Get fucked, Microsoft
2017-01-28 17:44:30 +01:00
45b3ff52c6 Tweak FormBrowser.ShowChildForm to use VisibleChanged instead of Shown event for reliability 2017-01-28 01:08:31 +01:00
4464991f4c Prevent automatic Settings tab selection from triggering autoclick in Notification tab 2017-01-28 01:07:57 +01:00
b0d2f77583 Merge pull request #101 from chylex/ipc
Replace WCF with native chromium IPC
2017-01-28 00:01:22 +01:00
b211a4405d Set CefSharpSettings.WcfEnabled to false 2017-01-27 23:59:01 +01:00
8823016d2c Make custom sound notification textbox font red when the file doesn't exist 2017-01-27 23:56:51 +01:00
859fdc7ec1 Rewrite custom sound notification to show an error message on failure instead of hiding it 2017-01-27 23:56:00 +01:00
028d5ed01f Improve debug script for easier extendibility, add sound notification simulation 2017-01-27 21:48:57 +01:00
5fd5a2a436 Use and test RegisterAsyncJsObject in FormBrowser 2017-01-27 18:51:14 +01:00
79a7e7470c Use and test RegisterAsyncJsObject in FormNotification 2017-01-27 17:00:09 +01:00
9ecef78aed Fix DismissedUpdate not being set after toggling updates 2017-01-27 16:21:36 +01:00
65a837a6e1 Move TweetDeckBridge properties to a separate JS object 2017-01-27 16:13:17 +01:00
6e4db4acea Rewrite custom CSS injection and automatically inject it while typing 2017-01-26 15:35:40 +01:00
26fb977d05 Remove unnecessary properties from TweetDeckBridge 2017-01-26 06:51:51 +01:00
b42cd1c048 Tweak screenshot notification script (minor edit) 2017-01-26 06:46:19 +01:00
467f7cd12f Rewrite update system to use RegisterAsyncJsObject 2017-01-26 06:41:20 +01:00
66699ce9df Change update progress form to show kB instead of MB 2017-01-26 06:39:46 +01:00
cf7d903932 Move updater event args to a separate namespace 2017-01-26 04:09:04 +01:00
a7ab67925c Allow moving the notification window when holding Alt in debug builds 2017-01-23 01:13:15 +01:00
a474ba4260 Fix incorrect cursor when hovering over quoted tweet in notification
Closes #97
2017-01-23 01:00:16 +01:00
09e5636e86 Remove unused 'using' statement 2017-01-23 00:59:19 +01:00
2295a875be Fix 'Copy' context menu item (separator in wrong place in browser, missing in notification) 2017-01-23 00:53:15 +01:00
82a2455afc Release 1.6.2 2017-01-23 00:33:06 +01:00
268de676ee Add NativeMethods.GetIdleSeconds for idle time detection 2017-01-22 16:00:54 +01:00
8fe26c07f1 Preserve plaintext when stripping HTML styles from clipboard text 2017-01-17 18:29:09 +01:00
da3921b1ca Add safeguards for clipboard update methods
Closes #91
2017-01-17 18:19:39 +01:00
4dd2e787d1 Remove unnecessary IsSystemSupported check in UpdateHandler 2017-01-17 02:48:17 +01:00
ce005ae6c2 Add unit tests for CombinedFileStream 2017-01-17 02:33:47 +01:00
1513f46a11 Add a safety net to CombinedFileStream.Entry.WriteToFile with createDirectory 2017-01-17 02:27:03 +01:00
7543eeb0f4 Add more methods to TestUtils and fix cleanup code not running 2017-01-17 01:38:55 +01:00
873242120c Add a TestUtils class for easy file manipulation and cleanup in unit tests 2017-01-17 01:20:12 +01:00
98f8095a65 Add unit tests for CommandLineArgsParser 2017-01-16 22:46:01 +01:00
785571a550 Add unit tests for BrowserUtils and CommandLineArgs 2017-01-16 22:06:04 +01:00
0c4bd4044e Add ReSharper code coverage settings and cleanup the test project 2017-01-16 20:56:36 +01:00
0319543dce Add a unit test project 2017-01-16 19:36:29 +01:00
82d70b2d7f Stealthfix a bug with CommandLineArgs.ToString causing an exception if there are no args 2017-01-10 21:58:17 +01:00
64 changed files with 3626 additions and 445 deletions

View File

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

View File

@@ -67,7 +67,7 @@ namespace TweetDck.Configuration{
public string NotificationSoundPath{ public string NotificationSoundPath{
get{ get{
return !string.IsNullOrEmpty(notificationSoundPath) && File.Exists(notificationSoundPath) ? notificationSoundPath : string.Empty; return string.IsNullOrEmpty(notificationSoundPath) ? string.Empty : notificationSoundPath;
} }
set{ set{

View File

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

View File

@@ -0,0 +1,34 @@
using System;
using System.Text;
namespace TweetDck.Core.Bridge{
static class PropertyBridge{
[Flags]
public enum Properties{
ExpandLinksOnHover = 1,
MuteNotifications = 2,
HasCustomNotificationSound = 4,
All = ExpandLinksOnHover | MuteNotifications | HasCustomNotificationSound
}
public static string GenerateScript(Properties properties = Properties.All){
StringBuilder build = new StringBuilder();
build.Append("(function(c){");
if (properties.HasFlag(Properties.ExpandLinksOnHover)){
build.Append("c.expandLinksOnHover=").Append(Program.UserConfig.ExpandLinksOnHover ? "true;" : "false;");
}
if (properties.HasFlag(Properties.MuteNotifications)){
build.Append("c.muteNotifications=").Append(Program.UserConfig.MuteNotifications ? "true;" : "false;");
}
if (properties.HasFlag(Properties.HasCustomNotificationSound)){
build.Append("c.hasCustomNotificationSound=").Append(Program.UserConfig.NotificationSoundPath.Length > 0 ? "true;" : "false;");
}
build.Append("})(window.$TDX=window.$TDX||{})");
return build.ToString();
}
}
}

View File

@@ -21,97 +21,55 @@ namespace TweetDck.Core.Bridge{
private readonly FormBrowser form; private readonly FormBrowser form;
private readonly FormNotification notification; private readonly FormNotification notification;
public string BrandName{
get{
return Program.BrandName;
}
}
public string VersionTag{
get{
return Program.VersionTag;
}
}
public bool MuteNotifications{
get{
return Program.UserConfig.MuteNotifications;
}
}
public bool HasCustomNotificationSound{
get{
return !string.IsNullOrEmpty(Program.UserConfig.NotificationSoundPath);
}
}
public bool ExpandLinksOnHover{
get{
return Program.UserConfig.ExpandLinksOnHover;
}
}
public bool HasCustomBrowserCSS{
get{
return !string.IsNullOrEmpty(Program.UserConfig.CustomBrowserCSS);
}
}
public string CustomBrowserCSS{
get{
return Program.UserConfig.CustomBrowserCSS;
}
}
public TweetDeckBridge(FormBrowser form, FormNotification notification){ public TweetDeckBridge(FormBrowser form, FormNotification notification){
this.form = form; this.form = form;
this.notification = notification; this.notification = notification;
} }
public void LoadFontSizeClass(string fsClass){ public void LoadFontSizeClass(string fsClass){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
TweetNotification.SetFontSizeClass(fsClass); TweetNotification.SetFontSizeClass(fsClass);
}); });
} }
public void LoadNotificationHeadContents(string headContents){ public void LoadNotificationHeadContents(string headContents){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
TweetNotification.SetHeadTag(headContents); TweetNotification.SetHeadTag(headContents);
}); });
} }
public void SetLastRightClickedLink(string link){ public void SetLastRightClickedLink(string link){
form.InvokeSafe(() => LastRightClickedLink = link); form.InvokeAsyncSafe(() => LastRightClickedLink = link);
} }
public void SetLastHighlightedTweet(string link, string quotedLink){ public void SetLastHighlightedTweet(string link, string quotedLink){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
LastHighlightedTweet = link; LastHighlightedTweet = link;
LastHighlightedQuotedTweet = quotedLink; LastHighlightedQuotedTweet = quotedLink;
}); });
} }
public void SetNotificationQuotedTweet(string link){ public void SetNotificationQuotedTweet(string link){
notification.InvokeSafe(() => notification.CurrentQuotedTweetUrl = link); notification.InvokeAsyncSafe(() => notification.CurrentQuotedTweetUrl = link);
} }
public void OpenSettingsMenu(){ public void OpenSettingsMenu(){
form.InvokeSafe(form.OpenSettings); form.InvokeAsyncSafe(form.OpenSettings);
} }
public void OpenPluginsMenu(){ public void OpenPluginsMenu(){
form.InvokeSafe(form.OpenPlugins); form.InvokeAsyncSafe(form.OpenPlugins);
} }
public void OnTweetPopup(string tweetHtml, string tweetUrl, int tweetCharacters){ public void OnTweetPopup(string tweetHtml, string tweetUrl, int tweetCharacters){
notification.InvokeSafe(() => { notification.InvokeAsyncSafe(() => {
form.OnTweetNotification(); form.OnTweetNotification();
notification.ShowNotification(new TweetNotification(tweetHtml, tweetUrl, tweetCharacters)); notification.ShowNotification(new TweetNotification(tweetHtml, tweetUrl, tweetCharacters));
}); });
} }
public void OnTweetSound(){ public void OnTweetSound(){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
form.OnTweetNotification(); form.OnTweetNotification();
form.PlayNotificationSound(); form.PlayNotificationSound();
}); });
@@ -119,15 +77,15 @@ namespace TweetDck.Core.Bridge{
public void DisplayTooltip(string text, bool showInNotification){ public void DisplayTooltip(string text, bool showInNotification){
if (showInNotification){ if (showInNotification){
notification.InvokeSafe(() => notification.DisplayTooltip(text)); notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
} }
else{ else{
form.InvokeSafe(() => form.DisplayTooltip(text)); form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
} }
} }
public void LoadNextNotification(){ public void LoadNextNotification(){
notification.InvokeSafe(notification.FinishCurrentTweet); notification.InvokeAsyncSafe(notification.FinishCurrentTweet);
} }
public void TryPasteImage(){ public void TryPasteImage(){
@@ -151,23 +109,15 @@ namespace TweetDck.Core.Bridge{
} }
public void ClickUploadImage(int offsetX, int offsetY){ public void ClickUploadImage(int offsetX, int offsetY){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => form.TriggerImageUpload(offsetX, offsetY));
Point prevPos = Cursor.Position;
Cursor.Position = form.PointToScreen(new Point(offsetX, offsetY));
NativeMethods.SimulateMouseClick(NativeMethods.MouseButton.Left);
Cursor.Position = prevPos;
form.OnImagePastedFinish();
});
} }
public void ScreenshotTweet(string html, int width, int height){ public void ScreenshotTweet(string html, int width, int height){
form.InvokeSafe(() => form.OnTweetScreenshotReady(html, width, height)); form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
} }
public void FixClipboard(){ public void FixClipboard(){
form.InvokeSafe(WindowsUtils.ClipboardStripHtmlStyles); form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
} }
public void OpenBrowser(string url){ public void OpenBrowser(string url){

View File

@@ -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){ public static void MoveToCenter(this Form targetForm, Form parentForm){
targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2); targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2);
} }

View File

@@ -13,10 +13,11 @@ using TweetDck.Updates;
using TweetDck.Plugins; using TweetDck.Plugins;
using TweetDck.Plugins.Enums; using TweetDck.Plugins.Enums;
using TweetDck.Plugins.Events; using TweetDck.Plugins.Events;
using System.Media;
using TweetDck.Core.Bridge; using TweetDck.Core.Bridge;
using TweetDck.Core.Notification; using TweetDck.Core.Notification;
using TweetDck.Core.Notification.Screenshot; using TweetDck.Core.Notification.Screenshot;
using TweetDck.Updates.Events;
using System.Diagnostics;
namespace TweetDck.Core{ namespace TweetDck.Core{
sealed partial class FormBrowser : Form{ sealed partial class FormBrowser : Form{
@@ -41,7 +42,7 @@ namespace TweetDck.Core{
private FormWindowState prevState; private FormWindowState prevState;
private TweetScreenshotManager notificationScreenshotManager; private TweetScreenshotManager notificationScreenshotManager;
private SoundPlayer notificationSound; private SoundNotification soundNotification;
public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){ public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
InitializeComponent(); InitializeComponent();
@@ -53,7 +54,11 @@ namespace TweetDck.Core{
this.plugins.PluginChangedState += plugins_PluginChangedState; this.plugins.PluginChangedState += plugins_PluginChangedState;
this.notification = CreateNotificationForm(NotificationFlags.AutoHide | NotificationFlags.TopMost); this.notification = CreateNotificationForm(NotificationFlags.AutoHide | NotificationFlags.TopMost);
#if DEBUG
this.notification.CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt;
#else
this.notification.CanMoveWindow = () => false; this.notification.CanMoveWindow = () => false;
#endif
this.notification.Show(); this.notification.Show();
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){ this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
@@ -69,11 +74,13 @@ namespace TweetDck.Core{
this.browser.LoadingStateChanged += Browser_LoadingStateChanged; this.browser.LoadingStateChanged += Browser_LoadingStateChanged;
this.browser.FrameLoadEnd += Browser_FrameLoadEnd; this.browser.FrameLoadEnd += Browser_FrameLoadEnd;
this.browser.RegisterJsObject("$TD", new TweetDeckBridge(this, notification)); this.browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge(this, notification));
this.browser.RegisterAsyncJsObject("$TDP", plugins.Bridge); this.browser.RegisterAsyncJsObject("$TDP", plugins.Bridge);
Controls.Add(browser); Controls.Add(browser);
Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
Disposed += (sender, args) => { Disposed += (sender, args) => {
browser.Dispose(); browser.Dispose();
@@ -81,8 +88,8 @@ namespace TweetDck.Core{
notificationScreenshotManager.Dispose(); notificationScreenshotManager.Dispose();
} }
if (notificationSound != null){ if (soundNotification != null){
notificationSound.Dispose(); soundNotification.Dispose();
} }
}; };
@@ -92,13 +99,16 @@ namespace TweetDck.Core{
UpdateTrayIcon(); UpdateTrayIcon();
Config.MuteToggled += Config_MuteToggled;
this.updates = new UpdateHandler(browser, this, updaterSettings); this.updates = new UpdateHandler(browser, this, updaterSettings);
this.updates.UpdateAccepted += updates_UpdateAccepted; this.updates.UpdateAccepted += updates_UpdateAccepted;
this.updates.UpdateDismissed += updates_UpdateDismissed;
} }
private void ShowChildForm(Form form){ private void ShowChildForm(Form form){
form.VisibleChanged += (sender, args) => form.MoveToCenter(this);
form.Show(this); form.Show(this);
form.Shown += (sender, args) => form.MoveToCenter(this);
} }
public void ForceClose(){ public void ForceClose(){
@@ -135,7 +145,9 @@ namespace TweetDck.Core{
private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){ private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && BrowserUtils.IsTweetDeckWebsite(e.Frame)){ if (e.Frame.IsMain && BrowserUtils.IsTweetDeckWebsite(e.Frame)){
UpdateProperties();
ScriptLoader.ExecuteFile(e.Frame, "code.js"); ScriptLoader.ExecuteFile(e.Frame, "code.js");
ReinjectCustomCSS(Config.CustomBrowserCSS);
#if DEBUG #if DEBUG
ScriptLoader.ExecuteFile(e.Frame, "debug.js"); ScriptLoader.ExecuteFile(e.Frame, "debug.js");
@@ -192,6 +204,10 @@ namespace TweetDck.Core{
} }
} }
private void Config_MuteToggled(object sender, EventArgs e){
UpdateProperties(PropertyBridge.Properties.MuteNotifications);
}
private void Config_TrayBehaviorChanged(object sender, EventArgs e){ private void Config_TrayBehaviorChanged(object sender, EventArgs e){
if (!isLoaded)return; if (!isLoaded)return;
@@ -215,7 +231,7 @@ namespace TweetDck.Core{
} }
private void plugins_Reloaded(object sender, PluginLoadEventArgs e){ private void plugins_Reloaded(object sender, PluginLoadEventArgs e){
ReloadBrowser(); browser.GetBrowser().Reload();
} }
private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){ private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
@@ -228,6 +244,7 @@ namespace TweetDck.Core{
FormUpdateDownload downloadForm = new FormUpdateDownload(e.UpdateInfo); FormUpdateDownload downloadForm = new FormUpdateDownload(e.UpdateInfo);
downloadForm.MoveToCenter(this); downloadForm.MoveToCenter(this);
downloadForm.ShowDialog(); downloadForm.ShowDialog();
downloadForm.Dispose();
if (downloadForm.UpdateStatus == FormUpdateDownload.Status.Succeeded){ if (downloadForm.UpdateStatus == FormUpdateDownload.Status.Succeeded){
UpdateInstallerPath = downloadForm.InstallerPath; UpdateInstallerPath = downloadForm.InstallerPath;
@@ -241,9 +258,19 @@ namespace TweetDck.Core{
} }
} }
private void updates_UpdateDismissed(object sender, UpdateDismissedEventArgs e){
Config.DismissedUpdate = e.VersionTag;
Config.Save();
}
protected override void WndProc(ref Message m){ protected override void WndProc(ref Message m){
if (isLoaded && m.Msg == Program.WindowRestoreMessage){ if (isLoaded && m.Msg == Program.WindowRestoreMessage){
trayIcon_ClickRestore(trayIcon, new EventArgs()); using(Process process = Process.GetCurrentProcess()){
if (process.Id == m.WParam.ToInt32()){
trayIcon_ClickRestore(trayIcon, new EventArgs());
}
}
return; return;
} }
@@ -269,29 +296,47 @@ namespace TweetDck.Core{
notification.ResumeNotification(); notification.ResumeNotification();
} }
// javascript calls
public void ReinjectCustomCSS(string css){
browser.ExecuteScriptAsync("TDGF_reinjectCustomCSS", css == null ? string.Empty : css.Replace(Environment.NewLine, " "));
}
public void UpdateProperties(PropertyBridge.Properties properties = PropertyBridge.Properties.All){
browser.ExecuteScriptAsync(PropertyBridge.GenerateScript(properties));
}
// callback handlers // callback handlers
public void OpenSettings(){ public void OpenSettings(){
OpenSettings(0);
}
public void OpenSettings(int tabIndex){
if (currentFormSettings != null){ if (currentFormSettings != null){
currentFormSettings.BringToFront(); currentFormSettings.BringToFront();
} }
else{ else{
bool prevEnableUpdateCheck = Config.EnableUpdateCheck; bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
currentFormSettings = new FormSettings(this, plugins, updates); currentFormSettings = new FormSettings(this, plugins, updates, tabIndex);
currentFormSettings.FormClosed += (sender, args) => { currentFormSettings.FormClosed += (sender, args) => {
currentFormSettings = null; currentFormSettings = null;
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){ if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
updates.Settings.DismissedUpdate = string.Empty;
Config.DismissedUpdate = string.Empty; Config.DismissedUpdate = string.Empty;
Config.Save(); Config.Save();
updates.Check(false); updates.Check(false);
} }
if (!Config.EnableTrayHighlight){ if (!Config.EnableTrayHighlight){
trayIcon.HasNotifications = false; trayIcon.HasNotifications = false;
} }
UpdateProperties(PropertyBridge.Properties.ExpandLinksOnHover | PropertyBridge.Properties.HasCustomNotificationSound);
}; };
ShowChildForm(currentFormSettings); ShowChildForm(currentFormSettings);
@@ -327,19 +372,15 @@ namespace TweetDck.Core{
} }
public void PlayNotificationSound(){ public void PlayNotificationSound(){
if (string.IsNullOrEmpty(Config.NotificationSoundPath)){ if (Config.NotificationSoundPath.Length == 0){
return; return;
} }
if (notificationSound == null){ if (soundNotification == null){
notificationSound = new SoundPlayer(); soundNotification = new SoundNotification(this);
} }
if (notificationSound.SoundLocation != Config.NotificationSoundPath){ soundNotification.Play(Config.NotificationSoundPath);
notificationSound.SoundLocation = Config.NotificationSoundPath;
}
notificationSound.Play();
} }
public void OnTweetScreenshotReady(string html, int width, int height){ public void OnTweetScreenshotReady(string html, int width, int height){
@@ -365,16 +406,14 @@ namespace TweetDck.Core{
browser.ExecuteScriptAsync("TDGF_tryPasteImage()"); browser.ExecuteScriptAsync("TDGF_tryPasteImage()");
} }
public void OnImagePastedFinish(){ public void TriggerImageUpload(int offsetX, int offsetY){
browser.ExecuteScriptAsync("TDGF_tryPasteImageFinish()"); IBrowserHost host = browser.GetBrowser().GetHost();
host.SendMouseClickEvent(offsetX, offsetY, MouseButtonType.Left, false, 1, CefEventFlags.None);
host.SendMouseClickEvent(offsetX, offsetY, MouseButtonType.Left, true, 1, CefEventFlags.None);
} }
public void TriggerTweetScreenshot(){ public void TriggerTweetScreenshot(){
browser.ExecuteScriptAsync("TDGF_triggerScreenshot()"); browser.ExecuteScriptAsync("TDGF_triggerScreenshot()");
} }
public void ReloadBrowser(){
browser.ExecuteScriptAsync("window.location.reload()");
}
} }
} }

View File

@@ -133,7 +133,7 @@ namespace TweetDck.Core{
if (!flags.HasFlag(NotificationFlags.DisableScripts)){ if (!flags.HasFlag(NotificationFlags.DisableScripts)){
notificationJS = ScriptLoader.LoadResource(NotificationScriptFile); notificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
browser.RegisterJsObject("$TD", new TweetDeckBridge(owner, this)); browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge(owner, this));
if (plugins != null){ if (plugins != null){
pluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile); pluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile);
@@ -238,6 +238,7 @@ namespace TweetDck.Core{
private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){ private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && notificationJS != null && browser.Address != "about:blank" && !flags.HasFlag(NotificationFlags.DisableScripts)){ 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); ScriptLoader.ExecuteScript(e.Frame, notificationJS, NotificationScriptIdentifier);
if (plugins != null && plugins.HasAnyPlugin(PluginEnvironment.Notification)){ if (plugins != null && plugins.HasAnyPlugin(PluginEnvironment.Notification)){
@@ -435,7 +436,7 @@ namespace TweetDck.Core{
else{ else{
Point position = PointToClient(Cursor.Position); Point position = PointToClient(Cursor.Position);
position.Offset(20, 5); position.Offset(20, 5);
toolTip.Show(text, this, position); toolTip.Show(text, this, position); // TODO figure out flickering when moving the mouse
} }
} }
} }

View File

@@ -3,6 +3,7 @@ using System;
using System.IO; using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDck.Core.Bridge; using TweetDck.Core.Bridge;
using TweetDck.Core.Controls;
using TweetDck.Core.Utils; using TweetDck.Core.Utils;
namespace TweetDck.Core.Handling{ namespace TweetDck.Core.Handling{
@@ -21,6 +22,12 @@ namespace TweetDck.Core.Handling{
} }
#endif #endif
private readonly Form form;
protected ContextMenuBase(Form form){
this.form = form;
}
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){ public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && !parameters.UnfilteredLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal)){ if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && !parameters.UnfilteredLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal)){
model.AddItem((CefMenuCommand)MenuOpenLinkUrl, "Open link in browser"); model.AddItem((CefMenuCommand)MenuOpenLinkUrl, "Open link in browser");
@@ -43,7 +50,7 @@ namespace TweetDck.Core.Handling{
break; break;
case MenuCopyLinkUrl: case MenuCopyLinkUrl:
Clipboard.SetText(string.IsNullOrEmpty(TweetDeckBridge.LastRightClickedLink) ? parameters.UnfilteredLinkUrl : TweetDeckBridge.LastRightClickedLink, TextDataFormat.UnicodeText); SetClipboardText(string.IsNullOrEmpty(TweetDeckBridge.LastRightClickedLink) ? parameters.UnfilteredLinkUrl : TweetDeckBridge.LastRightClickedLink);
break; break;
case MenuOpenImage: case MenuOpenImage:
@@ -74,7 +81,7 @@ namespace TweetDck.Core.Handling{
break; break;
case MenuCopyImageUrl: case MenuCopyImageUrl:
Clipboard.SetText(parameters.SourceUrl, TextDataFormat.UnicodeText); SetClipboardText(parameters.SourceUrl);
break; break;
#if DEBUG #if DEBUG
@@ -93,6 +100,10 @@ namespace TweetDck.Core.Handling{
return false; return false;
} }
protected void SetClipboardText(string text){
form.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
}
protected static void RemoveSeparatorIfLast(IMenuModel model){ protected static void RemoveSeparatorIfLast(IMenuModel model){
if (model.Count > 0 && model.GetTypeAt(model.Count-1) == MenuItemType.Separator){ if (model.Count > 0 && model.GetTypeAt(model.Count-1) == MenuItemType.Separator){
model.RemoveAt(model.Count-1); model.RemoveAt(model.Count-1);

View File

@@ -1,5 +1,4 @@
using CefSharp; using CefSharp;
using System.Windows.Forms;
using TweetDck.Core.Bridge; using TweetDck.Core.Bridge;
using TweetDck.Core.Controls; using TweetDck.Core.Controls;
using TweetDck.Core.Utils; using TweetDck.Core.Utils;
@@ -23,7 +22,7 @@ namespace TweetDck.Core.Handling{
private string lastHighlightedTweet; private string lastHighlightedTweet;
private string lastHighlightedQuotedTweet; private string lastHighlightedQuotedTweet;
public ContextMenuBrowser(FormBrowser form){ public ContextMenuBrowser(FormBrowser form) : base(form){
this.form = form; this.form = form;
} }
@@ -34,6 +33,10 @@ namespace TweetDck.Core.Handling{
model.Remove(CefMenuCommand.ViewSource); model.Remove(CefMenuCommand.ViewSource);
RemoveSeparatorIfLast(model); RemoveSeparatorIfLast(model);
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model); base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
lastHighlightedTweet = TweetDeckBridge.LastHighlightedTweet; lastHighlightedTweet = TweetDeckBridge.LastHighlightedTweet;
@@ -77,6 +80,8 @@ namespace TweetDck.Core.Handling{
AddDebugMenuItems(globalMenu); AddDebugMenuItems(globalMenu);
#endif #endif
} }
RemoveSeparatorIfLast(model);
} }
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){ public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
@@ -90,19 +95,19 @@ namespace TweetDck.Core.Handling{
return true; return true;
case MenuSettings: case MenuSettings:
form.InvokeSafe(form.OpenSettings); form.InvokeAsyncSafe(form.OpenSettings);
return true; return true;
case MenuAbout: case MenuAbout:
form.InvokeSafe(form.OpenAbout); form.InvokeAsyncSafe(form.OpenAbout);
return true; return true;
case MenuPlugins: case MenuPlugins:
form.InvokeSafe(form.OpenPlugins); form.InvokeAsyncSafe(form.OpenPlugins);
return true; return true;
case MenuMute: case MenuMute:
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
Program.UserConfig.MuteNotifications = !Program.UserConfig.MuteNotifications; Program.UserConfig.MuteNotifications = !Program.UserConfig.MuteNotifications;
Program.UserConfig.Save(); Program.UserConfig.Save();
}); });
@@ -114,11 +119,11 @@ namespace TweetDck.Core.Handling{
return true; return true;
case MenuCopyTweetUrl: case MenuCopyTweetUrl:
Clipboard.SetText(lastHighlightedTweet, TextDataFormat.UnicodeText); SetClipboardText(lastHighlightedTweet);
return true; return true;
case MenuScreenshotTweet: case MenuScreenshotTweet:
form.InvokeSafe(form.TriggerTweetScreenshot); form.InvokeAsyncSafe(form.TriggerTweetScreenshot);
return true; return true;
case MenuOpenQuotedTweetUrl: case MenuOpenQuotedTweetUrl:
@@ -126,7 +131,7 @@ namespace TweetDck.Core.Handling{
return true; return true;
case MenuCopyQuotedTweetUrl: case MenuCopyQuotedTweetUrl:
Clipboard.SetText(lastHighlightedQuotedTweet, TextDataFormat.UnicodeText); SetClipboardText(lastHighlightedQuotedTweet);
return true; return true;
} }

View File

@@ -1,5 +1,4 @@
using System.Windows.Forms; using CefSharp;
using CefSharp;
using TweetDck.Core.Controls; using TweetDck.Core.Controls;
namespace TweetDck.Core.Handling{ namespace TweetDck.Core.Handling{
@@ -12,13 +11,19 @@ namespace TweetDck.Core.Handling{
private readonly FormNotification form; private readonly FormNotification form;
private readonly bool enableCustomMenu; private readonly bool enableCustomMenu;
public ContextMenuNotification(FormNotification form, bool enableCustomMenu){ public ContextMenuNotification(FormNotification form, bool enableCustomMenu) : base(form){
this.form = form; this.form = form;
this.enableCustomMenu = enableCustomMenu; this.enableCustomMenu = enableCustomMenu;
} }
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){ public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
model.Clear(); model.Clear();
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
model.AddItem(CefMenuCommand.Copy, "Copy");
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model); base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (enableCustomMenu){ if (enableCustomMenu){
@@ -44,7 +49,7 @@ namespace TweetDck.Core.Handling{
RemoveSeparatorIfLast(model); 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){ public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
@@ -54,19 +59,19 @@ namespace TweetDck.Core.Handling{
switch((int)commandId){ switch((int)commandId){
case MenuSkipTweet: case MenuSkipTweet:
form.InvokeSafe(form.FinishCurrentTweet); form.InvokeAsyncSafe(form.FinishCurrentTweet);
return true; return true;
case MenuFreeze: case MenuFreeze:
form.InvokeSafe(() => form.FreezeTimer = !form.FreezeTimer); form.InvokeAsyncSafe(() => form.FreezeTimer = !form.FreezeTimer);
return true; return true;
case MenuCopyTweetUrl: case MenuCopyTweetUrl:
Clipboard.SetText(form.CurrentUrl, TextDataFormat.UnicodeText); SetClipboardText(form.CurrentUrl);
return true; return true;
case MenuCopyQuotedTweetUrl: case MenuCopyQuotedTweetUrl:
Clipboard.SetText(form.CurrentQuotedTweetUrl, TextDataFormat.UnicodeText); SetClipboardText(form.CurrentQuotedTweetUrl);
return true; return true;
} }
@@ -75,7 +80,7 @@ namespace TweetDck.Core.Handling{
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){ public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
base.OnContextMenuDismissed(browserControl, browser, frame); base.OnContextMenuDismissed(browserControl, browser, frame);
form.InvokeSafe(() => form.ContextMenuOpen = false); form.InvokeAsyncSafe(() => form.ContextMenuOpen = false);
} }
} }
} }

View File

@@ -12,7 +12,7 @@ namespace TweetDck.Core.Notification.Screenshot{
browser.FrameLoadEnd += (sender, args) => { browser.FrameLoadEnd += (sender, args) => {
if (args.Frame.IsMain && browser.Address != "about:blank"){ if (args.Frame.IsMain && browser.Address != "about:blank"){
ScriptLoader.ExecuteScript(args.Frame, "window.setTimeout(() => $TD_NotificationScreenshot.trigger(), 25)", "gen:screenshot"); ScriptLoader.ExecuteScript(args.Frame, "window.setTimeout($TD_NotificationScreenshot.trigger, 25)", "gen:screenshot");
} }
}; };

View 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();
}
}
}
}

View File

@@ -9,10 +9,13 @@ using TweetDck.Updates;
namespace TweetDck.Core.Other{ namespace TweetDck.Core.Other{
sealed partial class FormSettings : Form{ sealed partial class FormSettings : Form{
public const int TabIndexNotification = 1;
private readonly FormBrowser browser; private readonly FormBrowser browser;
private readonly Dictionary<Type, BaseTabSettings> tabs = new Dictionary<Type, BaseTabSettings>(4); private readonly Dictionary<Type, BaseTabSettings> tabs = new Dictionary<Type, BaseTabSettings>(4);
private readonly bool hasFinishedLoading;
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates){ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, int startTabIndex = 0){
InitializeComponent(); InitializeComponent();
Text = Program.BrandName+" Settings"; Text = Program.BrandName+" Settings";
@@ -22,10 +25,12 @@ namespace TweetDck.Core.Other{
this.tabPanel.SetupTabPanel(100); this.tabPanel.SetupTabPanel(100);
this.tabPanel.AddButton("General", SelectTab<TabSettingsGeneral>); this.tabPanel.AddButton("General", SelectTab<TabSettingsGeneral>);
this.tabPanel.AddButton("Notifications", () => SelectTab(() => new TabSettingsNotifications(browser.CreateNotificationForm(NotificationFlags.DisableContextMenu)))); this.tabPanel.AddButton("Notifications", () => SelectTab(() => new TabSettingsNotifications(browser.CreateNotificationForm(NotificationFlags.DisableContextMenu), !hasFinishedLoading)));
this.tabPanel.AddButton("Updates", () => SelectTab(() => new TabSettingsUpdates(updates))); this.tabPanel.AddButton("Updates", () => SelectTab(() => new TabSettingsUpdates(updates)));
this.tabPanel.AddButton("Advanced", () => SelectTab(() => new TabSettingsAdvanced(browser.ReloadBrowser, plugins))); this.tabPanel.AddButton("Advanced", () => SelectTab(() => new TabSettingsAdvanced(browser.ReinjectCustomCSS, plugins)));
this.tabPanel.SelectTab(tabPanel.Buttons.First());
this.tabPanel.SelectTab(tabPanel.Buttons.ElementAt(startTabIndex));
hasFinishedLoading = true;
} }
private void SelectTab<T>() where T : BaseTabSettings, new(){ private void SelectTab<T>() where T : BaseTabSettings, new(){

View File

@@ -23,6 +23,7 @@
/// the contents of this method with the code editor. /// the contents of this method with the code editor.
/// </summary> /// </summary>
private void InitializeComponent() { private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.textBoxBrowserCSS = new System.Windows.Forms.TextBox(); this.textBoxBrowserCSS = new System.Windows.Forms.TextBox();
this.btnCancel = new System.Windows.Forms.Button(); this.btnCancel = new System.Windows.Forms.Button();
this.btnApply = new System.Windows.Forms.Button(); this.btnApply = new System.Windows.Forms.Button();
@@ -32,6 +33,7 @@
this.textBoxNotificationCSS = new System.Windows.Forms.TextBox(); this.textBoxNotificationCSS = new System.Windows.Forms.TextBox();
this.labelWarning = new System.Windows.Forms.Label(); this.labelWarning = new System.Windows.Forms.Label();
this.btnOpenWiki = new System.Windows.Forms.Button(); this.btnOpenWiki = new System.Windows.Forms.Button();
this.timerTestBrowser = new System.Windows.Forms.Timer(this.components);
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
this.splitContainer.Panel1.SuspendLayout(); this.splitContainer.Panel1.SuspendLayout();
this.splitContainer.Panel2.SuspendLayout(); this.splitContainer.Panel2.SuspendLayout();
@@ -52,6 +54,7 @@
this.textBoxBrowserCSS.Size = new System.Drawing.Size(373, 253); this.textBoxBrowserCSS.Size = new System.Drawing.Size(373, 253);
this.textBoxBrowserCSS.TabIndex = 0; this.textBoxBrowserCSS.TabIndex = 0;
this.textBoxBrowserCSS.WordWrap = false; this.textBoxBrowserCSS.WordWrap = false;
this.textBoxBrowserCSS.KeyUp += new System.Windows.Forms.KeyEventHandler(this.textBoxBrowserCSS_KeyUp);
// //
// btnCancel // btnCancel
// //
@@ -132,7 +135,7 @@
this.textBoxNotificationCSS.Multiline = true; this.textBoxNotificationCSS.Multiline = true;
this.textBoxNotificationCSS.Name = "textBoxNotificationCSS"; this.textBoxNotificationCSS.Name = "textBoxNotificationCSS";
this.textBoxNotificationCSS.ScrollBars = System.Windows.Forms.ScrollBars.Both; this.textBoxNotificationCSS.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.textBoxNotificationCSS.Size = new System.Drawing.Size(376, 253); this.textBoxNotificationCSS.Size = new System.Drawing.Size(373, 253);
this.textBoxNotificationCSS.TabIndex = 1; this.textBoxNotificationCSS.TabIndex = 1;
this.textBoxNotificationCSS.WordWrap = false; this.textBoxNotificationCSS.WordWrap = false;
// //
@@ -159,6 +162,11 @@
this.btnOpenWiki.UseVisualStyleBackColor = true; this.btnOpenWiki.UseVisualStyleBackColor = true;
this.btnOpenWiki.Click += new System.EventHandler(this.btnOpenWiki_Click); this.btnOpenWiki.Click += new System.EventHandler(this.btnOpenWiki_Click);
// //
// timerTestBrowser
//
this.timerTestBrowser.Interval = 500;
this.timerTestBrowser.Tick += new System.EventHandler(this.timerTestBrowser_Tick);
//
// DialogSettingsCSS // DialogSettingsCSS
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -195,5 +203,6 @@
private System.Windows.Forms.Label labelNotification; private System.Windows.Forms.Label labelNotification;
private System.Windows.Forms.Label labelWarning; private System.Windows.Forms.Label labelWarning;
private System.Windows.Forms.Button btnOpenWiki; private System.Windows.Forms.Button btnOpenWiki;
private System.Windows.Forms.Timer timerTestBrowser;
} }
} }

View File

@@ -17,10 +17,14 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
} }
} }
public DialogSettingsCSS(){ private readonly Action<string> reinjectBrowserCSS;
public DialogSettingsCSS(Action<string> reinjectBrowserCSS){
InitializeComponent(); InitializeComponent();
Text = Program.BrandName+" Settings - CSS"; Text = Program.BrandName+" Settings - CSS";
this.reinjectBrowserCSS = reinjectBrowserCSS;
textBoxBrowserCSS.EnableMultilineShortcuts(); textBoxBrowserCSS.EnableMultilineShortcuts();
textBoxBrowserCSS.Text = Program.UserConfig.CustomBrowserCSS ?? ""; textBoxBrowserCSS.Text = Program.UserConfig.CustomBrowserCSS ?? "";
@@ -29,6 +33,16 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
textBoxNotificationCSS.Text = Program.UserConfig.CustomNotificationCSS ?? ""; textBoxNotificationCSS.Text = Program.UserConfig.CustomNotificationCSS ?? "";
} }
private void textBoxBrowserCSS_KeyUp(object sender, KeyEventArgs e){
timerTestBrowser.Stop();
timerTestBrowser.Start();
}
private void timerTestBrowser_Tick(object sender, EventArgs e){
reinjectBrowserCSS(textBoxBrowserCSS.Text);
timerTestBrowser.Stop();
}
private void btnOpenWiki_Click(object sender, EventArgs e){ private void btnOpenWiki_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki"); BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki");
} }

View File

@@ -104,6 +104,7 @@
this.Controls.Add(this.cbConfig); this.Controls.Add(this.cbConfig);
this.Controls.Add(this.btnApply); this.Controls.Add(this.btnApply);
this.Controls.Add(this.btnCancel); this.Controls.Add(this.btnCancel);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MinimumSize = new System.Drawing.Size(200, 170); this.MinimumSize = new System.Drawing.Size(200, 170);
this.Name = "DialogSettingsExport"; this.Name = "DialogSettingsExport";
this.ShowIcon = false; this.ShowIcon = false;

View File

@@ -121,8 +121,11 @@ namespace TweetDck.Core.Other.Settings.Export{
public void WriteToFile(string path, bool createDirectory){ public void WriteToFile(string path, bool createDirectory){
if (createDirectory){ if (createDirectory){
// ReSharper disable once AssignNullToNotNullAttribute string dir = Path.GetDirectoryName(path);
Directory.CreateDirectory(Path.GetDirectoryName(path));
if (!string.IsNullOrEmpty(dir)){
Directory.CreateDirectory(dir);
}
} }
File.WriteAllBytes(path, contents); File.WriteAllBytes(path, contents);

View File

@@ -9,13 +9,13 @@ using TweetDck.Plugins;
namespace TweetDck.Core.Other.Settings{ namespace TweetDck.Core.Other.Settings{
partial class TabSettingsAdvanced : BaseTabSettings{ partial class TabSettingsAdvanced : BaseTabSettings{
private readonly Action browserReloadAction; private readonly Action<string> reinjectBrowserCSS;
private readonly PluginManager plugins; private readonly PluginManager plugins;
public TabSettingsAdvanced(Action browserReloadAction, PluginManager plugins){ public TabSettingsAdvanced(Action<string> reinjectBrowserCSS, PluginManager plugins){
InitializeComponent(); InitializeComponent();
this.browserReloadAction = browserReloadAction; this.reinjectBrowserCSS = reinjectBrowserCSS;
this.plugins = plugins; this.plugins = plugins;
checkHardwareAcceleration.Checked = HardwareAcceleration.IsEnabled; checkHardwareAcceleration.Checked = HardwareAcceleration.IsEnabled;
@@ -80,17 +80,13 @@ namespace TweetDck.Core.Other.Settings{
} }
private void btnEditCSS_Click(object sender, EventArgs e){ private void btnEditCSS_Click(object sender, EventArgs e){
using(DialogSettingsCSS form = new DialogSettingsCSS()){ using(DialogSettingsCSS form = new DialogSettingsCSS(reinjectBrowserCSS)){
if (form.ShowDialog(ParentForm) == DialogResult.OK){ if (form.ShowDialog(ParentForm) == DialogResult.OK){
bool hasChangedBrowser = form.BrowserCSS != Config.CustomBrowserCSS;
Config.CustomBrowserCSS = form.BrowserCSS; Config.CustomBrowserCSS = form.BrowserCSS;
Config.CustomNotificationCSS = form.NotificationCSS; Config.CustomNotificationCSS = form.NotificationCSS;
if (hasChangedBrowser && MessageBox.Show("The browser CSS has changed, do you want to reload it?", "Browser CSS Changed", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes){
browserReloadAction();
}
} }
reinjectBrowserCSS(Config.CustomBrowserCSS); // reinject on cancel too, because the CSS is updated while typing
} }
} }

View File

@@ -381,6 +381,7 @@
this.tbCustomSound.Name = "tbCustomSound"; this.tbCustomSound.Name = "tbCustomSound";
this.tbCustomSound.Size = new System.Drawing.Size(170, 20); this.tbCustomSound.Size = new System.Drawing.Size(170, 20);
this.tbCustomSound.TabIndex = 0; this.tbCustomSound.TabIndex = 0;
this.tbCustomSound.TextChanged += new System.EventHandler(this.tbCustomSound_TextChanged);
// //
// TabSettingsNotifications // TabSettingsNotifications
// //

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDck.Core.Controls; using TweetDck.Core.Controls;
using TweetDck.Core.Notification; using TweetDck.Core.Notification;
@@ -11,7 +12,7 @@ namespace TweetDck.Core.Other.Settings{
private readonly FormNotification notification; private readonly FormNotification notification;
private readonly Point initCursorPosition; private readonly Point initCursorPosition;
public TabSettingsNotifications(FormNotification notification){ public TabSettingsNotifications(FormNotification notification, bool ignoreAutoClick){
InitializeComponent(); InitializeComponent();
this.notification = notification; this.notification = notification;
@@ -30,7 +31,7 @@ namespace TweetDck.Core.Other.Settings{
this.notification.Activated += notification_Activated; this.notification.Activated += notification_Activated;
this.notification.Show(this); this.notification.Show(this);
initCursorPosition = Cursor.Position; initCursorPosition = ignoreAutoClick ? ControlExtensions.InvisibleLocation : Cursor.Position;
switch(Config.NotificationPosition){ switch(Config.NotificationPosition){
case TweetNotification.Position.TopLeft: radioLocTL.Checked = true; break; case TweetNotification.Position.TopLeft: radioLocTL.Checked = true; break;
@@ -58,7 +59,7 @@ namespace TweetDck.Core.Other.Settings{
trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance); trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance);
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value.ToString(CultureInfo.InvariantCulture)+" px"; labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value.ToString(CultureInfo.InvariantCulture)+" px";
tbCustomSound.Text = Config.NotificationSoundPath ?? string.Empty; tbCustomSound.Text = Config.NotificationSoundPath;
Disposed += (sender, args) => this.notification.Dispose(); Disposed += (sender, args) => this.notification.Dispose();
} }
@@ -77,7 +78,7 @@ namespace TweetDck.Core.Other.Settings{
} }
private void notification_Activated(object sender, EventArgs e){ private void notification_Activated(object sender, EventArgs e){
if (Cursor.Position == initCursorPosition){ if (Cursor.Position == initCursorPosition && initCursorPosition != ControlExtensions.InvisibleLocation){
Timer delay = WindowsUtils.CreateSingleTickTimer(1); Timer delay = WindowsUtils.CreateSingleTickTimer(1);
delay.Tick += (sender2, args2) => { // here you can see a disgusting hack to force the freshly opened notification window out of focus delay.Tick += (sender2, args2) => { // here you can see a disgusting hack to force the freshly opened notification window out of focus
@@ -167,6 +168,13 @@ namespace TweetDck.Core.Other.Settings{
notification.ShowNotificationForSettings(false); notification.ShowNotificationForSettings(false);
} }
private void tbCustomSound_TextChanged(object sender, EventArgs e){
// also runs when the control is created, i.e. when Ready is false
bool fileExists = string.IsNullOrEmpty(tbCustomSound.Text) || File.Exists(tbCustomSound.Text);
tbCustomSound.ForeColor = fileExists ? SystemColors.WindowText : Color.Maroon;
}
private void btnBrowseSound_Click(object sender, EventArgs e){ private void btnBrowseSound_Click(object sender, EventArgs e){
using(OpenFileDialog dialog = new OpenFileDialog{ using(OpenFileDialog dialog = new OpenFileDialog{
AutoUpgradeEnabled = true, AutoUpgradeEnabled = true,

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDck.Updates; using TweetDck.Updates;
using TweetDck.Updates.Events;
namespace TweetDck.Core.Other.Settings{ namespace TweetDck.Core.Other.Settings{
partial class TabSettingsUpdates : BaseTabSettings{ partial class TabSettingsUpdates : BaseTabSettings{
@@ -27,12 +28,18 @@ namespace TweetDck.Core.Other.Settings{
private void btnCheckUpdates_Click(object sender, EventArgs e){ private void btnCheckUpdates_Click(object sender, EventArgs e){
if (!Ready)return; if (!Ready)return;
Config.DismissedUpdate = string.Empty;
Config.Save();
updateCheckEventId = updates.Check(true); updateCheckEventId = updates.Check(true);
btnCheckUpdates.Enabled = false; if (updateCheckEventId == -1){
MessageBox.Show("Sorry, your system is no longer supported.", "Unsupported System", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else{
btnCheckUpdates.Enabled = false;
updates.Settings.DismissedUpdate = string.Empty;
Config.DismissedUpdate = string.Empty;
Config.Save();
}
} }
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e){ private void updates_CheckFinished(object sender, UpdateCheckEventArgs e){

View File

@@ -105,7 +105,7 @@ namespace TweetDck.Core.Utils{
build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" "); build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" ");
} }
return build.Remove(build.Length-1, 1).ToString(); return build.Length == 0 ? string.Empty : build.Remove(build.Length-1, 1).ToString();
} }
} }
} }

View File

@@ -24,13 +24,26 @@ namespace TweetDck.Core.Utils{
Left, Right Left, Right
} }
private struct LASTINPUTINFO{
public static readonly uint Size = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
// ReSharper disable once NotAccessedField.Local
public uint cbSize;
#pragma warning disable 649
public uint dwTime;
#pragma warning restore 649
}
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", EntryPoint = "SetWindowPos")] [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern bool SetWindowPos(int hWnd, int hWndOrder, int x, int y, int width, int height, uint flags); private static extern bool SetWindowPos(int hWnd, int hWndOrder, int x, int y, int width, int height, uint flags);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo); private static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);
[DllImport("user32.dll")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO info);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam); public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam);
@@ -75,5 +88,23 @@ namespace TweetDck.Core.Utils{
mouse_event(flagHold, Cursor.Position.X, Cursor.Position.Y, 0, 0); mouse_event(flagHold, Cursor.Position.X, Cursor.Position.Y, 0, 0);
mouse_event(flagRelease, Cursor.Position.X, Cursor.Position.Y, 0, 0); mouse_event(flagRelease, Cursor.Position.X, Cursor.Position.Y, 0, 0);
} }
public static int GetIdleSeconds(){
LASTINPUTINFO info = new LASTINPUTINFO();
info.cbSize = LASTINPUTINFO.Size;
if (!GetLastInputInfo(ref info)){
return 0;
}
uint ticks;
unchecked{
ticks = (uint)Environment.TickCount;
}
int seconds = (int)Math.Floor(TimeSpan.FromMilliseconds(ticks-info.dwTime).TotalSeconds);
return Math.Max(0, seconds); // ignore rollover after several weeks of uptime
}
} }
} }

View 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;
}
}
}
}

View File

@@ -1,7 +1,11 @@
using System.Diagnostics; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using Timer = System.Windows.Forms.Timer;
namespace TweetDck.Core.Utils{ namespace TweetDck.Core.Utils{
static class WindowsUtils{ static class WindowsUtils{
@@ -44,18 +48,53 @@ namespace TweetDck.Core.Utils{
return timer; return timer;
} }
public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){
for(int waited = 0; waited < timeoutMillis; waited += timeStepMillis){
if (test()){
return true;
}
Thread.Sleep(timeStepMillis);
}
return false;
}
public static void ClipboardStripHtmlStyles(){ public static void ClipboardStripHtmlStyles(){
if (!Clipboard.ContainsText(TextDataFormat.Html)){ if (!Clipboard.ContainsText(TextDataFormat.Html)){
return; return;
} }
string original = Clipboard.GetText(TextDataFormat.Html); string originalText = Clipboard.GetText(TextDataFormat.UnicodeText);
string updated = RegexStripHtmlStyles.Replace(original, string.Empty); string originalHtml = Clipboard.GetText(TextDataFormat.Html);
int removed = original.Length-updated.Length; string updatedHtml = RegexStripHtmlStyles.Replace(originalHtml, string.Empty);
updated = RegexOffsetClipboardHtml.Replace(updated, match => (int.Parse(match.Value)-removed).ToString().PadLeft(match.Value.Length, '0'));
Clipboard.SetText(updated, TextDataFormat.Html); int removed = originalHtml.Length-updatedHtml.Length;
updatedHtml = RegexOffsetClipboardHtml.Replace(updatedHtml, match => (int.Parse(match.Value)-removed).ToString().PadLeft(match.Value.Length, '0'));
DataObject obj = new DataObject();
obj.SetText(originalText, TextDataFormat.UnicodeText);
obj.SetText(updatedHtml, TextDataFormat.Html);
SetClipboardData(obj);
}
public static void SetClipboard(string text, TextDataFormat format){
if (string.IsNullOrEmpty(text)){
return;
}
DataObject obj = new DataObject();
obj.SetText(text, format);
SetClipboardData(obj);
}
private static void SetClipboardData(DataObject obj){
try{
Clipboard.SetDataObject(obj);
}catch(ExternalException e){
Program.Reporter.HandleException("Clipboard Error", Program.BrandName+" could not access the clipboard as it is currently used by another process.", true, e);
}
} }
} }
} }

View File

@@ -1,24 +1,35 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using TweetDck.Core.Utils;
using TweetDck.Plugins.Enums; using TweetDck.Plugins.Enums;
using TweetDck.Plugins.Events; using TweetDck.Plugins.Events;
namespace TweetDck.Plugins{ namespace TweetDck.Plugins{
class PluginBridge{ class PluginBridge{
private static string SanitizeCacheKey(string key){
return key.Replace('\\', '/').Trim();
}
private readonly PluginManager manager; 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){ public PluginBridge(PluginManager manager){
this.manager = manager; this.manager = manager;
this.manager.Reloaded += manager_Reloaded; this.manager.Reloaded += manager_Reloaded;
this.manager.PluginChangedState += manager_PluginChangedState;
} }
private void manager_Reloaded(object sender, PluginLoadEventArgs e){ private void manager_Reloaded(object sender, PluginLoadEventArgs e){
fileCache.Clear(); 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){ private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
Plugin plugin = manager.GetPluginFromToken(token); Plugin plugin = manager.GetPluginFromToken(token);
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(folder, path); 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; string cachedContents;
if (readCached && fileCache.TryGetValue(fullPath, out cachedContents)){ if (readCached && fileCache.TryGetValue(token, cacheKey, out cachedContents)){
return cachedContents; return cachedContents;
} }
try{ try{
return fileCache[fullPath] = File.ReadAllText(fullPath, Encoding.UTF8); return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
}catch(FileNotFoundException){ }catch(FileNotFoundException){
throw new Exception("File not found."); throw new Exception("File not found.");
}catch(DirectoryNotFoundException){ }catch(DirectoryNotFoundException){
@@ -60,17 +73,17 @@ namespace TweetDck.Plugins{
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
File.WriteAllText(fullPath, contents, Encoding.UTF8); File.WriteAllText(fullPath, contents, Encoding.UTF8);
fileCache[fullPath] = contents; fileCache[token, SanitizeCacheKey(path)] = contents;
} }
public string ReadFile(int token, string path, bool cache){ 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){ public void DeleteFile(int token, string path){
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path); string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
fileCache.Remove(fullPath); fileCache.Remove(token, SanitizeCacheKey(path));
File.Delete(fullPath); File.Delete(fullPath);
} }
@@ -79,7 +92,7 @@ namespace TweetDck.Plugins{
} }
public string ReadFileRoot(int token, string path){ 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){ public bool CheckFileExistsRoot(int token, string path){

View File

@@ -13,6 +13,8 @@ namespace TweetDck.Plugins{
public const string PluginNotificationScriptFile = "plugins.notification.js"; public const string PluginNotificationScriptFile = "plugins.notification.js";
public const string PluginGlobalScriptFile = "plugins.js"; public const string PluginGlobalScriptFile = "plugins.js";
private const int InvalidToken = 0;
public string PathOfficialPlugins { get { return Path.Combine(rootPath, "official"); } } public string PathOfficialPlugins { get { return Path.Combine(rootPath, "official"); } }
public string PathCustomPlugins { get { return Path.Combine(rootPath, "user"); } } public string PathCustomPlugins { get { return Path.Combine(rootPath, "user"); } }
@@ -63,6 +65,16 @@ namespace TweetDck.Plugins{
return plugins.Any(plugin => plugin.Environments.HasFlag(environment)); 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){ public Plugin GetPluginFromToken(int token){
Plugin plugin; Plugin plugin;
return tokens.TryGetValue(token, out plugin) ? plugin : null; return tokens.TryGetValue(token, out plugin) ? plugin : null;
@@ -108,7 +120,7 @@ namespace TweetDck.Plugins{
int token; int token;
if (tokens.ContainsValue(plugin)){ if (tokens.ContainsValue(plugin)){
token = tokens.First(kvp => kvp.Value.Equals(plugin)).Key; token = GetTokenFromPlugin(plugin);
} }
else{ else{
token = GenerateToken(); token = GenerateToken();
@@ -141,7 +153,7 @@ namespace TweetDck.Plugins{
for(int attempt = 0; attempt < 1000; attempt++){ for(int attempt = 0; attempt < 1000; attempt++){
int token = rand.Next(); int token = rand.Next();
if (!tokens.ContainsKey(token)){ if (!tokens.ContainsKey(token) && token != InvalidToken){
return token; return token;
} }
} }

View File

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

View File

@@ -36,4 +36,8 @@ using TweetDck;
[assembly: AssemblyVersion(Program.VersionFull)] [assembly: AssemblyVersion(Program.VersionFull)]
[assembly: AssemblyFileVersion(Program.VersionFull)] [assembly: AssemblyFileVersion(Program.VersionFull)]
[assembly: NeutralResourcesLanguageAttribute("en")] [assembly: NeutralResourcesLanguageAttribute("en")]
#if DEBUG
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
#endif

View File

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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
enabled(){
}

View File

@@ -2,6 +2,7 @@
Revert TweetDeck design changes Revert TweetDeck design changes
[description] [description]
- Individually configurable options to revert and tweak TweetDeck design changes
- Moves action menu to the right and hides it by default - Moves action menu to the right and hides it by default
- Reverts interactive texts around tweets (such as 'Details' or 'Conversation') - Reverts interactive texts around tweets (such as 'Details' or 'Conversation')
@@ -9,10 +10,16 @@ Revert TweetDeck design changes
chylex chylex
[version] [version]
1.1 1.2
[website] [website]
https://tweetduck.chylex.com https://tweetduck.chylex.com
[configfile]
configuration.js
[configdefault]
configuration.default.js
[requires] [requires]
1.4.1 1.4.1

View File

@@ -1,34 +1,46 @@
enabled(){ enabled(){
// add a stylesheet to change tweet actions
this.css = window.TDPF_createCustomStyle(this); 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"]; this.prevFooterMustache = TD.mustaches["status/tweet_single_footer.mustache"];
var footerLayout = TD.mustaches["status/tweet_single_footer.mustache"]; // load configuration
footerLayout = footerLayout.replace('txt-mute txt-size--12', 'txt-mute txt-small'); window.TDPF_loadConfigurationFile(this, "configuration.js", "configuration.default.js", config => {
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}}'); if (config.hideTweetActions){
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>'); this.css.insert(".tweet-action { opacity: 0; }");
TD.mustaches["status/tweet_single_footer.mustache"] = footerLayout; 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 // fix layout for right-aligned actions menu
this.uiShowActionsMenuEvent = function(){ this.uiShowActionsMenuEvent = function(){
$(".js-dropdown.pos-r").toggleClass("pos-r pos-l"); $(".js-dropdown.pos-r").toggleClass("pos-r pos-l");
}; };
} }
ready(){
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
}
disabled(){ disabled(){
this.css.remove(); this.css.remove();
TD.mustaches["status/tweet_single_footer.mustache"] = this.prevFooterMustache;
$(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent); $(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent);
TD.mustaches["status/tweet_single_footer.mustache"] = this.prevFooterMustache; }
}

View 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
}

View 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

View File

@@ -0,0 +1,178 @@
enabled(){
this.emojiHTML = "";
var me = this;
// styles
this.css = window.TDPF_createCustomStyle(this);
this.css.insert(".emoji-keyboard { position: absolute; width: 15.35em; height: 11.2em; background-color: white; overflow-y: auto; padding: 0.1em; box-sizing: border-box; border-radius: 2px; font-size: 24px; z-index: 9999 }");
this.css.insert(".emoji-keyboard .separator { height: 26px; }");
this.css.insert(".emoji-keyboard .emoji { padding: 0.1em !important; cursor: pointer }");
// 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");
};
this.generateKeyboardFor = (input, left, top) => {
var created = document.createElement("div");
document.body.appendChild(created);
created.classList.add("emoji-keyboard");
created.style.left = left+"px";
created.style.top = top+"px";
created.innerHTML = this.emojiHTML;
created.addEventListener("click", function(e){
if (e.target.tagName === "IMG"){
input.val(input.val()+e.target.getAttribute("alt"));
input.trigger("change");
input.focus();
}
e.stopPropagation();
});
return created;
};
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.currentKeyboard = me.generateKeyboardFor($(".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 generated = [];
let addDeclaration = decl => {
generated.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join(""));
};
let skinTones = [
"1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"
];
for(let line of contents.split("\n")){
if (line[0] === '@'){
generated.push("___");
}
else{
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);
skinTones.map(skinTone => declPre+skinTone+declPost).forEach(addDeclaration);
}
else{
addDeclaration(decl);
}
}
}
let start = "<p style='font-size:13px;color:#444;margin:4px;text-align:center'>Please, note that most emoji will not show up properly in the text box above, but they will display in the tweet.</p>";
this.emojiHTML = start+TD.util.cleanWithEmoji(generated.join("")).replace(/___/g, "<div class='separator'></div>");
}).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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
(function($, $TD, TD){ (function($, $TD, $TDX, TD){
// //
// Variable: Current highlighted column jQuery object. // Variable: Current highlighted column jQuery object.
// //
@@ -10,71 +10,14 @@
var highlightedTweetEle; 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(){ var onAppReady = [];
// Settings button hook
$("[data-action='settings-menu']").click(function(){ //
setTimeout(function(){ // Variable: DOM object containing the main app element.
var menu = $(".js-dropdown-content").children("ul").first(); //
if (menu.length === 0)return; var app = $(document.body).children(".js-app");
menu.children(".drp-h-divider").last().after([
'<li class="is-selectable" data-std><a href="#" data-action="td-settings">'+$TD.brandName+' settings</a></li>',
'<li class="is-selectable" data-std><a href="#" data-action="td-plugins">'+$TD.brandName+' 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'>&#9658; 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();
}
};
// //
// Function: Prepends code at the beginning of a function. If the prepended function returns true, execution of the original function is cancelled. // Function: Prepends code at the beginning of a function. If the prepended function returns true, execution of the original function is cancelled.
@@ -112,6 +55,7 @@
html.css("border", "0"); html.css("border", "0");
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".js-quote-detail").removeClass("is-actionable");
var url = html.find("time").first().children("a").first().attr("href") || ""; var url = html.find("time").first().children("a").first().attr("href") || "";
@@ -136,23 +80,6 @@
return tags.join(""); 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. // Block: Hook into settings object to detect when the settings change.
// //
@@ -167,7 +94,7 @@
}); });
// //
// Block: Force popup notification settings. // Block: Enable popup notifications.
// //
TD.controller.notifications.hasNotifications = function(){ TD.controller.notifications.hasNotifications = function(){
return true; return true;
@@ -177,6 +104,49 @@
return true; 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. // Block: Expand shortened links on hover or display tooltip.
// //
@@ -198,7 +168,7 @@
return; return;
} }
if ($TD.expandLinksOnHover){ if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){ tooltipTimer = window.setTimeout(function(){
var expanded = me.attr("data-full-url"); var expanded = me.attr("data-full-url");
expanded = cutStart(expanded, "https://"); expanded = cutStart(expanded, "https://");
@@ -217,7 +187,7 @@
} }
} }
else if (e.type === "mouseleave"){ else if (e.type === "mouseleave"){
if ($TD.expandLinksOnHover){ if ($TDX.expandLinksOnHover){
var prevText = me.attr("td-prev-text"); var prevText = me.attr("td-prev-text");
if (prevText){ if (prevText){
@@ -256,7 +226,7 @@
var soundEle = document.getElementById("update-sound"); var soundEle = document.getElementById("update-sound");
soundEle.play = prependToFunction(soundEle.play, function(){ soundEle.play = prependToFunction(soundEle.play, function(){
return $TD.muteNotifications || $TD.hasCustomNotificationSound; return $TDX.muteNotifications || $TDX.hasCustomNotificationSound;
}); });
})(); })();
@@ -374,6 +344,11 @@
}; };
var clickUpload = function(){ var clickUpload = function(){
$(document).one("uiFilesAdded", function(){
getScroller().scrollTop(prevScrollTop);
$(".js-drawer").find(".js-compose-text").first()[0].focus();
});
var button = $(".js-add-image-button").first(); var button = $(".js-add-image-button").first();
var scroller = getScroller(); var scroller = getScroller();
@@ -386,7 +361,7 @@
$TD.clickUploadImage(Math.floor(buttonPos.left), Math.floor(buttonPos.top)); $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); lastPasteElement = $(this);
$TD.tryPasteImage(); $TD.tryPasteImage();
}); });
@@ -425,13 +400,6 @@
lastPasteElement = null; lastPasteElement = null;
} }
}; };
window.TDGF_tryPasteImageFinish = function(){
setTimeout(function(){
getScroller().scrollTop(prevScrollTop);
$(".js-drawer").find(".js-compose-text").first()[0].focus();
}, 10);
};
})(); })();
// //
@@ -524,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. // Block: Work around clipboard HTML formatting.
// //
@@ -543,12 +530,79 @@
styleOfficial.sheet.insertRule(".txt-base-smallest .badge-verified:before { height: 13px !important; }", 0); // fix cut off badge icon 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(".keyboard-shortcut-list { vertical-align: top; }", 0); // fix keyboard navigation alignment
if ($TD.hasCustomBrowserCSS){ styleOfficial.sheet.insertRule(".is-video a:not([href*='youtu']), .is-gif .js-media-gif-container { cursor: alias; }", 0); // change cursor on unsupported videos
var styleCustom = document.createElement("style"); 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
styleCustom.innerHTML = $TD.customBrowserCSS;
document.head.appendChild(styleCustom);
}
TD.services.TwitterActionRetweetedRetweet.prototype.iconClass = "icon-retweet icon-retweet-color txt-base-medium"; // fix retweet icon mismatch TD.services.TwitterActionRetweetedRetweet.prototype.iconClass = "icon-retweet icon-retweet-color txt-base-medium"; // fix retweet icon mismatch
window.TDGF_reinjectCustomCSS = function(styles){
$("#tweetduck-custom-css").remove();
if (styles && styles.length){
$(document.head).append("<style type='text/css' id='tweetduck-custom-css'>"+styles+"</style>");
}
};
})(); })();
})($, $TD, TD);
//
// 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);

View File

@@ -1,19 +1,49 @@
(function($, $TD, TD){ (function($, $TD, $TDX, TD){
var isDebugging = false;
$(document).keydown(function(e){ $(document).keydown(function(e){
// ============================== // ==========================
// F4 key - simulate notification // F4 key - toggle debug mode
// ============================== // ==========================
if (e.keyCode === 115){ if (e.keyCode === 115){
var col = TD.controller.columnManager.getAllOrdered()[0]; isDebugging = !isDebugging;
$(".app-title").first().css("background-color", isDebugging ? "#5A6B75" : "#292F33");
}
// Debug mode handling
else if (isDebugging){
e.preventDefault();
$.publish("/notifications/new",[{ // ===================================
column: col, // N key - simulate popup notification
items: [ // ===================================
col.updateArray[Math.floor(Math.random()*col.updateArray.length)]
] if (e.keyCode === 78){
}]); var col = TD.controller.columnManager.getAllOrdered()[0];
$.publish("/notifications/new",[{
column: col,
items: [
col.updateArray[Math.floor(Math.random()*col.updateArray.length)]
]
}]);
}
// ===================================
// S key - simulate sound notification
// ===================================
else if (e.keyCode === 83){
if ($TDX.hasCustomNotificationSound){
$TD.onTweetSound();
}
else{
document.getElementById("update-sound").play();
}
}
} }
}); });
})($, $TD, TD); })($, $TD, $TDX, TD);

View File

@@ -1,4 +1,4 @@
(function($TD){ (function($TD, $TDX){
// //
// Variable: Collection of all <a> tags. // Variable: Collection of all <a> tags.
// //
@@ -51,7 +51,7 @@
return; return;
} }
if ($TD.expandLinksOnHover){ if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){ tooltipTimer = window.setTimeout(function(){
var expanded = url; var expanded = url;
expanded = cutStart(expanded, "https://"); expanded = cutStart(expanded, "https://");
@@ -73,7 +73,7 @@
addEventListener(links, "mouseleave", function(e){ addEventListener(links, "mouseleave", function(e){
if (!e.currentTarget.hasAttribute("data-full-url"))return; if (!e.currentTarget.hasAttribute("data-full-url"))return;
if ($TD.expandLinksOnHover){ if ($TDX.expandLinksOnHover){
var prevText = e.currentTarget.getAttribute("td-prev-text"); var prevText = e.currentTarget.getAttribute("td-prev-text");
if (prevText){ if (prevText){
@@ -146,4 +146,4 @@
document.body.addEventListener("mouseleave", function(){ document.body.addEventListener("mouseleave", function(){
document.body.classList.remove("td-hover"); document.body.classList.remove("td-hover");
}); });
})($TD); })($TD, $TDX);

View File

@@ -7,33 +7,38 @@
// //
// Constant: Update exe file name. // Constant: Update exe file name.
// //
const updateFileName = $TDU.brandName+".Update.exe"; const updateFileName = "TweetDuck.Update.exe";
// //
// Constant: Url that returns JSON data about latest version. // Constant: Url that returns JSON data about latest version.
// //
const updateCheckUrlLatest = "https://api.github.com/repos/chylex/"+$TDU.brandName+"/releases/latest"; const updateCheckUrlLatest = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
// //
// Constant: Url that returns JSON data about all versions, including prereleases. // Constant: Url that returns JSON data about all versions, including prereleases.
// //
const updateCheckUrlAll = "https://api.github.com/repos/chylex/"+$TDU.brandName+"/releases"; const updateCheckUrlAll = "https://api.github.com/repos/chylex/TweetDuck/releases";
//
// Constant: Fallback url in case the update installer file is missing.
//
const updateDownloadFallback = "https://tweetduck.chylex.com/#download";
// //
// Function: Creates the update notification element. Removes the old one if already exists. // Function: Creates the update notification element. Removes the old one if already exists.
// //
var createUpdateNotificationElement = function(version, download){ var displayNotification = function(version, download){
var outdated = version === "unsupported"; var outdated = version === "unsupported";
var ele = $("#tweetdck-update"); var ele = $("#tweetduck-update");
var existed = ele.length > 0; var existed = ele.length > 0;
if (existed > 0){ if (existed){
ele.remove(); ele.remove();
} }
var html = outdated ? [ var html = outdated ? [
"<div id='tweetdck-update'>", "<div id='tweetduck-update'>",
"<p class='tdu-title'>Unsupported System</p>", "<p class='tdu-title'>Unsupported System</p>",
"<p class='tdu-info'>You will not receive updates.</p>", "<p class='tdu-info'>You will not receive updates.</p>",
"<div class='tdu-buttons'>", "<div class='tdu-buttons'>",
@@ -42,8 +47,8 @@
"</div>", "</div>",
"</div>" "</div>"
] : [ ] : [
"<div id='tweetdck-update'>", "<div id='tweetduck-update'>",
"<p class='tdu-title'>"+$TDU.brandName+" Update</p>", "<p class='tdu-title'>TweetDuck Update</p>",
"<p class='tdu-info'>Version "+version+" is now available.</p>", "<p class='tdu-info'>Version "+version+" is now available.</p>",
"<div class='tdu-buttons'>", "<div class='tdu-buttons'>",
"<button class='btn btn-positive tdu-btn-download'><span class='label'>Download</span></button>", "<button class='btn btn-positive tdu-btn-download'><span class='label'>Download</span></button>",
@@ -54,7 +59,7 @@
$(document.body).append(html.join("")); $(document.body).append(html.join(""));
ele = $("#tweetdck-update"); ele = $("#tweetduck-update");
var buttonDiv = ele.children("div.tdu-buttons").first(); var buttonDiv = ele.children("div.tdu-buttons").first();
@@ -106,7 +111,13 @@
buttonDiv.children(".tdu-btn-download").click(function(){ buttonDiv.children(".tdu-btn-download").click(function(){
ele.remove(); ele.remove();
$TDU.onUpdateAccepted(version, download);
if (download){
$TDU.onUpdateAccepted(version, download);
}
else{
$TDU.openBrowser(updateDownloadFallback);
}
}); });
buttonDiv.children(".tdu-btn-unsupported").click(function(){ buttonDiv.children(".tdu-btn-unsupported").click(function(){
@@ -125,36 +136,31 @@
return ele; return ele;
}; };
//
// Function: Returns milliseconds until the start of the next hour, with an offset in seconds that can skip an hour if the clock would roll over too soon.
//
var getTimeUntilNextHour = function(offset){
var now = new Date();
var offset = new Date(+now+offset*1000);
return new Date(offset.getFullYear(), offset.getMonth(), offset.getDate(), offset.getHours()+1, 0, 0)-now;
};
// //
// Function: Runs an update check and updates all DOM elements appropriately. // Function: Runs an update check and updates all DOM elements appropriately.
// //
var runUpdateCheck = function(force, eventID){ var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){
if (!$TDU.isSystemSupported){
if ($TDU.dismissedVersionTag !== "unsupported"){
createUpdateNotificationElement("unsupported");
}
return;
}
clearTimeout(updateCheckTimeoutID); clearTimeout(updateCheckTimeoutID);
updateCheckTimeoutID = setTimeout(runUpdateCheck, 1000*60*60); // 1 hour updateCheckTimeoutID = setTimeout($TDU.triggerUpdateCheck, getTimeUntilNextHour(60*30)); // 30 minute offset
if (!$TDU.updateCheckEnabled && !force){
return;
}
var allowPre = $TDU.allowPreReleases;
$.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){ $.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){
var release = allowPre ? response[0] : response; var release = allowPre ? response[0] : response;
var tagName = release.tag_name; var tagName = release.tag_name;
var hasUpdate = tagName !== $TDU.versionTag && tagName !== $TDU.dismissedVersionTag && release.assets.length > 0; var hasUpdate = tagName !== versionTag && tagName !== dismissedVersionTag && release.assets.length > 0;
if (hasUpdate){ if (hasUpdate){
var obj = release.assets.find(asset => asset.name === updateFileName) || release.assets[0]; var obj = release.assets.find(asset => asset.name === updateFileName) || { browser_download_url: "" };
createUpdateNotificationElement(tagName, obj.browser_download_url); displayNotification(tagName, obj.browser_download_url);
} }
if (eventID){ // ignore undefined and 0 if (eventID){ // ignore undefined and 0
@@ -166,6 +172,6 @@
// //
// Block: Setup global functions. // Block: Setup global functions.
// //
window.TDUF_displayNotification = displayNotification;
window.TDUF_runUpdateCheck = runUpdateCheck; window.TDUF_runUpdateCheck = runUpdateCheck;
runUpdateCheck();
})($, $TDU); })($, $TDU);

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props')" /> <Import Project="packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props')" /> <Import Project="packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -10,11 +10,10 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetDck</RootNamespace> <RootNamespace>TweetDck</RootNamespace>
<AssemblyName Condition=" '$(Configuration)' == 'Debug' ">TweetDick</AssemblyName> <AssemblyName>TweetDuck</AssemblyName>
<AssemblyName Condition=" '$(Configuration)' == 'Release' ">TweetDuck</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>783c0e30</NuGetPackageImportStamp> <NuGetPackageImportStamp>886d3074</NuGetPackageImportStamp>
<TargetFrameworkProfile> <TargetFrameworkProfile>
</TargetFrameworkProfile> </TargetFrameworkProfile>
<PublishUrl>publish\</PublishUrl> <PublishUrl>publish\</PublishUrl>
@@ -59,9 +58,6 @@
</DefineConstants> </DefineConstants>
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<AssemblyName>TweetDuck</AssemblyName>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@@ -71,6 +67,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="Configuration\LockManager.cs" /> <Compile Include="Configuration\LockManager.cs" />
<Compile Include="Configuration\UserConfig.cs" /> <Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Core\Bridge\PropertyBridge.cs" />
<Compile Include="Core\Controls\ControlExtensions.cs" /> <Compile Include="Core\Controls\ControlExtensions.cs" />
<Compile Include="Core\Controls\FlatButton.cs"> <Compile Include="Core\Controls\FlatButton.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
@@ -112,6 +109,7 @@
<Compile Include="Core\Handling\FileDialogHandler.cs" /> <Compile Include="Core\Handling\FileDialogHandler.cs" />
<Compile Include="Core\Handling\JavaScriptDialogHandler.cs" /> <Compile Include="Core\Handling\JavaScriptDialogHandler.cs" />
<Compile Include="Core\Handling\LifeSpanHandler.cs" /> <Compile Include="Core\Handling\LifeSpanHandler.cs" />
<Compile Include="Core\Notification\SoundNotification.cs" />
<Compile Include="Core\Notification\TweetNotification.cs" /> <Compile Include="Core\Notification\TweetNotification.cs" />
<Compile Include="Core\Other\FormAbout.cs"> <Compile Include="Core\Other\FormAbout.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
@@ -190,6 +188,7 @@
</Compile> </Compile>
<Compile Include="Core\Notification\NotificationFlags.cs" /> <Compile Include="Core\Notification\NotificationFlags.cs" />
<Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" /> <Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Core\Utils\TwoKeyDictionary.cs" />
<Compile Include="Core\Utils\WindowState.cs" /> <Compile Include="Core\Utils\WindowState.cs" />
<Compile Include="Core\Utils\WindowsUtils.cs" /> <Compile Include="Core\Utils\WindowsUtils.cs" />
<Compile Include="Core\Bridge\TweetDeckBridge.cs" /> <Compile Include="Core\Bridge\TweetDeckBridge.cs" />
@@ -222,6 +221,7 @@
<Compile Include="Plugins\PluginManager.cs" /> <Compile Include="Plugins\PluginManager.cs" />
<Compile Include="Plugins\PluginScriptGenerator.cs" /> <Compile Include="Plugins\PluginScriptGenerator.cs" />
<Compile Include="Reporter.cs" /> <Compile Include="Reporter.cs" />
<Compile Include="Updates\Events\UpdateDismissedEventArgs.cs" />
<Compile Include="Updates\FormUpdateDownload.cs"> <Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -238,8 +238,8 @@
<Compile Include="Core\Utils\BrowserUtils.cs" /> <Compile Include="Core\Utils\BrowserUtils.cs" />
<Compile Include="Core\Utils\HardwareAcceleration.cs" /> <Compile Include="Core\Utils\HardwareAcceleration.cs" />
<Compile Include="Core\Utils\NativeMethods.cs" /> <Compile Include="Core\Utils\NativeMethods.cs" />
<Compile Include="Updates\UpdateAcceptedEventArgs.cs" /> <Compile Include="Updates\Events\UpdateAcceptedEventArgs.cs" />
<Compile Include="Updates\UpdateCheckEventArgs.cs" /> <Compile Include="Updates\Events\UpdateCheckEventArgs.cs" />
<Compile Include="Updates\UpdateHandler.cs" /> <Compile Include="Updates\UpdateHandler.cs" />
<Compile Include="Updates\UpdateInfo.cs" /> <Compile Include="Updates\UpdateInfo.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
@@ -252,7 +252,6 @@
<Compile Include="Resources\ScriptLoader.cs" /> <Compile Include="Resources\ScriptLoader.cs" />
<Compile Include="Updates\UpdaterSettings.cs" /> <Compile Include="Updates\UpdaterSettings.cs" />
<None Include="Configuration\app.config" /> <None Include="Configuration\app.config" />
<None Include="Configuration\packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client"> <BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
@@ -304,6 +303,7 @@
</ContentWithTargetPath> </ContentWithTargetPath>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Configuration\packages.config" />
<None Include="Resources\icon-small.ico" /> <None Include="Resources\icon-small.ico" />
<None Include="Resources\icon-tray-new.ico" /> <None Include="Resources\icon-tray-new.ico" />
<None Include="Resources\icon-tray.ico" /> <None Include="Resources\icon-tray.ico" />
@@ -325,12 +325,12 @@
<PropertyGroup> <PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> <ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup> </PropertyGroup>
<Error Condition="!Exists('packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets'))" /> <Error Condition="!Exists('packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets'))" />
<Error Condition="!Exists('packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets'))" /> <Error Condition="!Exists('packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets'))" />
<Error Condition="!Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.53.0.1\build\CefSharp.Common.props'))" /> <Error Condition="!Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.55.0.0\build\CefSharp.Common.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets'))" /> <Error Condition="!Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.props'))" /> <Error Condition="!Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.props'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets'))" /> <Error Condition="!Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets'))" />
</Target> </Target>
<PropertyGroup> <PropertyGroup>
<PostBuildEvent>del "$(TargetPath).config" <PostBuildEvent>del "$(TargetPath).config"
@@ -349,12 +349,20 @@ mkdir "$(TargetDir)plugins\official"
mkdir "$(TargetDir)plugins\user" mkdir "$(TargetDir)plugins\user"
xcopy "$(ProjectDir)Resources\Plugins\*" "$(TargetDir)plugins\official\" /E /Y xcopy "$(ProjectDir)Resources\Plugins\*" "$(TargetDir)plugins\official\" /E /Y
rmdir "$(ProjectDir)\bin\Debug" 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> </PropertyGroup>
<Import Project="packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets')" /> <Import Project="packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2883.1552\build\cef.redist.x86.targets')" />
<Import Project="packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets')" /> <Import Project="packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2883.1552\build\cef.redist.x64.targets')" />
<Import Project="packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.53.0.1\build\CefSharp.Common.targets')" /> <Import Project="packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.55.0.0\build\CefSharp.Common.targets')" />
<Import Project="packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.53.0.1\build\CefSharp.WinForms.targets')" /> <Import Project="packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.55.0.0\build\CefSharp.WinForms.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">

View File

@@ -1,21 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013 # Visual Studio 2013
VisualStudioVersion = 12.0.40629.0 VisualStudioVersion = 12.0.40629.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDck", "TweetDck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDck", "TweetDck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "tests\UnitTests.csproj", "{A958FA7A-4A2C-42A7-BFA0-159343483F4E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x86 = Debug|x86 Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x86 = Release|x86 Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|Any CPU.ActiveCfg = Debug|x86
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|x86.ActiveCfg = Debug|x86 {2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|x86.ActiveCfg = Debug|x86
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|x86.Build.0 = Debug|x86 {2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|x86.Build.0 = Debug|x86
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|x86.Deploy.0 = Debug|x86 {2389A7CD-E0D3-4706-8294-092929A33A2D}.Debug|x86.Deploy.0 = Debug|x86
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Release|Any CPU.ActiveCfg = Release|x86
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Release|x86.ActiveCfg = Release|x86 {2389A7CD-E0D3-4706-8294-092929A33A2D}.Release|x86.ActiveCfg = Release|x86
{2389A7CD-E0D3-4706-8294-092929A33A2D}.Release|x86.Build.0 = Release|x86 {2389A7CD-E0D3-4706-8294-092929A33A2D}.Release|x86.Build.0 = Release|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|Any CPU.ActiveCfg = Debug|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|x86.ActiveCfg = Debug|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|x86.Build.0 = Debug|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Release|Any CPU.ActiveCfg = Release|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Release|x86.ActiveCfg = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

2
TweetDck.sln.DotSettings Normal file
View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="UnitTests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String></wpf:ResourceDictionary>

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace TweetDck.Updates{ namespace TweetDck.Updates.Events{
class UpdateAcceptedEventArgs : EventArgs{ class UpdateAcceptedEventArgs : EventArgs{
public readonly UpdateInfo UpdateInfo; public readonly UpdateInfo UpdateInfo;

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace TweetDck.Updates{ namespace TweetDck.Updates.Events{
class UpdateCheckEventArgs : EventArgs{ class UpdateCheckEventArgs : EventArgs{
public int EventId { get; private set; } public int EventId { get; private set; }
public bool UpdateAvailable { get; private set; } public bool UpdateAvailable { get; private set; }
@@ -12,4 +12,4 @@ namespace TweetDck.Updates{
LatestVersion = latestVersion; LatestVersion = latestVersion;
} }
} }
} }

View File

@@ -0,0 +1,11 @@
using System;
namespace TweetDck.Updates.Events{
class UpdateDismissedEventArgs : EventArgs{
public readonly string VersionTag;
public UpdateDismissedEventArgs(string versionTag){
this.VersionTag = versionTag;
}
}
}

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Windows.Forms; using System.Windows.Forms;
@@ -9,7 +8,7 @@ using TweetDck.Core.Utils;
namespace TweetDck.Updates{ namespace TweetDck.Updates{
sealed partial class FormUpdateDownload : Form{ sealed partial class FormUpdateDownload : Form{
private const double BytesToMB = 1024.0*1024.0; private const double BytesToKB = 1024.0;
public string InstallerPath{ public string InstallerPath{
get{ get{
@@ -69,7 +68,7 @@ namespace TweetDck.Updates{
progressDownload.SetValueInstant(1000); progressDownload.SetValueInstant(1000);
} }
labelStatus.Text = (e.BytesReceived/BytesToMB).ToString("0.0", CultureInfo.CurrentCulture)+" MB"; labelStatus.Text = (long)(e.BytesReceived/BytesToKB)+" kB";
} }
else{ else{
if (progressDownload.Style != ProgressBarStyle.Continuous){ if (progressDownload.Style != ProgressBarStyle.Continuous){
@@ -77,7 +76,7 @@ namespace TweetDck.Updates{
} }
progressDownload.SetValueInstant(e.ProgressPercentage*10); progressDownload.SetValueInstant(e.ProgressPercentage*10);
labelStatus.Text = (e.BytesReceived/BytesToMB).ToString("0.0", CultureInfo.CurrentCulture)+" / "+(e.TotalBytesToReceive/BytesToMB).ToString("0.0", CultureInfo.CurrentCulture)+" MB"; labelStatus.Text = (long)(e.BytesReceived/BytesToKB)+" / "+(long)(e.TotalBytesToReceive/BytesToKB)+" kB";
} }
}); });
} }

View File

@@ -5,14 +5,28 @@ using TweetDck.Core;
using TweetDck.Core.Controls; using TweetDck.Core.Controls;
using TweetDck.Core.Utils; using TweetDck.Core.Utils;
using TweetDck.Resources; using TweetDck.Resources;
using TweetDck.Updates.Events;
namespace TweetDck.Updates{ namespace TweetDck.Updates{
class UpdateHandler{ class UpdateHandler{
private static bool IsSystemSupported{
get{
return true; // Environment.OSVersion.Version >= new Version("6.1"); // 6.1 NT version = Windows 7
}
}
public UpdaterSettings Settings{
get{
return settings;
}
}
private readonly ChromiumWebBrowser browser; private readonly ChromiumWebBrowser browser;
private readonly FormBrowser form; private readonly FormBrowser form;
private readonly UpdaterSettings settings; private readonly UpdaterSettings settings;
public event EventHandler<UpdateAcceptedEventArgs> UpdateAccepted; public event EventHandler<UpdateAcceptedEventArgs> UpdateAccepted;
public event EventHandler<UpdateDismissedEventArgs> UpdateDismissed;
public event EventHandler<UpdateCheckEventArgs> CheckFinished; public event EventHandler<UpdateCheckEventArgs> CheckFinished;
private int lastEventId; private int lastEventId;
@@ -23,75 +37,67 @@ namespace TweetDck.Updates{
this.settings = settings; this.settings = settings;
browser.FrameLoadEnd += browser_FrameLoadEnd; browser.FrameLoadEnd += browser_FrameLoadEnd;
browser.RegisterJsObject("$TDU", new Bridge(this)); browser.RegisterAsyncJsObject("$TDU", new Bridge(this));
} }
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){ private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && BrowserUtils.IsTweetDeckWebsite(e.Frame)){ if (e.Frame.IsMain && BrowserUtils.IsTweetDeckWebsite(e.Frame)){
ScriptLoader.ExecuteFile(e.Frame, "update.js"); ScriptLoader.ExecuteFile(e.Frame, "update.js");
Check(false);
} }
} }
public int Check(bool force){ public int Check(bool force){
browser.ExecuteScriptAsync("TDUF_runUpdateCheck", force, ++lastEventId); if (IsSystemSupported){
return lastEventId; if (Program.UserConfig.EnableUpdateCheck || force){
string dismissedUpdate = force || settings.DismissedUpdate == null ? string.Empty : settings.DismissedUpdate;
browser.ExecuteScriptAsync("TDUF_runUpdateCheck", ++lastEventId, Program.VersionTag, dismissedUpdate, settings.AllowPreReleases);
return lastEventId;
}
return 0;
}
else if (settings.DismissedUpdate != "unsupported"){
browser.ExecuteScriptAsync("TDUF_displayNotification", "unsupported");
}
return -1;
} }
private void TriggerUpdateAcceptedEvent(UpdateAcceptedEventArgs args){ private void TriggerUpdateAcceptedEvent(UpdateAcceptedEventArgs args){
if (UpdateAccepted != null){ if (UpdateAccepted != null){
form.InvokeSafe(() => UpdateAccepted(this, args)); form.InvokeAsyncSafe(() => UpdateAccepted(this, args));
} }
} }
private void TriggerUpdateDismissedEvent(UpdateDismissedEventArgs args){
form.InvokeAsyncSafe(() => {
settings.DismissedUpdate = args.VersionTag;
if (UpdateDismissed != null){
UpdateDismissed(this, args);
}
});
}
private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){ private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){
if (CheckFinished != null){ if (CheckFinished != null){
form.InvokeSafe(() => CheckFinished(this, args)); form.InvokeAsyncSafe(() => CheckFinished(this, args));
} }
} }
public class Bridge{ public class Bridge{
public string BrandName{
get{
return Program.BrandName;
}
}
public string VersionTag{
get{
return Program.VersionTag;
}
}
public bool UpdateCheckEnabled{
get{
return Program.UserConfig.EnableUpdateCheck;
}
}
public string DismissedVersionTag{
get{
return Program.UserConfig.DismissedUpdate ?? string.Empty;
}
}
public bool AllowPreReleases{
get{
return owner.settings.AllowPreReleases;
}
}
public bool IsSystemSupported{
get{
return Environment.OSVersion.Version >= new Version("6.1"); // 6.1 NT version = Windows 7
}
}
private readonly UpdateHandler owner; private readonly UpdateHandler owner;
public Bridge(UpdateHandler owner){ public Bridge(UpdateHandler owner){
this.owner = owner; this.owner = owner;
} }
public void TriggerUpdateCheck(){
owner.Check(false);
}
public void OnUpdateCheckFinished(int eventId, bool isUpdateAvailable, string latestVersion){ public void OnUpdateCheckFinished(int eventId, bool isUpdateAvailable, string latestVersion){
owner.TriggerCheckFinishedEvent(new UpdateCheckEventArgs(eventId, isUpdateAvailable, latestVersion)); owner.TriggerCheckFinishedEvent(new UpdateCheckEventArgs(eventId, isUpdateAvailable, latestVersion));
} }
@@ -101,10 +107,7 @@ namespace TweetDck.Updates{
} }
public void OnUpdateDismissed(string versionTag){ public void OnUpdateDismissed(string versionTag){
owner.form.InvokeSafe(() => { owner.TriggerUpdateDismissedEvent(new UpdateDismissedEventArgs(versionTag));
Program.UserConfig.DismissedUpdate = versionTag;
Program.UserConfig.Save();
});
} }
public void OpenBrowser(string url){ public void OpenBrowser(string url){

View File

@@ -1,5 +1,6 @@
namespace TweetDck.Updates{ namespace TweetDck.Updates{
class UpdaterSettings{ class UpdaterSettings{
public bool AllowPreReleases { get; set; } public bool AllowPreReleases { get; set; }
public string DismissedUpdate { get; set; }
} }
} }

View File

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

View File

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

View File

@@ -10,14 +10,14 @@
[Setup] [Setup]
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06} AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
AppName={#MyAppName} AppName={#MyAppName} Portable
AppVersion={#MyAppVersion} AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher} AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL} AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL} AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL} AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\{#MyAppName} DefaultDirName={sd}\{#MyAppName}
DefaultGroupName={#MyAppName} DefaultGroupName={#MyAppName}
OutputBaseFilename={#MyAppName}.Portable OutputBaseFilename={#MyAppName}.Portable
VersionInfoVersion={#MyAppVersion} VersionInfoVersion={#MyAppVersion}
@@ -36,7 +36,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Files] [Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,devtools_resources.pak,d3dcompiler_43.dll,widevinecdmadapter.dll,debug.js" Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.pdb,devtools_resources.pak,d3dcompiler_43.dll,widevinecdmadapter.dll,debug.js"
[Run] [Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec

View File

@@ -8,11 +8,11 @@
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06" #define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe") #define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
#define CefVersion "3.2785.1486.0" #define CefVersion "3.2883.1552.0"
[Setup] [Setup]
AppId={{{#MyAppID}} AppId={{{#MyAppID}}
AppName={#MyAppName} AppName={#MyAppName} Update
AppVersion={#MyAppVersion} AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher} AppPublisher={#MyAppPublisher}
@@ -41,7 +41,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Files] [Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.dll,*.pak,*.bin,*.dat,debug.js" Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.xml,*.pdb,*.dll,*.pak,*.bin,*.dat,debug.js"
[Icons] [Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable

View File

@@ -0,0 +1,143 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using TweetDck.Core.Other.Settings.Export;
namespace UnitTests.Core.Settings{
[TestClass]
public class TestCombinedFileStream{
[TestMethod]
public void TestNoFiles(){
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_empty"))){
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_empty"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_empty"))){
Assert.IsNull(cfs.ReadFile());
}
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_empty"))){
Assert.IsNull(cfs.SkipFile());
}
}
[TestMethod]
public void TestEmptyFiles(){
TestUtils.WriteText("cfs_input_empty_1", string.Empty);
TestUtils.WriteText("cfs_input_empty_2", string.Empty);
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_blank_files"))){
cfs.WriteFile("id1", "cfs_input_empty_1");
cfs.WriteFile("id2", "cfs_input_empty_2");
cfs.WriteFile("id2_clone", "cfs_input_empty_2");
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_blank_files"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_blank_files"))){
CombinedFileStream.Entry entry1 = cfs.ReadFile();
string entry2key = cfs.SkipFile();
CombinedFileStream.Entry entry3 = cfs.ReadFile();
Assert.IsNull(cfs.ReadFile());
Assert.IsNull(cfs.SkipFile());
Assert.AreEqual("id1", entry1.KeyName);
Assert.AreEqual("id1", entry1.Identifier);
CollectionAssert.AreEqual(new string[0], entry1.KeyValue);
Assert.AreEqual("id2", entry2key);
Assert.AreEqual("id2_clone", entry3.KeyName);
Assert.AreEqual("id2_clone", entry3.Identifier);
CollectionAssert.AreEqual(new string[0], entry3.KeyValue);
entry1.WriteToFile("cfs_blank_file_1");
entry3.WriteToFile("cfs_blank_file_2");
TestUtils.DeleteFileOnExit("cfs_blank_file_1");
TestUtils.DeleteFileOnExit("cfs_blank_file_2");
}
Assert.IsTrue(File.Exists("cfs_blank_file_1"));
Assert.IsTrue(File.Exists("cfs_blank_file_2"));
Assert.AreEqual(string.Empty, TestUtils.ReadText("cfs_blank_file_1"));
Assert.AreEqual(string.Empty, TestUtils.ReadText("cfs_blank_file_2"));
}
[TestMethod]
public void TestTextFilesAndComplexKeys(){
TestUtils.WriteText("cfs_input_text_1", "Hello World!"+Environment.NewLine);
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_text_files"))){
cfs.WriteFile(new string[]{ "key1", "a", "bb", "ccc", "dddd" }, "cfs_input_text_1");
cfs.WriteFile(new string[]{ "key2", "a", "bb", "ccc", "dddd" }, "cfs_input_text_1");
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_text_files"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_text_files"))){
CombinedFileStream.Entry entry = cfs.ReadFile();
Assert.AreEqual("key2", cfs.SkipFile());
Assert.IsNull(cfs.ReadFile());
Assert.IsNull(cfs.SkipFile());
Assert.AreEqual("key1|a|bb|ccc|dddd", entry.Identifier);
Assert.AreEqual("key1", entry.KeyName);
CollectionAssert.AreEqual(new string[]{ "a", "bb", "ccc", "dddd" }, entry.KeyValue);
entry.WriteToFile("cfs_text_file_1");
TestUtils.DeleteFileOnExit("cfs_text_file_1");
}
Assert.IsTrue(File.Exists("cfs_text_file_1"));
Assert.AreEqual("Hello World!"+Environment.NewLine, TestUtils.ReadText("cfs_text_file_1"));
}
[TestMethod]
public void TestEntryWriteWithDirectory(){
if (Directory.Exists("cfs_directory")){
Directory.Delete("cfs_directory", true);
}
TestUtils.WriteText("cfs_input_dir_1", "test");
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_dir_test"))){
cfs.WriteFile("key1", "cfs_input_dir_1");
cfs.WriteFile("key2", "cfs_input_dir_1");
cfs.WriteFile("key3", "cfs_input_dir_1");
cfs.WriteFile("key4", "cfs_input_dir_1");
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_dir_test"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_dir_test"))){
try{
cfs.ReadFile().WriteToFile("cfs_directory/cfs_dir_test_file", false);
Assert.Fail("WriteToFile did not trigger an exception.");
}catch(DirectoryNotFoundException){}
cfs.ReadFile().WriteToFile("cfs_directory/cfs_dir_test_file", true);
cfs.ReadFile().WriteToFile("cfs_dir_test_file", true);
cfs.ReadFile().WriteToFile("cfs_dir_test_file.txt", true);
TestUtils.DeleteFileOnExit("cfs_dir_test_file");
TestUtils.DeleteFileOnExit("cfs_dir_test_file.txt");
}
Assert.IsTrue(Directory.Exists("cfs_directory"));
Assert.IsTrue(File.Exists("cfs_directory/cfs_dir_test_file"));
Assert.IsTrue(File.Exists("cfs_dir_test_file"));
Assert.IsTrue(File.Exists("cfs_dir_test_file.txt"));
Assert.AreEqual("test", TestUtils.ReadText("cfs_directory/cfs_dir_test_file"));
Assert.AreEqual("test", TestUtils.ReadText("cfs_dir_test_file"));
Assert.AreEqual("test", TestUtils.ReadText("cfs_dir_test_file.txt"));
Directory.Delete("cfs_directory", true);
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDck.Core.Utils;
namespace UnitTests.Core.Utils{
[TestClass]
public class TestBrowserUtils{
[TestMethod]
public void TestGetFileNameFromUrl(){
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html"));
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html?"));
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html?param1=abc&param2=false"));
Assert.AreEqual("index", BrowserUtils.GetFileNameFromUrl("http://test.com/index"));
Assert.AreEqual("index.", BrowserUtils.GetFileNameFromUrl("http://test.com/index."));
Assert.IsNull(BrowserUtils.GetFileNameFromUrl("http://test.com/"));
}
}
}

View File

@@ -0,0 +1,157 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using TweetDck.Core.Utils;
namespace UnitTests.Core.Utils{
[TestClass]
public class TestCommandLineArgs{
[TestMethod]
public void TestEmpty(){
CommandLineArgs args = new CommandLineArgs();
Assert.AreEqual(0, args.Count);
Assert.AreEqual(string.Empty, args.ToString());
Assert.IsFalse(args.HasFlag("x"));
Assert.IsFalse(args.HasValue("x"));
Assert.AreEqual("default", args.GetValue("x", "default"));
args.RemoveFlag("x");
args.RemoveValue("x");
var dict = new Dictionary<string, string>();
args.ToDictionary(dict);
Assert.AreEqual(0, dict.Count);
}
[TestMethod]
public void TestFlags(){
CommandLineArgs args = new CommandLineArgs();
args.AddFlag("my_test_flag_1");
args.AddFlag("my_test_flag_2");
args.AddFlag("aAaAa");
Assert.IsFalse(args.HasValue("aAaAa"));
Assert.AreEqual(3, args.Count);
Assert.IsTrue(args.HasFlag("my_test_flag_1"));
Assert.IsTrue(args.HasFlag("my_test_flag_2"));
Assert.IsTrue(args.HasFlag("aaaaa"));
Assert.IsTrue(args.HasFlag("AAAAA"));
Assert.AreEqual("my_test_flag_1 my_test_flag_2 aaaaa", args.ToString());
args.RemoveFlag("Aaaaa");
Assert.AreEqual(2, args.Count);
Assert.IsTrue(args.HasFlag("my_test_flag_1"));
Assert.IsTrue(args.HasFlag("my_test_flag_2"));
Assert.IsFalse(args.HasFlag("aaaaa"));
Assert.AreEqual("my_test_flag_1 my_test_flag_2", args.ToString());
}
[TestMethod]
public void TestValues(){
CommandLineArgs args = new CommandLineArgs();
args.SetValue("test_value", "My Test Value");
args.SetValue("aAaAa", "aaaaa");
Assert.IsFalse(args.HasFlag("aAaAa"));
Assert.AreEqual(2, args.Count);
Assert.IsTrue(args.HasValue("test_value"));
Assert.IsTrue(args.HasValue("aaaaa"));
Assert.IsTrue(args.HasValue("AAAAA"));
Assert.AreEqual("My Test Value", args.GetValue("test_value", string.Empty));
Assert.AreEqual("aaaaa", args.GetValue("aaaaa", string.Empty));
Assert.AreEqual("test_value \"My Test Value\" aaaaa \"aaaaa\"", args.ToString());
args.RemoveValue("Aaaaa");
Assert.AreEqual(1, args.Count);
Assert.IsTrue(args.HasValue("test_value"));
Assert.IsFalse(args.HasValue("aaaaa"));
Assert.AreEqual("test_value \"My Test Value\"", args.ToString());
}
[TestMethod]
public void TestFlagAndValueMix(){
CommandLineArgs args = new CommandLineArgs();
args.AddFlag("my_test_flag_1");
args.AddFlag("my_test_flag_2");
args.AddFlag("aAaAa");
args.SetValue("test_value", "My Test Value");
args.SetValue("aAaAa", "aaaaa");
Assert.AreEqual(5, args.Count);
Assert.IsTrue(args.HasFlag("aaaaa"));
Assert.IsTrue(args.HasValue("aaaaa"));
Assert.AreEqual("my_test_flag_1 my_test_flag_2 aaaaa test_value \"My Test Value\" aaaaa \"aaaaa\"", args.ToString());
var dict = new Dictionary<string, string>();
args.ToDictionary(dict); // loses 'aaaaa' flag
Assert.AreEqual(4, dict.Count);
Assert.AreEqual("1", dict["my_test_flag_1"]);
Assert.AreEqual("1", dict["my_test_flag_2"]);
Assert.AreEqual("My Test Value", dict["test_value"]);
Assert.AreEqual("aaaaa", dict["aaaaa"]);
}
[TestMethod]
public void TestClone(){
CommandLineArgs args = new CommandLineArgs();
args.AddFlag("my_test_flag_1");
args.AddFlag("my_test_flag_2");
args.AddFlag("aAaAa");
args.SetValue("test_value", "My Test Value");
args.SetValue("aAaAa", "aaaaa");
CommandLineArgs clone = args.Clone();
args.RemoveFlag("aaaaa");
args.RemoveValue("aaaaa");
clone.RemoveFlag("my_test_flag_1");
clone.RemoveFlag("my_test_flag_2");
clone.RemoveValue("test_value");
Assert.AreEqual(3, args.Count);
Assert.AreEqual(2, clone.Count);
Assert.AreEqual("my_test_flag_1 my_test_flag_2 test_value \"My Test Value\"", args.ToString());
Assert.AreEqual("aaaaa aaaaa \"aaaaa\"", clone.ToString());
}
[TestMethod]
public void TestEmptyStringArray(){
CommandLineArgs args;
args = CommandLineArgs.FromStringArray('-', new string[0]);
Assert.AreEqual(0, args.Count);
args = CommandLineArgs.FromStringArray('-', new string[]{ "", "+fail", "@nope" });
Assert.AreEqual(0, args.Count);
}
[TestMethod]
public void TestValidStringArray(){
CommandLineArgs args;
args = CommandLineArgs.FromStringArray('-', new string[]{ "-flag1", "-flag2", "-FLAG3" });
Assert.AreEqual(3, args.Count);
Assert.IsTrue(args.HasFlag("-flag1"));
Assert.IsTrue(args.HasFlag("-flag2"));
Assert.IsTrue(args.HasFlag("-flag3"));
args = CommandLineArgs.FromStringArray('-', new string[]{ "-flag", "-value", "Here is some text!" });
Assert.AreEqual(2, args.Count);
Assert.IsTrue(args.HasFlag("-flag"));
Assert.IsTrue(args.HasValue("-value"));
Assert.AreEqual("Here is some text!", args.GetValue("-value", string.Empty));
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDck.Core.Utils;
namespace UnitTests.Core.Utils{
[TestClass]
public class TestCommandLineArgsParser{
[TestMethod]
public void TestEmptyString(){
Assert.AreEqual(0, CommandLineArgsParser.ReadCefArguments("").Count);
Assert.AreEqual(0, CommandLineArgsParser.ReadCefArguments(" ").Count);
}
[TestMethod]
public void TestValidString(){
CommandLineArgs args = CommandLineArgsParser.ReadCefArguments("--aaa --bbb --first-value=123 --SECOND-VALUE=\"a b c d e\" --ccc");
// cef has no flags, flag arguments have a value of 1
// the processing removes all dashes in front of each key
Assert.AreEqual(5, args.Count);
Assert.IsTrue(args.HasValue("aaa"));
Assert.IsTrue(args.HasValue("bbb"));
Assert.IsTrue(args.HasValue("ccc"));
Assert.IsTrue(args.HasValue("first-value"));
Assert.IsTrue(args.HasValue("second-value"));
Assert.AreEqual("1", args.GetValue("aaa", string.Empty));
Assert.AreEqual("1", args.GetValue("bbb", string.Empty));
Assert.AreEqual("1", args.GetValue("ccc", string.Empty));
Assert.AreEqual("123", args.GetValue("first-value", string.Empty));
Assert.AreEqual("a b c d e", args.GetValue("second-value", string.Empty));
}
}
}

View 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));
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("UnitTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("UnitTests")]
[assembly: AssemblyCopyright("")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("99036b78-aad6-4a76-8bf3-40c77eca2464")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

65
tests/TestUtils.cs Normal file
View File

@@ -0,0 +1,65 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests{
public static class TestUtils{
private static readonly HashSet<string> CreatedFiles = new HashSet<string>();
public static void WriteText(string file, string text){
DeleteFileOnExit(file);
File.WriteAllText(file, text, Encoding.UTF8);
}
public static void WriteLines(string file, IEnumerable<string> lines){
DeleteFileOnExit(file);
File.WriteAllLines(file, lines, Encoding.UTF8);
}
public static FileStream WriteFile(string file){
DeleteFileOnExit(file);
return new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
}
public static string ReadText(string file){
try{
return File.ReadAllText(file, Encoding.UTF8);
}catch(Exception){
return string.Empty;
}
}
public static IEnumerable<string> ReadLines(string file){
try{
return File.ReadLines(file, Encoding.UTF8);
}catch(Exception){
return Enumerable.Empty<string>();
}
}
public static FileStream ReadFile(string file){
return new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None);
}
public static void DeleteFileOnExit(string file){
CreatedFiles.Add(file);
}
[TestClass]
public static class Cleanup{
[AssemblyCleanup]
public static void DeleteFilesOnExit(){
foreach(string file in CreatedFiles){
try{
File.Delete(file);
}catch(Exception){
// ignore
}
}
}
}
}
}

91
tests/UnitTests.csproj Normal file
View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A958FA7A-4A2C-42A7-BFA0-159343483F4E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>UnitTests</RootNamespace>
<AssemblyName>UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Debug\</OutputPath>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
</ItemGroup>
<Choose>
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" />
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="Core\Settings\TestCombinedFileStream.cs" />
<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>
<ItemGroup>
<ProjectReference Include="..\TweetDck.csproj">
<Project>{2389a7cd-e0d3-4706-8294-092929a33a2d}</Project>
<Name>TweetDck</Name>
</ProjectReference>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
</ItemGroup>
</When>
</Choose>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.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">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>