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

Compare commits

..

26 Commits
1.6.3 ... 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
26 changed files with 2543 additions and 195 deletions

View File

@@ -39,26 +39,26 @@ namespace TweetDck.Core.Bridge{
} }
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){
@@ -69,7 +69,7 @@ namespace TweetDck.Core.Bridge{
} }
public void OnTweetSound(){ public void OnTweetSound(){
form.InvokeSafe(() => { form.InvokeAsyncSafe(() => {
form.OnTweetNotification(); form.OnTweetNotification();
form.PlayNotificationSound(); form.PlayNotificationSound();
}); });
@@ -77,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(){
@@ -121,11 +121,11 @@ namespace TweetDck.Core.Bridge{
} }
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,12 +13,10 @@ 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 TweetDck.Updates.Events;
using System.IO;
namespace TweetDck.Core{ namespace TweetDck.Core{
sealed partial class FormBrowser : Form{ sealed partial class FormBrowser : Form{
@@ -43,8 +41,7 @@ namespace TweetDck.Core{
private FormWindowState prevState; private FormWindowState prevState;
private TweetScreenshotManager notificationScreenshotManager; private TweetScreenshotManager notificationScreenshotManager;
private SoundPlayer notificationSound; private SoundNotification soundNotification;
private bool ignoreNotificationSoundError;
public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){ public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
InitializeComponent(); InitializeComponent();
@@ -90,8 +87,8 @@ namespace TweetDck.Core{
notificationScreenshotManager.Dispose(); notificationScreenshotManager.Dispose();
} }
if (notificationSound != null){ if (soundNotification != null){
notificationSound.Dispose(); soundNotification.Dispose();
} }
}; };
@@ -305,13 +302,17 @@ namespace TweetDck.Core{
// 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;
@@ -368,45 +369,11 @@ namespace TweetDck.Core{
return; return;
} }
if (notificationSound == null){ if (soundNotification == null){
notificationSound = new SoundPlayer{ soundNotification = new SoundNotification(this);
LoadTimeout = 5000
};
} }
if (notificationSound.SoundLocation != Config.NotificationSoundPath){ soundNotification.Play(Config.NotificationSoundPath);
notificationSound.SoundLocation = Config.NotificationSoundPath;
ignoreNotificationSoundError = false;
}
try{
notificationSound.Play();
}catch(FileNotFoundException e){
OnNotificationSoundError("File not found: "+e.FileName);
}catch(InvalidOperationException){
OnNotificationSoundError("File is not a valid sound file.");
}catch(TimeoutException){
OnNotificationSoundError("File took too long to load.");
}
}
private void OnNotificationSoundError(string message){
if (!ignoreNotificationSoundError){
ignoreNotificationSoundError = true;
using(FormMessage form = new FormMessage("Notification Sound Error", "Could not play custom notification sound."+Environment.NewLine+message, MessageBoxIcon.Error)){
form.AddButton("Ignore");
Button btnOpenSettings = form.AddButton("Open Settings");
btnOpenSettings.Width += 16;
btnOpenSettings.Location = new Point(btnOpenSettings.Location.X-16, btnOpenSettings.Location.Y);
if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnOpenSettings){
OpenSettings();
currentFormSettings.SelectTab(FormSettings.TabIndexNotification);
}
}
}
} }
public void OnTweetScreenshotReady(string html, int width, int height){ public void OnTweetScreenshotReady(string html, int width, int height){

View File

@@ -101,7 +101,7 @@ namespace TweetDck.Core.Handling{
} }
protected void SetClipboardText(string text){ 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){ protected static void RemoveSeparatorIfLast(IMenuModel model){

View File

@@ -95,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();
}); });
@@ -123,7 +123,7 @@ namespace TweetDck.Core.Handling{
return true; return true;
case MenuScreenshotTweet: case MenuScreenshotTweet:
form.InvokeSafe(form.TriggerTweetScreenshot); form.InvokeAsyncSafe(form.TriggerTweetScreenshot);
return true; return true;
case MenuOpenQuotedTweetUrl: case MenuOpenQuotedTweetUrl:

View File

@@ -49,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){
@@ -59,11 +59,11 @@ 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:
@@ -80,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

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

@@ -13,10 +13,9 @@ namespace TweetDck.Core.Other{
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;
private bool wasTabSelectedAutomatically; public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, int startTabIndex = 0){
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates){
InitializeComponent(); InitializeComponent();
Text = Program.BrandName+" Settings"; Text = Program.BrandName+" Settings";
@@ -26,16 +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), wasTabSelectedAutomatically))); 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.ReinjectCustomCSS, plugins))); this.tabPanel.AddButton("Advanced", () => SelectTab(() => new TabSettingsAdvanced(browser.ReinjectCustomCSS, plugins)));
this.tabPanel.SelectTab(tabPanel.Buttons.First());
}
public void SelectTab(int index){ this.tabPanel.SelectTab(tabPanel.Buttons.ElementAt(startTabIndex));
wasTabSelectedAutomatically = true; hasFinishedLoading = true;
this.tabPanel.SelectTab(tabPanel.Buttons.ElementAt(index));
wasTabSelectedAutomatically = false;
} }
private void SelectTab<T>() where T : BaseTabSettings, new(){ private void SelectTab<T>() where T : BaseTabSettings, new(){

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

@@ -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;
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.3"; public const string VersionTag = "1.6.4";
public const string VersionFull = "1.6.3.0"; public const string VersionFull = "1.6.4.0";
public static readonly Version Version = new Version(VersionTag); public static readonly Version Version = new Version(VersionTag);

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,20 +1,36 @@
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(){
@@ -22,13 +38,9 @@ enabled(){
}; };
} }
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

@@ -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(){
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>', // Variable: DOM object containing the main app element.
'<li class="is-selectable" data-std><a href="#" data-action="td-plugins">TweetDuck plugins</a></li>', //
'<li class="drp-h-divider"></li>' var app = $(document.body).children(".js-app");
].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.
@@ -137,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.
// //
@@ -168,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;
@@ -178,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.
// //
@@ -387,7 +356,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();
}); });
@@ -525,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. // Block: Work around clipboard HTML formatting.
// //
@@ -544,6 +532,9 @@
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
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 TD.services.TwitterActionRetweetedRetweet.prototype.iconClass = "icon-retweet icon-retweet-color txt-base-medium"; // fix retweet icon mismatch
window.TDGF_reinjectCustomCSS = function(styles){ window.TDGF_reinjectCustomCSS = function(styles){
@@ -554,4 +545,66 @@
} }
}; };
})(); })();
//
// Block: Setup unsupported video element hook.
//
(function(){
var cancelModal = false;
TD.components.MediaGallery.prototype._loadTweet = appendToFunction(TD.components.MediaGallery.prototype._loadTweet, function(){
var media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service !== "youtube"){
$TD.openBrowser(this.clickedLink);
cancelModal = true;
}
});
TD.components.BaseModal.prototype.setAndShowContainer = prependToFunction(TD.components.BaseModal.prototype.setAndShowContainer, function(){
if (cancelModal){
cancelModal = false;
return true;
}
});
TD.ui.Column.prototype.playGifIfNotManuallyPaused = function(){};
TD.mustaches["status/media_thumb.mustache"] = TD.mustaches["status/media_thumb.mustache"].replace("is-gif", "is-gif is-paused");
app.delegate(".js-gif-play", "click", function(e){
var parent = $(e.target).closest(".js-tweet").first();
var link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
$TD.openBrowser(link.attr("href"));
e.stopPropagation();
});
})();
//
// Block: Finish initialization and load plugins.
//
onAppReady.push(function(){
$TD.loadFontSizeClass(TD.settings.getFontSize());
$TD.loadNotificationHeadContents(getNotificationHeadContents());
if (window.TD_PLUGINS){
window.TD_PLUGINS.onReady();
}
});
//
// Block: Observe the main app element and call the ready event whenever the contents are loaded.
//
new MutationObserver(function(){
if (window.TD_APP_READY && app.hasClass("is-hidden")){
window.TD_APP_READY = false;
}
else if (!window.TD_APP_READY && !app.hasClass("is-hidden")){
onAppReady.forEach(func => func());
window.TD_APP_READY = true;
}
}).observe(app[0], {
attributes: true,
attributeFilter: [ "class" ]
});
})($, $TD, $TDX, TD); })($, $TD, $TDX, TD);

View File

@@ -113,6 +113,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>
@@ -191,6 +192,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" />
@@ -351,7 +353,15 @@ 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.2785.1486\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2785.1486\build\cef.redist.x86.targets')" />
<Import Project="packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2785.1486\build\cef.redist.x64.targets')" /> <Import Project="packages\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

@@ -17,7 +17,7 @@ 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}

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\TestBrowserUtils.cs" />
<Compile Include="Core\Utils\TestCommandLineArgs.cs" /> <Compile Include="Core\Utils\TestCommandLineArgs.cs" />
<Compile Include="Core\Utils\TestCommandLineArgsParser.cs" /> <Compile Include="Core\Utils\TestCommandLineArgsParser.cs" />
<Compile Include="Core\Utils\TestTwoKeyDictionary.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestUtils.cs" /> <Compile Include="TestUtils.cs" />
</ItemGroup> </ItemGroup>