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

Compare commits

...

51 Commits
1.6.2 ... 1.6.4

Author SHA1 Message Date
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
46 changed files with 2838 additions and 335 deletions

View File

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

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,48 +21,6 @@ namespace TweetDck.Core.Bridge{
private readonly FormBrowser form;
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){
this.form = form;
this.notification = notification;
@@ -81,26 +39,26 @@ namespace TweetDck.Core.Bridge{
}
public void SetLastRightClickedLink(string link){
form.InvokeSafe(() => LastRightClickedLink = link);
form.InvokeAsyncSafe(() => LastRightClickedLink = link);
}
public void SetLastHighlightedTweet(string link, string quotedLink){
form.InvokeSafe(() => {
form.InvokeAsyncSafe(() => {
LastHighlightedTweet = link;
LastHighlightedQuotedTweet = quotedLink;
});
}
public void SetNotificationQuotedTweet(string link){
notification.InvokeSafe(() => notification.CurrentQuotedTweetUrl = link);
notification.InvokeAsyncSafe(() => notification.CurrentQuotedTweetUrl = link);
}
public void OpenSettingsMenu(){
form.InvokeSafe(form.OpenSettings);
form.InvokeAsyncSafe(form.OpenSettings);
}
public void OpenPluginsMenu(){
form.InvokeSafe(form.OpenPlugins);
form.InvokeAsyncSafe(form.OpenPlugins);
}
public void OnTweetPopup(string tweetHtml, string tweetUrl, int tweetCharacters){
@@ -111,7 +69,7 @@ namespace TweetDck.Core.Bridge{
}
public void OnTweetSound(){
form.InvokeSafe(() => {
form.InvokeAsyncSafe(() => {
form.OnTweetNotification();
form.PlayNotificationSound();
});
@@ -119,15 +77,15 @@ namespace TweetDck.Core.Bridge{
public void DisplayTooltip(string text, bool showInNotification){
if (showInNotification){
notification.InvokeSafe(() => notification.DisplayTooltip(text));
notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
}
else{
form.InvokeSafe(() => form.DisplayTooltip(text));
form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
}
}
public void LoadNextNotification(){
notification.InvokeSafe(notification.FinishCurrentTweet);
notification.InvokeAsyncSafe(notification.FinishCurrentTweet);
}
public void TryPasteImage(){
@@ -163,11 +121,11 @@ namespace TweetDck.Core.Bridge{
}
public void ScreenshotTweet(string html, int width, int height){
form.InvokeSafe(() => form.OnTweetScreenshotReady(html, width, height));
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
}
public void FixClipboard(){
form.InvokeSafe(WindowsUtils.ClipboardStripHtmlStyles);
form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
}
public void OpenBrowser(string url){

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){
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,10 @@ using TweetDck.Updates;
using TweetDck.Plugins;
using TweetDck.Plugins.Enums;
using TweetDck.Plugins.Events;
using System.Media;
using TweetDck.Core.Bridge;
using TweetDck.Core.Notification;
using TweetDck.Core.Notification.Screenshot;
using TweetDck.Updates.Events;
namespace TweetDck.Core{
sealed partial class FormBrowser : Form{
@@ -41,7 +41,7 @@ namespace TweetDck.Core{
private FormWindowState prevState;
private TweetScreenshotManager notificationScreenshotManager;
private SoundPlayer notificationSound;
private SoundNotification soundNotification;
public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
InitializeComponent();
@@ -53,7 +53,11 @@ namespace TweetDck.Core{
this.plugins.PluginChangedState += plugins_PluginChangedState;
this.notification = CreateNotificationForm(NotificationFlags.AutoHide | NotificationFlags.TopMost);
#if DEBUG
this.notification.CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt;
#else
this.notification.CanMoveWindow = () => false;
#endif
this.notification.Show();
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
@@ -69,11 +73,13 @@ namespace TweetDck.Core{
this.browser.LoadingStateChanged += Browser_LoadingStateChanged;
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);
Controls.Add(browser);
Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
Disposed += (sender, args) => {
browser.Dispose();
@@ -81,8 +87,8 @@ namespace TweetDck.Core{
notificationScreenshotManager.Dispose();
}
if (notificationSound != null){
notificationSound.Dispose();
if (soundNotification != null){
soundNotification.Dispose();
}
};
@@ -92,13 +98,16 @@ namespace TweetDck.Core{
UpdateTrayIcon();
Config.MuteToggled += Config_MuteToggled;
this.updates = new UpdateHandler(browser, this, updaterSettings);
this.updates.UpdateAccepted += updates_UpdateAccepted;
this.updates.UpdateDismissed += updates_UpdateDismissed;
}
private void ShowChildForm(Form form){
form.VisibleChanged += (sender, args) => form.MoveToCenter(this);
form.Show(this);
form.Shown += (sender, args) => form.MoveToCenter(this);
}
public void ForceClose(){
@@ -135,7 +144,9 @@ namespace TweetDck.Core{
private void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && BrowserUtils.IsTweetDeckWebsite(e.Frame)){
UpdateProperties();
ScriptLoader.ExecuteFile(e.Frame, "code.js");
ReinjectCustomCSS(Config.CustomBrowserCSS);
#if DEBUG
ScriptLoader.ExecuteFile(e.Frame, "debug.js");
@@ -192,6 +203,10 @@ namespace TweetDck.Core{
}
}
private void Config_MuteToggled(object sender, EventArgs e){
UpdateProperties(PropertyBridge.Properties.MuteNotifications);
}
private void Config_TrayBehaviorChanged(object sender, EventArgs e){
if (!isLoaded)return;
@@ -241,6 +256,11 @@ namespace TweetDck.Core{
}
}
private void updates_UpdateDismissed(object sender, UpdateDismissedEventArgs e){
Config.DismissedUpdate = e.VersionTag;
Config.Save();
}
protected override void WndProc(ref Message m){
if (isLoaded && m.Msg == Program.WindowRestoreMessage){
trayIcon_ClickRestore(trayIcon, new EventArgs());
@@ -269,29 +289,47 @@ namespace TweetDck.Core{
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
public void OpenSettings(){
OpenSettings(0);
}
public void OpenSettings(int tabIndex){
if (currentFormSettings != null){
currentFormSettings.BringToFront();
}
else{
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
currentFormSettings = new FormSettings(this, plugins, updates);
currentFormSettings = new FormSettings(this, plugins, updates, tabIndex);
currentFormSettings.FormClosed += (sender, args) => {
currentFormSettings = null;
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
updates.Settings.DismissedUpdate = string.Empty;
Config.DismissedUpdate = string.Empty;
Config.Save();
updates.Check(false);
}
if (!Config.EnableTrayHighlight){
trayIcon.HasNotifications = false;
}
UpdateProperties(PropertyBridge.Properties.ExpandLinksOnHover | PropertyBridge.Properties.HasCustomNotificationSound);
};
ShowChildForm(currentFormSettings);
@@ -327,19 +365,15 @@ namespace TweetDck.Core{
}
public void PlayNotificationSound(){
if (string.IsNullOrEmpty(Config.NotificationSoundPath)){
if (Config.NotificationSoundPath.Length == 0){
return;
}
if (notificationSound == null){
notificationSound = new SoundPlayer();
if (soundNotification == null){
soundNotification = new SoundNotification(this);
}
if (notificationSound.SoundLocation != Config.NotificationSoundPath){
notificationSound.SoundLocation = Config.NotificationSoundPath;
}
notificationSound.Play();
soundNotification.Play(Config.NotificationSoundPath);
}
public void OnTweetScreenshotReady(string html, int width, int height){

View File

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

View File

@@ -101,7 +101,7 @@ namespace TweetDck.Core.Handling{
}
protected void SetClipboardText(string text){
form.InvokeSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
form.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
}
protected static void RemoveSeparatorIfLast(IMenuModel model){

View File

@@ -33,6 +33,10 @@ namespace TweetDck.Core.Handling{
model.Remove(CefMenuCommand.ViewSource);
RemoveSeparatorIfLast(model);
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
lastHighlightedTweet = TweetDeckBridge.LastHighlightedTweet;
@@ -76,6 +80,8 @@ namespace TweetDck.Core.Handling{
AddDebugMenuItems(globalMenu);
#endif
}
RemoveSeparatorIfLast(model);
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
@@ -89,19 +95,19 @@ namespace TweetDck.Core.Handling{
return true;
case MenuSettings:
form.InvokeSafe(form.OpenSettings);
form.InvokeAsyncSafe(form.OpenSettings);
return true;
case MenuAbout:
form.InvokeSafe(form.OpenAbout);
form.InvokeAsyncSafe(form.OpenAbout);
return true;
case MenuPlugins:
form.InvokeSafe(form.OpenPlugins);
form.InvokeAsyncSafe(form.OpenPlugins);
return true;
case MenuMute:
form.InvokeSafe(() => {
form.InvokeAsyncSafe(() => {
Program.UserConfig.MuteNotifications = !Program.UserConfig.MuteNotifications;
Program.UserConfig.Save();
});
@@ -117,7 +123,7 @@ namespace TweetDck.Core.Handling{
return true;
case MenuScreenshotTweet:
form.InvokeSafe(form.TriggerTweetScreenshot);
form.InvokeAsyncSafe(form.TriggerTweetScreenshot);
return true;
case MenuOpenQuotedTweetUrl:

View File

@@ -1,6 +1,5 @@
using CefSharp;
using TweetDck.Core.Controls;
using TweetDck.Core.Utils;
namespace TweetDck.Core.Handling{
class ContextMenuNotification : ContextMenuBase{
@@ -19,6 +18,12 @@ namespace TweetDck.Core.Handling{
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
model.Clear();
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
model.AddItem(CefMenuCommand.Copy, "Copy");
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (enableCustomMenu){
@@ -44,7 +49,7 @@ namespace TweetDck.Core.Handling{
RemoveSeparatorIfLast(model);
form.InvokeSafe(() => form.ContextMenuOpen = true);
form.InvokeAsyncSafe(() => form.ContextMenuOpen = true);
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
@@ -54,11 +59,11 @@ namespace TweetDck.Core.Handling{
switch((int)commandId){
case MenuSkipTweet:
form.InvokeSafe(form.FinishCurrentTweet);
form.InvokeAsyncSafe(form.FinishCurrentTweet);
return true;
case MenuFreeze:
form.InvokeSafe(() => form.FreezeTimer = !form.FreezeTimer);
form.InvokeAsyncSafe(() => form.FreezeTimer = !form.FreezeTimer);
return true;
case MenuCopyTweetUrl:
@@ -75,7 +80,7 @@ namespace TweetDck.Core.Handling{
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
base.OnContextMenuDismissed(browserControl, browser, frame);
form.InvokeSafe(() => form.ContextMenuOpen = false);
form.InvokeAsyncSafe(() => form.ContextMenuOpen = false);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace TweetDck.Core.Notification.Screenshot{
browser.FrameLoadEnd += (sender, args) => {
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{
sealed partial class FormSettings : Form{
public const int TabIndexNotification = 1;
private readonly FormBrowser browser;
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();
Text = Program.BrandName+" Settings";
@@ -22,10 +25,12 @@ namespace TweetDck.Core.Other{
this.tabPanel.SetupTabPanel(100);
this.tabPanel.AddButton("General", SelectTab<TabSettingsGeneral>);
this.tabPanel.AddButton("Notifications", () => SelectTab(() => new TabSettingsNotifications(browser.CreateNotificationForm(NotificationFlags.DisableContextMenu))));
this.tabPanel.AddButton("Notifications", () => SelectTab(() => new TabSettingsNotifications(browser.CreateNotificationForm(NotificationFlags.DisableContextMenu), !hasFinishedLoading)));
this.tabPanel.AddButton("Updates", () => SelectTab(() => new TabSettingsUpdates(updates)));
this.tabPanel.AddButton("Advanced", () => SelectTab(() => new TabSettingsAdvanced(browser.ReloadBrowser, plugins)));
this.tabPanel.SelectTab(tabPanel.Buttons.First());
this.tabPanel.AddButton("Advanced", () => SelectTab(() => new TabSettingsAdvanced(browser.ReinjectCustomCSS, plugins)));
this.tabPanel.SelectTab(tabPanel.Buttons.ElementAt(startTabIndex));
hasFinishedLoading = true;
}
private void SelectTab<T>() where T : BaseTabSettings, new(){

View File

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

View File

@@ -17,11 +17,15 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
}
}
public DialogSettingsCSS(){
private readonly Action<string> reinjectBrowserCSS;
public DialogSettingsCSS(Action<string> reinjectBrowserCSS){
InitializeComponent();
Text = Program.BrandName+" Settings - CSS";
this.reinjectBrowserCSS = reinjectBrowserCSS;
textBoxBrowserCSS.EnableMultilineShortcuts();
textBoxBrowserCSS.Text = Program.UserConfig.CustomBrowserCSS ?? "";
@@ -29,6 +33,16 @@ namespace TweetDck.Core.Other.Settings.Dialogs{
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){
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki");
}

View File

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

View File

@@ -9,13 +9,13 @@ using TweetDck.Plugins;
namespace TweetDck.Core.Other.Settings{
partial class TabSettingsAdvanced : BaseTabSettings{
private readonly Action browserReloadAction;
private readonly Action<string> reinjectBrowserCSS;
private readonly PluginManager plugins;
public TabSettingsAdvanced(Action browserReloadAction, PluginManager plugins){
public TabSettingsAdvanced(Action<string> reinjectBrowserCSS, PluginManager plugins){
InitializeComponent();
this.browserReloadAction = browserReloadAction;
this.reinjectBrowserCSS = reinjectBrowserCSS;
this.plugins = plugins;
checkHardwareAcceleration.Checked = HardwareAcceleration.IsEnabled;
@@ -80,17 +80,13 @@ namespace TweetDck.Core.Other.Settings{
}
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){
bool hasChangedBrowser = form.BrowserCSS != Config.CustomBrowserCSS;
Config.CustomBrowserCSS = form.BrowserCSS;
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.Size = new System.Drawing.Size(170, 20);
this.tbCustomSound.TabIndex = 0;
this.tbCustomSound.TextChanged += new System.EventHandler(this.tbCustomSound_TextChanged);
//
// TabSettingsNotifications
//

View File

@@ -1,6 +1,7 @@
using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Windows.Forms;
using TweetDck.Core.Controls;
using TweetDck.Core.Notification;
@@ -11,7 +12,7 @@ namespace TweetDck.Core.Other.Settings{
private readonly FormNotification notification;
private readonly Point initCursorPosition;
public TabSettingsNotifications(FormNotification notification){
public TabSettingsNotifications(FormNotification notification, bool ignoreAutoClick){
InitializeComponent();
this.notification = notification;
@@ -30,7 +31,7 @@ namespace TweetDck.Core.Other.Settings{
this.notification.Activated += notification_Activated;
this.notification.Show(this);
initCursorPosition = Cursor.Position;
initCursorPosition = ignoreAutoClick ? ControlExtensions.InvisibleLocation : Cursor.Position;
switch(Config.NotificationPosition){
case TweetNotification.Position.TopLeft: radioLocTL.Checked = true; break;
@@ -58,7 +59,7 @@ namespace TweetDck.Core.Other.Settings{
trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance);
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value.ToString(CultureInfo.InvariantCulture)+" px";
tbCustomSound.Text = Config.NotificationSoundPath ?? string.Empty;
tbCustomSound.Text = Config.NotificationSoundPath;
Disposed += (sender, args) => this.notification.Dispose();
}
@@ -77,7 +78,7 @@ namespace TweetDck.Core.Other.Settings{
}
private void notification_Activated(object sender, EventArgs e){
if (Cursor.Position == initCursorPosition){
if (Cursor.Position == initCursorPosition && initCursorPosition != ControlExtensions.InvisibleLocation){
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
@@ -167,6 +168,13 @@ namespace TweetDck.Core.Other.Settings{
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){
using(OpenFileDialog dialog = new OpenFileDialog{
AutoUpgradeEnabled = true,

View File

@@ -1,6 +1,7 @@
using System;
using System.Windows.Forms;
using TweetDck.Updates;
using TweetDck.Updates.Events;
namespace TweetDck.Core.Other.Settings{
partial class TabSettingsUpdates : BaseTabSettings{
@@ -27,12 +28,18 @@ namespace TweetDck.Core.Other.Settings{
private void btnCheckUpdates_Click(object sender, EventArgs e){
if (!Ready)return;
Config.DismissedUpdate = string.Empty;
Config.Save();
updateCheckEventId = updates.Check(true);
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){

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

View File

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

View File

@@ -21,8 +21,8 @@ namespace TweetDck{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.6.2";
public const string VersionFull = "1.6.2.0";
public const string VersionTag = "1.6.4";
public const string VersionFull = "1.6.4.0";
public static readonly Version Version = new Version(VersionTag);
@@ -126,6 +126,8 @@ namespace TweetDck{
BrowserCache.ClearOldCacheFiles();
CefSharpSettings.WcfEnabled = false;
CefSettings settings = new CefSettings{
AcceptLanguageList = BrowserUtils.HeaderAcceptLanguage,
UserAgent = BrowserUtils.HeaderUserAgent,
@@ -155,7 +157,8 @@ namespace TweetDck{
plugins.Reload();
FormBrowser mainForm = new FormBrowser(plugins, new UpdaterSettings{
AllowPreReleases = Args.HasFlag("-debugupdates")
AllowPreReleases = Args.HasFlag("-debugupdates"),
DismissedUpdate = UserConfig.DismissedUpdate
});
Application.Run(mainForm);

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

View File

@@ -1,20 +1,36 @@
enabled(){
// add a stylesheet to change tweet actions
this.css = window.TDPF_createCustomStyle(this);
this.css.insert(".tweet-actions { float: right !important; width: auto !important; }");
this.prevFooterMustache = TD.mustaches["status/tweet_single_footer.mustache"];
// load configuration
window.TDPF_loadConfigurationFile(this, "configuration.js", "configuration.default.js", config => {
if (config.hideTweetActions){
this.css.insert(".tweet-action { opacity: 0; }");
this.css.insert(".is-favorite .tweet-action, .is-retweet .tweet-action { opacity: 0.5; visibility: visible !important; }");
this.css.insert(".tweet:hover .tweet-action, .is-favorite .tweet-action[rel='favorite'], .is-retweet .tweet-action[rel='retweet'] { opacity: 1; visibility: visible !important; }");
}
if (config.moveTweetActionsToRight){
this.css.insert(".tweet-actions { float: right !important; width: auto !important; }");
this.css.insert(".tweet-actions > li:nth-child(4) { margin-right: 2px !important; }");
}
// revert small links around the tweet
this.prevFooterMustache = TD.mustaches["status/tweet_single_footer.mustache"];
if (config.smallComposeTextSize){
this.css.insert(".compose-text { font-size: 12px !important; height: 120px !important; }");
}
var footerLayout = TD.mustaches["status/tweet_single_footer.mustache"];
footerLayout = footerLayout.replace('txt-mute txt-size--12', 'txt-mute txt-small');
footerLayout = footerLayout.replace('{{#inReplyToID}}', '{{^inReplyToID}} <a class="pull-left margin-txs txt-mute txt-small is-vishidden-narrow" href="#" rel="viewDetails">{{_i}}Details{{/i}}</a> <a class="pull-left margin-txs txt-mute txt-small is-vishidden is-visshown-narrow" href="#" rel="viewDetails">{{_i}}Open{{/i}}</a> {{/inReplyToID}} {{#inReplyToID}}');
footerLayout = footerLayout.replace('<span class="link-complex-target"> {{_i}}View Conversation{{/i}}', '<i class="icon icon-conversation icon-small-context"></i> <span class="link-complex-target"> <span class="is-vishidden-wide is-vishidden-narrow">{{_i}}View{{/i}}</span> <span class="is-vishidden is-visshown-wide">{{_i}}Conversation{{/i}}</span>');
TD.mustaches["status/tweet_single_footer.mustache"] = footerLayout;
if (config.revertConversationLinks){
var footer = TD.mustaches["status/tweet_single_footer.mustache"];
footer = footer.replace('txt-mute txt-size--12', 'txt-mute txt-small');
footer = footer.replace('{{#inReplyToID}}', '{{^inReplyToID}} <a class="pull-left margin-txs txt-mute txt-small is-vishidden-narrow" href="#" rel="viewDetails">{{_i}}Details{{/i}}</a> <a class="pull-left margin-txs txt-mute txt-small is-vishidden is-visshown-narrow" href="#" rel="viewDetails">{{_i}}Open{{/i}}</a> {{/inReplyToID}} {{#inReplyToID}}');
footer = footer.replace('<span class="link-complex-target"> {{_i}}View Conversation{{/i}}', '<i class="icon icon-conversation icon-small-context"></i> <span class="link-complex-target"> <span class="is-vishidden-wide is-vishidden-narrow">{{_i}}View{{/i}}</span> <span class="is-vishidden is-visshown-wide">{{_i}}Conversation{{/i}}</span>');
TD.mustaches["status/tweet_single_footer.mustache"] = footer;
}
if (config.moveTweetActionsToRight){
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
}
});
// fix layout for right-aligned actions menu
this.uiShowActionsMenuEvent = function(){
@@ -22,13 +38,9 @@ enabled(){
};
}
ready(){
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
}
disabled(){
this.css.remove();
TD.mustaches["status/tweet_single_footer.mustache"] = this.prevFooterMustache;
$(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent);
TD.mustaches["status/tweet_single_footer.mustache"] = this.prevFooterMustache;
}

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.
//
@@ -10,71 +10,14 @@
var highlightedTweetEle;
//
// Function: Initializes TweetD*ck events. Called after the website app is loaded.
// Variable: Array of functions called after the website app is loaded.
//
var initializeTweetDck = function(){
// Settings button hook
$("[data-action='settings-menu']").click(function(){
setTimeout(function(){
var menu = $(".js-dropdown-content").children("ul").first();
if (menu.length === 0)return;
var onAppReady = [];
menu.children(".drp-h-divider").last().after([
'<li class="is-selectable" data-std><a href="#" data-action="td-settings">'+$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();
}
};
//
// Variable: DOM object containing the main app element.
//
var app = $(document.body).children(".js-app");
//
// Function: Prepends code at the beginning of a function. If the prepended function returns true, execution of the original function is cancelled.
@@ -112,6 +55,7 @@
html.css("border", "0");
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") || "";
@@ -136,23 +80,6 @@
return tags.join("");
};
//
// Block: Observe the app <div> element and initialize TweetD*ck whenever possible.
//
var app = $("body").children(".js-app");
new MutationObserver(function(){
if (window.TD_APP_READY && app.hasClass("is-hidden")){
window.TD_APP_READY = false;
}
else if (!window.TD_APP_READY && !app.hasClass("is-hidden")){
initializeTweetDck();
}
}).observe(app[0], {
attributes: true,
attributeFilter: [ "class" ]
});
//
// Block: Hook into settings object to detect when the settings change.
//
@@ -167,7 +94,7 @@
});
//
// Block: Force popup notification settings.
// Block: Enable popup notifications.
//
TD.controller.notifications.hasNotifications = function(){
return true;
@@ -177,6 +104,49 @@
return true;
};
$.subscribe("/notifications/new", function(obj){
for(let index = obj.items.length-1; index >= 0; index--){
onNewTweet(obj.column, obj.items[index]);
}
});
//
// Block: Add TweetDuck buttons to the settings menu.
//
onAppReady.push(function(){
$("[data-action='settings-menu']").click(function(){
setTimeout(function(){
var menu = $(".js-dropdown-content").children("ul").first();
if (menu.length === 0)return;
menu.children(".drp-h-divider").last().after([
'<li class="is-selectable" data-std><a href="#" data-action="td-settings">TweetDuck settings</a></li>',
'<li class="is-selectable" data-std><a href="#" data-action="td-plugins">TweetDuck plugins</a></li>',
'<li class="drp-h-divider"></li>'
].join(""));
var buttons = menu.children("[data-std]");
buttons.on("click", "a", function(){
var action = $(this).attr("data-action");
if (action === "td-settings"){
$TD.openSettingsMenu();
}
else if (action === "td-plugins"){
$TD.openPluginsMenu();
}
});
buttons.hover(function(){
$(this).addClass("is-selected");
}, function(){
$(this).removeClass("is-selected");
});
}, 0);
});
});
//
// Block: Expand shortened links on hover or display tooltip.
//
@@ -198,7 +168,7 @@
return;
}
if ($TD.expandLinksOnHover){
if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){
var expanded = me.attr("data-full-url");
expanded = cutStart(expanded, "https://");
@@ -217,7 +187,7 @@
}
}
else if (e.type === "mouseleave"){
if ($TD.expandLinksOnHover){
if ($TDX.expandLinksOnHover){
var prevText = me.attr("td-prev-text");
if (prevText){
@@ -256,7 +226,7 @@
var soundEle = document.getElementById("update-sound");
soundEle.play = prependToFunction(soundEle.play, function(){
return $TD.muteNotifications || $TD.hasCustomNotificationSound;
return $TDX.muteNotifications || $TDX.hasCustomNotificationSound;
});
})();
@@ -386,7 +356,7 @@
$TD.clickUploadImage(Math.floor(buttonPos.left), Math.floor(buttonPos.top));
};
$(".js-app").delegate(".js-compose-text,.js-reply-tweetbox", "paste", function(){
app.delegate(".js-compose-text,.js-reply-tweetbox", "paste", function(){
lastPasteElement = $(this);
$TD.tryPasteImage();
});
@@ -524,6 +494,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.
//
@@ -543,12 +532,79 @@
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
if ($TD.hasCustomBrowserCSS){
var styleCustom = document.createElement("style");
styleCustom.innerHTML = $TD.customBrowserCSS;
document.head.appendChild(styleCustom);
}
styleOfficial.sheet.insertRule(".is-video a:not([href*='youtu']), .is-gif .js-media-gif-container { cursor: alias; }", 0); // change cursor on unsupported videos
styleOfficial.sheet.insertRule(".is-video a:not([href*='youtu']) .icon-bg-dot, .is-gif .icon-bg-dot { color: #bd3d37; }", 0); // change play icon color on unsupported videos
TD.services.TwitterActionRetweetedRetweet.prototype.iconClass = "icon-retweet icon-retweet-color txt-base-medium"; // fix retweet icon mismatch
window.TDGF_reinjectCustomCSS = function(styles){
$("#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,11 +1,27 @@
(function($, $TD, TD){
(function($, $TD, $TDX, TD){
var isDebugging = false;
$(document).keydown(function(e){
// ==============================
// F4 key - simulate notification
// ==============================
// ==========================
// F4 key - toggle debug mode
// ==========================
if (e.keyCode === 115){
isDebugging = !isDebugging;
$(".app-title").first().css("background-color", isDebugging ? "#5A6B75" : "#292F33");
}
// Debug mode handling
else if (isDebugging){
e.preventDefault();
// ===================================
// N key - simulate popup notification
// ===================================
if (e.keyCode === 78){
var col = TD.controller.columnManager.getAllOrdered()[0];
$.publish("/notifications/new",[{
@@ -15,5 +31,19 @@
]
}]);
}
// ===================================
// 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.
//
@@ -51,7 +51,7 @@
return;
}
if ($TD.expandLinksOnHover){
if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){
var expanded = url;
expanded = cutStart(expanded, "https://");
@@ -73,7 +73,7 @@
addEventListener(links, "mouseleave", function(e){
if (!e.currentTarget.hasAttribute("data-full-url"))return;
if ($TD.expandLinksOnHover){
if ($TDX.expandLinksOnHover){
var prevText = e.currentTarget.getAttribute("td-prev-text");
if (prevText){
@@ -146,4 +146,4 @@
document.body.addEventListener("mouseleave", function(){
document.body.classList.remove("td-hover");
});
})($TD);
})($TD, $TDX);

View File

@@ -7,25 +7,25 @@
//
// 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.
//
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.
//
const updateCheckUrlAll = "https://api.github.com/repos/chylex/"+$TDU.brandName+"/releases";
const updateCheckUrlAll = "https://api.github.com/repos/chylex/TweetDuck/releases";
//
// 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 ele = $("#tweetdck-update");
var ele = $("#tweetduck-update");
var existed = ele.length > 0;
if (existed > 0){
@@ -33,7 +33,7 @@
}
var html = outdated ? [
"<div id='tweetdck-update'>",
"<div id='tweetduck-update'>",
"<p class='tdu-title'>Unsupported System</p>",
"<p class='tdu-info'>You will not receive updates.</p>",
"<div class='tdu-buttons'>",
@@ -42,8 +42,8 @@
"</div>",
"</div>"
] : [
"<div id='tweetdck-update'>",
"<p class='tdu-title'>"+$TDU.brandName+" Update</p>",
"<div id='tweetduck-update'>",
"<p class='tdu-title'>TweetDuck Update</p>",
"<p class='tdu-info'>Version "+version+" is now available.</p>",
"<div class='tdu-buttons'>",
"<button class='btn btn-positive tdu-btn-download'><span class='label'>Download</span></button>",
@@ -54,7 +54,7 @@
$(document.body).append(html.join(""));
ele = $("#tweetdck-update");
ele = $("#tweetduck-update");
var buttonDiv = ele.children("div.tdu-buttons").first();
@@ -128,33 +128,19 @@
//
// Function: Runs an update check and updates all DOM elements appropriately.
//
var runUpdateCheck = function(force, eventID){
if (!$TDU.isSystemSupported){
if ($TDU.dismissedVersionTag !== "unsupported"){
createUpdateNotificationElement("unsupported");
}
return;
}
var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){
clearTimeout(updateCheckTimeoutID);
updateCheckTimeoutID = setTimeout(runUpdateCheck, 1000*60*60); // 1 hour
if (!$TDU.updateCheckEnabled && !force){
return;
}
var allowPre = $TDU.allowPreReleases;
updateCheckTimeoutID = setTimeout($TDU.triggerUpdateCheck, 1000*60*60); // 1 hour
$.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){
var release = allowPre ? response[0] : response;
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){
var obj = release.assets.find(asset => asset.name === updateFileName) || release.assets[0];
createUpdateNotificationElement(tagName, obj.browser_download_url);
displayNotification(tagName, obj.browser_download_url);
}
if (eventID){ // ignore undefined and 0
@@ -166,6 +152,6 @@
//
// Block: Setup global functions.
//
window.TDUF_displayNotification = displayNotification;
window.TDUF_runUpdateCheck = runUpdateCheck;
runUpdateCheck();
})($, $TDU);

View File

@@ -71,6 +71,7 @@
<ItemGroup>
<Compile Include="Configuration\LockManager.cs" />
<Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Core\Bridge\PropertyBridge.cs" />
<Compile Include="Core\Controls\ControlExtensions.cs" />
<Compile Include="Core\Controls\FlatButton.cs">
<SubType>Component</SubType>
@@ -112,6 +113,7 @@
<Compile Include="Core\Handling\FileDialogHandler.cs" />
<Compile Include="Core\Handling\JavaScriptDialogHandler.cs" />
<Compile Include="Core\Handling\LifeSpanHandler.cs" />
<Compile Include="Core\Notification\SoundNotification.cs" />
<Compile Include="Core\Notification\TweetNotification.cs" />
<Compile Include="Core\Other\FormAbout.cs">
<SubType>Form</SubType>
@@ -190,6 +192,7 @@
</Compile>
<Compile Include="Core\Notification\NotificationFlags.cs" />
<Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Core\Utils\TwoKeyDictionary.cs" />
<Compile Include="Core\Utils\WindowState.cs" />
<Compile Include="Core\Utils\WindowsUtils.cs" />
<Compile Include="Core\Bridge\TweetDeckBridge.cs" />
@@ -222,6 +225,7 @@
<Compile Include="Plugins\PluginManager.cs" />
<Compile Include="Plugins\PluginScriptGenerator.cs" />
<Compile Include="Reporter.cs" />
<Compile Include="Updates\Events\UpdateDismissedEventArgs.cs" />
<Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType>
</Compile>
@@ -238,8 +242,8 @@
<Compile Include="Core\Utils\BrowserUtils.cs" />
<Compile Include="Core\Utils\HardwareAcceleration.cs" />
<Compile Include="Core\Utils\NativeMethods.cs" />
<Compile Include="Updates\UpdateAcceptedEventArgs.cs" />
<Compile Include="Updates\UpdateCheckEventArgs.cs" />
<Compile Include="Updates\Events\UpdateAcceptedEventArgs.cs" />
<Compile Include="Updates\Events\UpdateCheckEventArgs.cs" />
<Compile Include="Updates\UpdateHandler.cs" />
<Compile Include="Updates\UpdateInfo.cs" />
<Compile Include="Program.cs" />
@@ -349,7 +353,15 @@ mkdir "$(TargetDir)plugins\official"
mkdir "$(TargetDir)plugins\user"
xcopy "$(ProjectDir)Resources\Plugins\*" "$(TargetDir)plugins\official\" /E /Y
rmdir "$(ProjectDir)\bin\Debug"
rmdir "$(ProjectDir)\bin\Release"</PostBuildEvent>
rmdir "$(ProjectDir)\bin\Release"
rmdir "$(TargetDir)plugins\official\.debug" /S /Q
if $(ConfigurationName) == Debug (
rmdir "$(TargetDir)plugins\official\.debug" /S /Q
mkdir "$(TargetDir)plugins\user\.debug"
xcopy "$(ProjectDir)Resources\Plugins\.debug\*" "$(TargetDir)plugins\user\.debug\" /E /Y
)</PostBuildEvent>
</PropertyGroup>
<Import Project="packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets')" />
<Import Project="packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets')" />

View File

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

View File

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

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

@@ -9,7 +9,7 @@ using TweetDck.Core.Utils;
namespace TweetDck.Updates{
sealed partial class FormUpdateDownload : Form{
private const double BytesToMB = 1024.0*1024.0;
private const double BytesToKB = 1024.0;
public string InstallerPath{
get{
@@ -69,7 +69,7 @@ namespace TweetDck.Updates{
progressDownload.SetValueInstant(1000);
}
labelStatus.Text = (e.BytesReceived/BytesToMB).ToString("0.0", CultureInfo.CurrentCulture)+" MB";
labelStatus.Text = (e.BytesReceived/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" kB";
}
else{
if (progressDownload.Style != ProgressBarStyle.Continuous){
@@ -77,7 +77,7 @@ namespace TweetDck.Updates{
}
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 = (e.BytesReceived/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" / "+(e.TotalBytesToReceive/BytesToKB).ToString("0.0", CultureInfo.CurrentCulture)+" kB";
}
});
}

View File

@@ -5,14 +5,28 @@ using TweetDck.Core;
using TweetDck.Core.Controls;
using TweetDck.Core.Utils;
using TweetDck.Resources;
using TweetDck.Updates.Events;
namespace TweetDck.Updates{
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 FormBrowser form;
private readonly UpdaterSettings settings;
public event EventHandler<UpdateAcceptedEventArgs> UpdateAccepted;
public event EventHandler<UpdateDismissedEventArgs> UpdateDismissed;
public event EventHandler<UpdateCheckEventArgs> CheckFinished;
private int lastEventId;
@@ -23,26 +37,50 @@ namespace TweetDck.Updates{
this.settings = settings;
browser.FrameLoadEnd += browser_FrameLoadEnd;
browser.RegisterJsObject("$TDU", new Bridge(this));
browser.RegisterAsyncJsObject("$TDU", new Bridge(this));
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && BrowserUtils.IsTweetDeckWebsite(e.Frame)){
ScriptLoader.ExecuteFile(e.Frame, "update.js");
Check(false);
}
}
public int Check(bool force){
browser.ExecuteScriptAsync("TDUF_runUpdateCheck", force, ++lastEventId);
if (IsSystemSupported){
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){
if (UpdateAccepted != null){
form.InvokeSafe(() => UpdateAccepted(this, args));
}
}
private void TriggerUpdateDismissedEvent(UpdateDismissedEventArgs args){
form.InvokeSafe(() => {
settings.DismissedUpdate = args.VersionTag;
if (UpdateDismissed != null){
UpdateDismissed(this, args);
}
});
}
private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){
if (CheckFinished != null){
form.InvokeSafe(() => CheckFinished(this, args));
@@ -50,48 +88,16 @@ namespace TweetDck.Updates{
}
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 true; // Environment.OSVersion.Version >= new Version("6.1"); // 6.1 NT version = Windows 7
}
}
private readonly UpdateHandler owner;
public Bridge(UpdateHandler owner){
this.owner = owner;
}
public void TriggerUpdateCheck(){
owner.Check(false);
}
public void OnUpdateCheckFinished(int eventId, bool isUpdateAvailable, string latestVersion){
owner.TriggerCheckFinishedEvent(new UpdateCheckEventArgs(eventId, isUpdateAvailable, latestVersion));
}
@@ -101,10 +107,7 @@ namespace TweetDck.Updates{
}
public void OnUpdateDismissed(string versionTag){
owner.form.InvokeSafe(() => {
Program.UserConfig.DismissedUpdate = versionTag;
Program.UserConfig.Save();
});
owner.TriggerUpdateDismissedEvent(new UpdateDismissedEventArgs(versionTag));
}
public void OpenBrowser(string url){

View File

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

View File

@@ -10,14 +10,14 @@
[Setup]
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
AppName={#MyAppName}
AppName={#MyAppName} Portable
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\{#MyAppName}
DefaultDirName={sd}\{#MyAppName}
DefaultGroupName={#MyAppName}
OutputBaseFilename={#MyAppName}.Portable
VersionInfoVersion={#MyAppVersion}

View File

@@ -12,7 +12,7 @@
[Setup]
AppId={{{#MyAppID}}
AppName={#MyAppName}
AppName={#MyAppName} Update
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}

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

@@ -51,6 +51,7 @@
<Compile Include="Core\Utils\TestBrowserUtils.cs" />
<Compile Include="Core\Utils\TestCommandLineArgs.cs" />
<Compile Include="Core\Utils\TestCommandLineArgsParser.cs" />
<Compile Include="Core\Utils\TestTwoKeyDictionary.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestUtils.cs" />
</ItemGroup>