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

Compare commits

...

101 Commits
1.8.4 ... 1.8.7

Author SHA1 Message Date
68dca6e3d9 Fix spacebar not toggling video pause when the main app was focused 2017-08-21 14:14:38 +02:00
017f883e0b Disable custom emoji input, fix selection handling and support twemoji font if installed 2017-08-21 13:37:21 +02:00
77b5c95f75 Add basic js minification (trim whitespace and remove single line comments) 2017-08-21 09:41:15 +02:00
9d052c8339 Update close button fix to only affect New Tweet drawer 2017-08-21 02:17:48 +02:00
d67623a657 Tweak follow notification padding in the browser 2017-08-21 01:52:19 +02:00
c740b3dd46 What the fuck are you doing Twitter 2017-08-21 01:35:53 +02:00
2ef5f7f96f Fix border radius on media previews in tweet detail 2017-08-16 18:27:44 +02:00
404568d795 Fix pre-build powershell command causing build error 2017-08-16 18:23:38 +02:00
b5a6337a0c Update custom CSS to work better with recent TweetDeck changes 2017-08-14 17:15:18 +02:00
82170c3fbd Fix sensitive media in notification previews and tweak follow notification padding 2017-08-14 16:12:34 +02:00
e6d6275fcc Work on emoji keyboard contenteditable fixes (selection, focus, editor migration) 2017-08-14 15:37:55 +02:00
97c865a127 Make emoji editor only show after adding emoji, fix minor UI issues 2017-08-14 04:22:13 +02:00
1ff21f0ee0 Make emoji keyboard replace tweet input with one that displays emoji
Closes #146
2017-08-14 00:47:08 +02:00
2a3dca4467 Rewrite video player to use duplex pipe for process communication 2017-08-13 17:52:46 +02:00
d4ecfcceec Tweak DuplexPipe to set key instead of data when separator is missing 2017-08-13 17:31:58 +02:00
ec5d503e4d Make DuplexPipe data serialized as key/value pairs 2017-08-13 17:23:23 +02:00
346391ca2d Remove unused 'using' statements for the billionth time 2017-08-13 16:55:08 +02:00
9074cdf340 Add a hover effect to video player seek bar 2017-08-13 16:46:33 +02:00
2fcf3604a8 Move video player form controls to a different namespace 2017-08-13 16:14:46 +02:00
34e5185fa1 Fuck localized .NET exceptions 2017-08-13 15:53:39 +02:00
e09e0e69ca Fuck browser process when building the project 2017-08-13 15:50:43 +02:00
963c98e588 Move interprocess comms to a separate project & implement duplex pipe 2017-08-13 15:20:04 +02:00
92acb823a4 Implement a duplex anonymous pipe in TweetLib.Communication 2017-08-13 15:14:17 +02:00
b967b1288f Move process communication to a separate project 2017-08-13 13:54:34 +02:00
1db271ce90 Fix spacebar triggering fullscreen in video player 2017-08-13 00:23:08 +02:00
58c64025e3 Fix level 2 lists and links in update changelog modal 2017-08-12 23:52:38 +02:00
643a7a87aa Release 1.8.6 2017-08-12 23:39:41 +02:00
5e9ed5d713 Improve video player startup and ensure it's always closed with the main app 2017-08-12 23:36:14 +02:00
78e492c764 Tweak 'stay open' pin position and tooltip 2017-08-12 20:33:52 +02:00
59c2a3642b Bump version of subprocess exe (should have been done a long time ago) 2017-08-12 19:10:38 +02:00
40ca923745 Cleanup FormPlayer code and set sync timer interval to 15 instead of 10 2017-08-12 19:08:17 +02:00
03af6cecaa Replace 'Stay open' checkbox with a pin icon
Closes #154
2017-08-12 17:49:27 +02:00
3992e447f4 Change tooltip border radius to be almost square 2017-08-12 17:47:55 +02:00
14a9edeb73 Fix various focus issues with video player and fix double-clicking control panel 2017-08-12 15:12:54 +02:00
92f1e9f7ec Make video player progress bar seek on mouse down instead of up 2017-08-12 14:31:46 +02:00
19c294c53e Terminate video player when pressing back mouse button over it 2017-08-12 13:43:53 +02:00
fe88ea5c05 Fix ctrl key not opening gifs externally 2017-08-12 03:37:24 +02:00
c9d551213a Remove license screen from installers 2017-08-12 03:15:51 +02:00
1e86a33ceb Hide video player overlay when video process exits gracelessly 2017-08-12 03:12:50 +02:00
551dd229f5 Make back mouse button hide video player and overlay 2017-08-12 03:04:24 +02:00
5ecf3c4147 Fix video player going past the end of a video when paused near the end 2017-08-12 02:26:52 +02:00
91bb2f4df0 Fix video player control panel not disappearing & improve error handling 2017-08-12 01:02:09 +02:00
ae3a0ae83d Fix crash when trying to update with 'Edit CSS' or 'Edit CEF Arguments' open 2017-08-12 00:05:56 +02:00
63ce7523de Fix oversight from previous commit 2017-08-12 00:01:13 +02:00
9e3b92bfc1 Move PluginManager initialization and move Form manipulation to FormManager 2017-08-11 23:57:44 +02:00
bc1767fb84 Change namespace of BrowserProcesses, MemoryUsageTracker, VideoPlayer 2017-08-11 23:50:16 +02:00
f917096cc7 Refactor plugin execution code 2017-08-11 23:32:47 +02:00
308926a2ae Add video player volume sync with user config 2017-08-11 20:58:37 +02:00
76f2b1a454 Make video player volume slider constant width 2017-08-11 20:20:07 +02:00
d899e4b38b Refactor video player control outside designer for dev convenience 2017-08-11 20:14:45 +02:00
e1422e35cc Add seeking + current time and duration to video player 2017-08-11 16:49:23 +02:00
2c00c6bb81 Expand the video player control panel and add progress bar 2017-08-11 16:21:31 +02:00
7e56ba6408 Make custom video player triggerable in tweet detail 2017-08-11 15:52:20 +02:00
8ceb70e67d Fix back button and context menu handling with a video playing 2017-08-11 15:22:45 +02:00
37d5efef1d Add an icon to TweetDuck.Video.exe 2017-08-11 15:06:38 +02:00
924065c26e Change video play icon color and handle playback errors 2017-08-11 13:59:05 +02:00
58cc7ea10d Add WIP video player for MP4s 2017-08-11 13:27:15 +02:00
f93e275ddf Add a volume slider to video player 2017-08-11 13:22:12 +02:00
06d2a5f715 Make video player pause/unpause when pressing space 2017-08-11 13:20:50 +02:00
3a7455eafe Fix video player cursor & pause/unpause on click 2017-08-11 12:33:34 +02:00
8b676fe6ce Implement video player in TweetDeck 2017-08-11 11:56:19 +02:00
54d12686af Tweak video player UI handling 2017-08-11 11:32:20 +02:00
f231256402 Improve player UI handling (cursor, position, setting owner handle) 2017-08-11 10:31:23 +02:00
410ead66f8 Add video player args and adjust location and size to owner window 2017-08-11 09:36:29 +02:00
c833a810af Add TweetDuck.Video project for video playback 2017-08-11 08:22:12 +02:00
50f1336b1d Tweak headings in update changelog renderer 2017-08-10 16:33:45 +02:00
60ed0b8cde Release 1.8.5.1 2017-08-10 16:25:55 +02:00
cc55a81c1b Remove emoji-instructions.txt during an update 2017-08-10 16:25:49 +02:00
f832e04e9e Remove unnecessary resx files and cleanup csproj 2017-08-10 15:09:15 +02:00
fc760b9a0c Fix another case in duplicate DM notifications 2017-08-10 13:46:13 +02:00
9addff0521 Exclude emoji-instructions.txt from build 2017-08-10 13:45:32 +02:00
dcaa3aab19 Work around duplicate DM notifications and rewrite recent tweet check 2017-08-10 00:51:38 +02:00
628785c68c Move _postbuild.bat to an MSBuild target directly in the project file 2017-08-10 00:20:58 +02:00
a5aa396fda Fix image quality setting not working in columns with large previews 2017-08-09 18:48:36 +02:00
f53a9f05e3 Fix image download filename for avatars and add more unit tests 2017-08-07 14:48:20 +02:00
7749b14156 Increment emoji keyboard plugin version 2017-08-06 20:32:13 +02:00
c15f339718 Fix emoji keyboard not disappearing after pressing ctrl+enter to tweet 2017-08-06 20:31:44 +02:00
775f590bfa Release 1.8.5 2017-08-06 15:58:28 +02:00
76408ea56f Increment verison of edit-design and emoji-keyboard plugins 2017-08-06 15:58:23 +02:00
a391d8ee83 Fix image pasting allowing more than 1 image in DMs 2017-08-05 21:52:38 +02:00
48c38f6e1d Include tweet author and quality in image download filename 2017-08-05 21:32:07 +02:00
37c5fba162 Change text color of sound notification file option for invalid paths 2017-08-05 19:50:30 +02:00
23e99b1d44 Update GC memory threshold defaults, also GC reload is enabled by default 2017-08-05 19:42:10 +02:00
8432240a47 Update HW acceleration & GC reload tooltips to note they won't be exported 2017-08-05 19:37:14 +02:00
a4bab743d6 Remove notification warning in GC reload option tooltip 2017-08-05 19:34:20 +02:00
60766789ab Move GC reload options to SystemConfig 2017-08-05 19:27:20 +02:00
ca014f881c Rewrite unknown property handling in FileSerializer 2017-08-05 19:23:42 +02:00
886eabe26c Show notifications that were missed during a browser reload 2017-08-05 18:43:57 +02:00
65b7167b5f Rewrite browser reload to save column notification state in session data 2017-08-05 18:36:17 +02:00
abbdde851e Make quoted tweets and RT account selectors square, fix RT account selector heading 2017-08-05 18:30:42 +02:00
54ac54aba6 Add session data that persists across browser reloads 2017-08-05 18:08:22 +02:00
184340f400 Increase delay for clearing recent notifications to prevent duplicates 2017-08-05 17:06:02 +02:00
93dd6813e8 Fix old icon alignment in 'Add column' dialog 2017-08-05 14:44:49 +02:00
b689b08711 Make follow notification button less visible when not hovered 2017-08-05 12:56:18 +02:00
1479a097d6 Fix alignment of old icons on buttons after TweetDeck update 2017-08-05 02:39:23 +02:00
e4967ea46d Add paragraphs and level 1-2 headings to update notification markdown renderer 2017-08-01 17:33:47 +02:00
3f28f18fb4 Release 1.8.4.1 2017-08-01 17:11:34 +02:00
1b90e0f65e Slightly increase default notification height 2017-08-01 17:05:31 +02:00
756ed649e6 Change default avatar shape to square, rename 'Default' to 'Legacy' 2017-08-01 17:03:29 +02:00
fbc423e2a7 Fix like/retweet notifications having invisible space with notification media previews disabled 2017-08-01 16:59:25 +02:00
f04cdb6a13 Fix PropertyBridge not updating properly 2017-08-01 16:58:46 +02:00
65 changed files with 1920 additions and 523 deletions

View File

@@ -1,9 +1,10 @@
using System;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using TweetDuck.Core.Utils;
using TweetLib.Communication;
namespace TweetDuck.Configuration{
sealed class LockManager{
@@ -137,7 +138,7 @@ namespace TweetDuck.Configuration{
public bool RestoreLockingProcess(int failTimeout){
if (lockingProcess != null){
if (lockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, Program.WindowRestoreMessage, new UIntPtr((uint)lockingProcess.Id), IntPtr.Zero);
Comms.BroadcastMessage(Program.WindowRestoreMessage, (uint)lockingProcess.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckLockingProcessExited() || (lockingProcess.MainWindowHandle != IntPtr.Zero && lockingProcess.Responding), failTimeout, RetryDelay)){
return true;

View File

@@ -6,7 +6,7 @@ using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration{
sealed class SystemConfig{
private static readonly FileSerializer<SystemConfig> Serializer = new FileSerializer<SystemConfig>{
OnReadUnknownProperty = (obj, property, value) => false
// HandleUnknownProperties = (obj, data) => {}
};
public static readonly bool IsHardwareAccelerationSupported = File.Exists(Path.Combine(Program.ProgramPath, "libEGL.dll")) &&
@@ -16,6 +16,9 @@ namespace TweetDuck.Configuration{
private bool _hardwareAcceleration = true;
public bool EnableBrowserGCReload { get; set; } = true;
public int BrowserMemoryThreshold { get; set; } = 400;
// SPECIAL PROPERTIES
public bool HardwareAcceleration{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using TweetDuck.Core;
@@ -10,9 +11,26 @@ using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration{
sealed class UserConfig{
private static readonly FileSerializer<UserConfig> Serializer = new FileSerializer<UserConfig>{
OnReadUnknownProperty = (obj, property, value) => false
};
private static readonly FileSerializer<UserConfig> Serializer = new FileSerializer<UserConfig>{ HandleUnknownProperties = HandleUnknownProperties };
private static void HandleUnknownProperties(UserConfig obj, Dictionary<string, string> data){
if (data.TryGetValue("EnableBrowserGCReload", out string propGCReload) && data.TryGetValue("BrowserMemoryThreshold", out string propMemThreshold)){
if (bool.TryParse(propGCReload, out bool isGCReloadEnabled) && isGCReloadEnabled && int.TryParse(propMemThreshold, out int memThreshold)){
// SystemConfig initialization was moved before UserConfig to allow for this
// TODO remove the migration soon
Program.SystemConfig.EnableBrowserGCReload = true;
Program.SystemConfig.BrowserMemoryThreshold = memThreshold;
Program.SystemConfig.Save();
}
}
data.Remove("EnableBrowserGCReload");
data.Remove("BrowserMemoryThreshold");
if (data.Count == 0){
obj.Save();
}
}
static UserConfig(){
Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
@@ -43,6 +61,7 @@ namespace TweetDuck.Configuration{
public bool SwitchAccountSelectors { get; set; } = true;
public bool BestImageQuality { get; set; } = true;
public bool EnableSpellCheck { get; set; } = false;
public int VideoPlayerVolume { get; set; } = 50;
private int _zoomLevel = 100;
private bool _muteNotifications;
@@ -76,9 +95,6 @@ namespace TweetDuck.Configuration{
public string CustomCefArgs { get; set; } = null;
public string CustomBrowserCSS { get; set; } = null;
public string CustomNotificationCSS { get; set; } = null;
public bool EnableBrowserGCReload { get; set; } = false;
public int BrowserMemoryThreshold { get; set; } = 350;
// SPECIAL PROPERTIES

View File

@@ -8,25 +8,25 @@ namespace TweetDuck.Core.Bridge{
public static string GenerateScript(Environment environment){
string Bool(bool value){
return value ? "true," : "false,";
return value ? "true;" : "false;";
}
StringBuilder build = new StringBuilder().Append("window.$TDX={");
StringBuilder build = new StringBuilder().Append("(function(x){");
build.Append("expandLinksOnHover:").Append(Bool(Program.UserConfig.ExpandLinksOnHover));
build.Append("x.expandLinksOnHover=").Append(Bool(Program.UserConfig.ExpandLinksOnHover));
if (environment == Environment.Browser){
build.Append("switchAccountSelectors:").Append(Bool(Program.UserConfig.SwitchAccountSelectors));
build.Append("muteNotifications:").Append(Bool(Program.UserConfig.MuteNotifications));
build.Append("hasCustomNotificationSound:").Append(Bool(Program.UserConfig.NotificationSoundPath.Length > 0));
build.Append("notificationMediaPreviews:").Append(Bool(Program.UserConfig.NotificationMediaPreviews));
build.Append("x.switchAccountSelectors=").Append(Bool(Program.UserConfig.SwitchAccountSelectors));
build.Append("x.muteNotifications=").Append(Bool(Program.UserConfig.MuteNotifications));
build.Append("x.hasCustomNotificationSound=").Append(Bool(Program.UserConfig.NotificationSoundPath.Length > 0));
build.Append("x.notificationMediaPreviews=").Append(Bool(Program.UserConfig.NotificationMediaPreviews));
}
if (environment == Environment.Notification){
build.Append("skipOnLinkClick:").Append(Bool(Program.UserConfig.NotificationSkipOnLinkClick));
build.Append("x.skipOnLinkClick=").Append(Bool(Program.UserConfig.NotificationSkipOnLinkClick));
}
return build.Append("}").ToString();
return build.Append("})(window.$TDX=window.$TDX||{})").ToString();
}
}
}

View File

@@ -1,8 +1,12 @@
using System.Windows.Forms;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Resources;
namespace TweetDuck.Core.Bridge{
sealed class TweetDeckBridge{
@@ -10,13 +14,28 @@ namespace TweetDuck.Core.Bridge{
public static string LastRightClickedImage = string.Empty;
public static string LastHighlightedTweet = string.Empty;
public static string LastHighlightedQuotedTweet = string.Empty;
public static string LastHighlightedTweetAuthor = string.Empty;
public static string[] LastHighlightedTweetImages = StringUtils.EmptyArray;
public static Dictionary<string, string> SessionData = new Dictionary<string, string>(2);
public static void ResetStaticProperties(){
LastRightClickedLink = LastRightClickedImage = LastHighlightedTweet = LastHighlightedQuotedTweet = string.Empty;
LastRightClickedLink = LastRightClickedImage = LastHighlightedTweet = LastHighlightedQuotedTweet = LastHighlightedTweetAuthor = string.Empty;
LastHighlightedTweetImages = StringUtils.EmptyArray;
}
public static void RestoreSessionData(IFrame frame){
if (SessionData.Count > 0){
StringBuilder build = new StringBuilder().Append("window.TD_SESSION={");
foreach(KeyValuePair<string, string> kvp in SessionData){
build.Append(kvp.Key).Append(":'").Append(kvp.Value.Replace("'", "\\'")).Append("',");
}
ScriptLoader.ExecuteScript(frame, build.Append("}").ToString(), "gen:session");
SessionData.Clear();
}
}
private readonly FormBrowser form;
private readonly FormNotificationMain notification;
@@ -45,10 +64,11 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(() => LastRightClickedImage = link);
}
public void SetLastHighlightedTweet(string link, string quotedLink, string imageList){
public void SetLastHighlightedTweet(string link, string quotedLink, string author, string imageList){
form.InvokeAsyncSafe(() => {
LastHighlightedTweet = link;
LastHighlightedQuotedTweet = quotedLink;
LastHighlightedTweetAuthor = author;
LastHighlightedTweetImages = imageList.Split(';');
});
}
@@ -80,6 +100,12 @@ namespace TweetDuck.Core.Bridge{
}
}
public void SetSessionData(string key, string value){
form.InvokeSafe(() => { // do not use InvokeAsyncSafe, return only after invocation
SessionData.Add(key, value);
});
}
public void LoadNextNotification(){
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
}
@@ -88,6 +114,10 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
}
public void PlayVideo(string url){
form.InvokeAsyncSafe(() => form.PlayVideo(url));
}
public void FixClipboard(){
form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
}

View File

@@ -1,8 +1,9 @@
using System;
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Utils;
using TweetLib.Communication;
namespace TweetDuck.Core.Controls{
static class ControlExtensions{
@@ -70,7 +71,7 @@ namespace TweetDuck.Core.Controls{
public static void SetElevated(this Button button){
button.Text = " "+button.Text;
button.FlatStyle = FlatStyle.System;
NativeMethods.SendMessage(button.Handle, NativeMethods.BCM_SETSHIELD, new UIntPtr(0), new IntPtr(1));
Comms.SendMessage(button.Handle, NativeMethods.BCM_SETSHIELD, 0, 1);
}
public static void EnableMultilineShortcuts(this TextBox textBox){

View File

@@ -23,7 +23,7 @@ namespace TweetDuck.Core.Controls{
Rectangle rect = e.ClipRectangle;
rect.Width = (int)(rect.Width*((double)Value/Maximum));
e.Graphics.FillRectangle(brush,rect);
e.Graphics.FillRectangle(brush, rect);
}
protected override void Dispose(bool disposing){

View File

@@ -2,7 +2,6 @@
using CefSharp.WinForms;
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
@@ -12,6 +11,7 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Screenshot;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Management;
using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
@@ -58,15 +58,18 @@ namespace TweetDuck.Core{
private TweetScreenshotManager notificationScreenshotManager;
private SoundNotification soundNotification;
private VideoPlayer videoPlayer;
public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
public FormBrowser(UpdaterSettings updaterSettings){
InitializeComponent();
Text = Program.BrandName;
this.plugins = pluginManager;
this.plugins = new PluginManager(Program.PluginPath, Program.PluginConfigFilePath);
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
this.plugins.PluginChangedState += plugins_PluginChangedState;
this.plugins.Reload();
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
this.memoryUsageTracker = new MemoryUsageTracker("TDGF_tryRunCleanup");
@@ -84,6 +87,7 @@ namespace TweetDuck.Core{
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
MenuHandler = new ContextMenuBrowser(this),
JsDialogHandler = new JavaScriptDialogHandler(),
KeyboardHandler = new KeyboardHandlerBrowser(this),
LifeSpanHandler = new LifeSpanHandler(),
RequestHandler = new RequestHandlerBrowser()
};
@@ -114,6 +118,7 @@ namespace TweetDuck.Core{
notificationScreenshotManager?.Dispose();
soundNotification?.Dispose();
videoPlayer?.Dispose();
};
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
@@ -132,16 +137,6 @@ namespace TweetDuck.Core{
RestoreWindow();
}
private bool TryBringToFront<T>() where T : Form{
T form = Application.OpenForms.OfType<T>().FirstOrDefault();
if (form != null){
form.BringToFront();
return true;
}
else return false;
}
private void ShowChildForm(Form form){
form.VisibleChanged += (sender, args) => form.MoveToCenter(this);
form.Show(this);
@@ -204,19 +199,15 @@ namespace TweetDuck.Core{
e.Frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorFix);
UpdateProperties(PropertyBridge.Environment.Browser);
TweetDeckBridge.RestoreSessionData(e.Frame);
ScriptLoader.ExecuteFile(e.Frame, "code.js");
ReinjectCustomCSS(Config.CustomBrowserCSS);
if (plugins.HasAnyPlugin(PluginEnvironment.Browser)){
ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginBrowserScriptFile);
ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginGlobalScriptFile);
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Browser, true);
}
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Browser);
TweetDeckBridge.ResetStaticProperties();
if (Config.EnableBrowserGCReload){
memoryUsageTracker.Start(this, e.Browser, Config.BrowserMemoryThreshold);
if (Program.SystemConfig.EnableBrowserGCReload){
memoryUsageTracker.Start(this, e.Browser, Program.SystemConfig.BrowserMemoryThreshold);
}
}
}
@@ -327,7 +318,19 @@ namespace TweetDuck.Core{
}
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
browser.GetBrowser().Reload();
if (e.HasErrors){
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
}
if (isLoaded){
ReloadToTweetDeck();
}
}
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
}
}
private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
@@ -336,11 +339,7 @@ namespace TweetDuck.Core{
private void updates_UpdateAccepted(object sender, UpdateAcceptedEventArgs e){
this.InvokeAsyncSafe(() => {
foreach(Form form in Application.OpenForms.Cast<Form>().Reverse()){
if (form is FormSettings || form is FormPlugins || form is FormAbout){
form.Close();
}
}
FormManager.CloseAllDialogs();
updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
if (update.DownloadStatus == UpdateDownloadStatus.Done){
@@ -396,7 +395,13 @@ namespace TweetDuck.Core{
}
if (isBrowserReady && m.Msg == NativeMethods.WM_PARENTNOTIFY && (m.WParam.ToInt32() & 0xFFFF) == NativeMethods.WM_XBUTTONDOWN){
browser.ExecuteScriptAsync("TDGF_onMouseClickExtra", (m.WParam.ToInt32() >> 16) & 0xFFFF);
if (videoPlayer != null && videoPlayer.Running){
videoPlayer.Close();
}
else{
browser.ExecuteScriptAsync("TDGF_onMouseClickExtra", (m.WParam.ToInt32() >> 16) & 0xFFFF);
}
return;
}
@@ -428,7 +433,7 @@ namespace TweetDuck.Core{
}
public void ReloadToTweetDeck(){
browser.ExecuteScriptAsync($"gc&&gc();window.location.href='{TwitterUtils.TweetDeckURL}'");
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUtils.TweetDeckURL}'");
}
// callback handlers
@@ -442,7 +447,7 @@ namespace TweetDuck.Core{
}
public void OpenSettings(Type startTab){
if (!TryBringToFront<FormSettings>()){
if (!FormManager.TryBringToFront<FormSettings>()){
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
FormSettings form = new FormSettings(this, plugins, updates, startTab);
@@ -457,8 +462,8 @@ namespace TweetDuck.Core{
trayIcon.HasNotifications = false;
}
if (Config.EnableBrowserGCReload){
memoryUsageTracker.Start(this, browser.GetBrowser(), Config.BrowserMemoryThreshold);
if (Program.SystemConfig.EnableBrowserGCReload){
memoryUsageTracker.Start(this, browser.GetBrowser(), Program.SystemConfig.BrowserMemoryThreshold);
}
else{
memoryUsageTracker.Stop();
@@ -475,13 +480,13 @@ namespace TweetDuck.Core{
}
public void OpenAbout(){
if (!TryBringToFront<FormAbout>()){
if (!FormManager.TryBringToFront<FormAbout>()){
ShowChildForm(new FormAbout());
}
}
public void OpenPlugins(){
if (!TryBringToFront<FormPlugins>()){
if (!FormManager.TryBringToFront<FormPlugins>()){
ShowChildForm(new FormPlugins(plugins));
}
}
@@ -505,6 +510,37 @@ namespace TweetDuck.Core{
soundNotification.Play(Config.NotificationSoundPath);
}
public void PlayVideo(string url){
if (string.IsNullOrEmpty(url)){
videoPlayer?.Close();
return;
}
if (videoPlayer == null){
videoPlayer = new VideoPlayer(this);
videoPlayer.ProcessExited += (sender, args) => {
browser.GetBrowser().GetHost().SendFocusEvent(true);
HideVideoOverlay();
};
}
videoPlayer.Launch(url);
}
public bool ToggleVideoPause(){
if (videoPlayer != null && videoPlayer.Running){
videoPlayer.TogglePause();
return true;
}
return false;
}
public void HideVideoOverlay(){
browser.ExecuteScriptAsync("$('#td-video-player-overlay').remove()");
}
public void OnTweetScreenshotReady(string html, int width, int height){
if (notificationScreenshotManager == null){
notificationScreenshotManager = new TweetScreenshotManager(this, plugins);

25
Core/FormManager.cs Normal file
View File

@@ -0,0 +1,25 @@
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Other;
namespace TweetDuck.Core{
static class FormManager{
public static bool TryBringToFront<T>() where T : Form{
T form = Application.OpenForms.OfType<T>().FirstOrDefault();
if (form != null){
form.BringToFront();
return true;
}
else return false;
}
public static void CloseAllDialogs(){
foreach(Form form in Application.OpenForms.Cast<Form>().Reverse()){
if (form is FormSettings || form is FormPlugins || form is FormAbout){
form.Close();
}
}
}
}
}

View File

@@ -32,6 +32,7 @@ namespace TweetDuck.Core.Handling{
private readonly Form form;
private string lastHighlightedTweetAuthor;
private string[] lastHighlightedTweetImageList;
protected ContextMenuBase(Form form){
@@ -40,9 +41,11 @@ namespace TweetDuck.Core.Handling{
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
bool hasTweetImage = !string.IsNullOrEmpty(TweetDeckBridge.LastRightClickedImage);
lastHighlightedTweetAuthor = TweetDeckBridge.LastHighlightedTweetAuthor;
lastHighlightedTweetImageList = TweetDeckBridge.LastHighlightedTweetImages;
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
lastHighlightedTweetAuthor = string.Empty;
lastHighlightedTweetImageList = StringUtils.EmptyArray;
}
@@ -88,11 +91,11 @@ namespace TweetDuck.Core.Handling{
break;
case MenuSaveImage:
TwitterUtils.DownloadImage(GetImage(parameters), ImageQuality);
TwitterUtils.DownloadImage(GetImage(parameters), lastHighlightedTweetAuthor, ImageQuality);
break;
case MenuSaveAllImages:
TwitterUtils.DownloadImages(lastHighlightedTweetImageList, ImageQuality);
TwitterUtils.DownloadImages(lastHighlightedTweetImageList, lastHighlightedTweetAuthor, ImageQuality);
break;
case MenuCopyImageUrl:

View File

@@ -0,0 +1,24 @@
using System.Windows.Forms;
using CefSharp;
namespace TweetDuck.Core.Handling{
sealed class KeyboardHandlerBrowser : IKeyboardHandler{
private readonly FormBrowser form;
public KeyboardHandlerBrowser(FormBrowser form){
this.form = form;
}
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){
if (type == KeyType.RawKeyDown && (Keys)windowsKeyCode == Keys.Space){
return form.ToggleVideoPause();
}
return false;
}
bool IKeyboardHandler.OnKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey){
return false;
}
}
}

View File

@@ -7,6 +7,7 @@ using TweetDuck.Configuration;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Management;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Notification{

View File

@@ -17,10 +17,7 @@ namespace TweetDuck.Core.Notification{
private const int TimerBarHeight = 4;
private static readonly string NotificationScriptIdentifier = ScriptLoader.GetRootIdentifier(NotificationScriptFile);
private static readonly string PluginScriptIdentifier = ScriptLoader.GetRootIdentifier(PluginManager.PluginNotificationScriptFile);
private static readonly string NotificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
private static readonly string PluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile);
private readonly PluginManager plugins;
@@ -67,7 +64,7 @@ namespace TweetDuck.Core.Notification{
get{
switch(Program.UserConfig.NotificationSize){
default:
return BrowserUtils.Scale(118, SizeScale*(1.0+0.075*TweetNotification.FontSizeLevel));
return BrowserUtils.Scale(122, SizeScale*(1.0+0.075*TweetNotification.FontSizeLevel));
case TweetNotification.Size.Custom:
return Program.UserConfig.CustomNotificationSize.Height;
@@ -169,12 +166,7 @@ namespace TweetDuck.Core.Notification{
if (e.Frame.IsMain && NotificationJS != null && browser.Address != "about:blank"){
e.Frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification));
ScriptLoader.ExecuteScript(e.Frame, NotificationJS, NotificationScriptIdentifier);
if (plugins.HasAnyPlugin(PluginEnvironment.Notification)){
ScriptLoader.ExecuteScript(e.Frame, PluginJS, PluginScriptIdentifier);
ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginGlobalScriptFile);
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Notification, false);
}
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Notification);
}
}

View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Diagnostics;
using CefSharp;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Utils{
namespace TweetDuck.Core.Other.Management{
static class BrowserProcesses{
private static readonly Dictionary<int, int> PIDs = new Dictionary<int, int>();

View File

@@ -5,7 +5,7 @@ using System.Windows.Forms;
using CefSharp;
using Timer = System.Timers.Timer;
namespace TweetDuck.Core.Utils{
namespace TweetDuck.Core.Other.Management{
sealed class MemoryUsageTracker : IDisposable{
private const int IntervalMemoryCheck = 60000*30; // 30 minutes
private const int IntervalCleanupAttempt = 60000*5; // 5 minutes

View File

@@ -0,0 +1,175 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetLib.Communication;
namespace TweetDuck.Core.Other.Management{
sealed class VideoPlayer : IDisposable{
private readonly string PlayerExe = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe");
public bool Running{
get{
if (currentProcess == null){
return false;
}
currentProcess.Refresh();
return !currentProcess.HasExited;
}
}
public event EventHandler ProcessExited;
private readonly Form owner;
private string lastUrl;
private Process currentProcess;
private DuplexPipe.Server currentPipe;
private bool isClosing;
public VideoPlayer(Form owner){
this.owner = owner;
this.owner.FormClosing += owner_FormClosing;
}
public void Launch(string url){
if (Running){
Destroy();
isClosing = false;
}
lastUrl = url;
try{
currentPipe = DuplexPipe.CreateServer();
currentPipe.DataIn += currentPipe_DataIn;
if ((currentProcess = Process.Start(new ProcessStartInfo{
FileName = PlayerExe,
Arguments = $"{owner.Handle} {Program.UserConfig.VideoPlayerVolume} \"{url}\" \"{currentPipe.GenerateToken()}\"",
UseShellExecute = false,
RedirectStandardOutput = true
})) != null){
currentProcess.EnableRaisingEvents = true;
currentProcess.Exited += process_Exited;
#if DEBUG
currentProcess.BeginOutputReadLine();
currentProcess.OutputDataReceived += (sender, args) => Debug.WriteLine("VideoPlayer: "+args.Data);
#endif
}
currentPipe.DisposeToken();
}catch(Exception e){
Program.Reporter.HandleException("Video Playback Error", "Error launching video player.", true, e);
}
}
public void TogglePause(){
currentPipe?.Write("pause");
}
private void currentPipe_DataIn(object sender, DuplexPipe.PipeReadEventArgs e){
owner.InvokeSafe(() => {
switch(e.Key){
case "vol":
if (int.TryParse(e.Data, out int volume) && volume != Program.UserConfig.VideoPlayerVolume){
Program.UserConfig.VideoPlayerVolume = volume;
Program.UserConfig.Save();
}
break;
case "rip":
currentPipe.Dispose();
currentPipe = null;
currentProcess.Dispose();
currentProcess = null;
isClosing = false;
TriggerProcessExitEventUnsafe();
break;
}
});
}
public void Close(){
if (currentProcess != null){
if (isClosing){
Destroy();
isClosing = false;
}
else{
isClosing = true;
currentProcess.Exited -= process_Exited;
currentPipe.Write("die");
}
}
}
public void Dispose(){
ProcessExited = null;
isClosing = true;
Destroy();
}
private void Destroy(){
if (currentProcess != null){
try{
currentProcess.Kill();
}catch{
// kill me instead then
}
currentProcess.Dispose();
currentProcess = null;
currentPipe.Dispose();
currentPipe = null;
TriggerProcessExitEventUnsafe();
}
}
private void owner_FormClosing(object sender, FormClosingEventArgs e){
if (currentProcess != null){
currentProcess.Exited -= process_Exited;
}
}
private void process_Exited(object sender, EventArgs e){
switch(currentProcess.ExitCode){
case 3: // CODE_LAUNCH_FAIL
if (FormMessage.Error("Video Playback Error", "Error launching video player, this may be caused by missing Windows Media Player. Do you want to open the video in a browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl);
}
break;
case 4: // CODE_MEDIA_ERROR
if (FormMessage.Error("Video Playback Error", "The video could not be loaded, most likely due to unknown format. Do you want to open the video in a browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl);
}
break;
}
currentProcess.Dispose();
currentProcess = null;
currentPipe.Dispose();
currentPipe = null;
owner.InvokeAsyncSafe(TriggerProcessExitEventUnsafe);
}
private void TriggerProcessExitEventUnsafe(){
ProcessExited?.Invoke(this, new EventArgs());
}
}
}

View File

@@ -68,7 +68,7 @@
this.checkHardwareAcceleration.Size = new System.Drawing.Size(134, 17);
this.checkHardwareAcceleration.TabIndex = 0;
this.checkHardwareAcceleration.Text = "Hardware Acceleration";
this.toolTip.SetToolTip(this.checkHardwareAcceleration, "Uses your graphics card to improve performance.\r\nDisable if you experience issues with rendering.");
this.toolTip.SetToolTip(this.checkHardwareAcceleration, "Uses graphics card to improve performance. Disable if you experience\r\nvisual glitches. This option will not be exported in a profile.");
this.checkHardwareAcceleration.UseVisualStyleBackColor = true;
//
// btnEditCefArgs
@@ -142,26 +142,14 @@
0,
0});
this.numMemoryThreshold.Location = new System.Drawing.Point(202, 82);
this.numMemoryThreshold.Maximum = new decimal(new int[] {
3000,
0,
0,
0});
this.numMemoryThreshold.Minimum = new decimal(new int[] {
200,
0,
0,
0});
this.numMemoryThreshold.Maximum = 2000;
this.numMemoryThreshold.Minimum = 200;
this.numMemoryThreshold.Name = "numMemoryThreshold";
this.numMemoryThreshold.Size = new System.Drawing.Size(97, 20);
this.numMemoryThreshold.TabIndex = 4;
this.numMemoryThreshold.TextSuffix = " MB";
this.toolTip.SetToolTip(this.numMemoryThreshold, "Minimum amount of memory usage by the browser process to trigger the cleanup.\r\nThis is not a limit, the usage is allowed to exceed this value.");
this.numMemoryThreshold.Value = new decimal(new int[] {
350,
0,
0,
0});
this.numMemoryThreshold.Value = 400;
//
// checkBrowserGCReload
//
@@ -172,7 +160,7 @@
this.checkBrowserGCReload.Size = new System.Drawing.Size(190, 17);
this.checkBrowserGCReload.TabIndex = 3;
this.checkBrowserGCReload.Text = "Enable Browser Memory Threshold";
this.toolTip.SetToolTip(this.checkBrowserGCReload, "Automatically reloads TweetDeck to save memory. This option only works\r\nif the browser is in a \'default state\', i.e. all modals and drawers are closed,\r\nand all columns are scrolled to top. Some notifications may be lost.");
this.toolTip.SetToolTip(this.checkBrowserGCReload, "Automatically reloads TweetDeck to save memory. This option only works if\r\nthe browser is in a \'default state\', i.e. all modals and drawers are closed, and\r\nall columns are scrolled to top. This option will not be exported in a profile.");
this.checkBrowserGCReload.UseVisualStyleBackColor = true;
//
// labelApp

View File

@@ -8,6 +8,8 @@ using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Other.Settings{
partial class TabSettingsAdvanced : BaseTabSettings{
private static SystemConfig SysConfig => Program.SystemConfig;
private readonly Action<string> reinjectBrowserCSS;
public TabSettingsAdvanced(Action<string> reinjectBrowserCSS){
@@ -16,16 +18,16 @@ namespace TweetDuck.Core.Other.Settings{
this.reinjectBrowserCSS = reinjectBrowserCSS;
if (SystemConfig.IsHardwareAccelerationSupported){
checkHardwareAcceleration.Checked = Program.SystemConfig.HardwareAcceleration;
checkHardwareAcceleration.Checked = SysConfig.HardwareAcceleration;
}
else{
checkHardwareAcceleration.Enabled = false;
checkHardwareAcceleration.Checked = false;
}
checkBrowserGCReload.Checked = Config.EnableBrowserGCReload;
checkBrowserGCReload.Checked = SysConfig.EnableBrowserGCReload;
numMemoryThreshold.Enabled = checkBrowserGCReload.Checked;
numMemoryThreshold.SetValueSafe(Config.BrowserMemoryThreshold);
numMemoryThreshold.SetValueSafe(SysConfig.BrowserMemoryThreshold);
BrowserCache.CalculateCacheSize(bytes => this.InvokeSafe(() => {
if (bytes == -1L){
@@ -53,6 +55,10 @@ namespace TweetDuck.Core.Other.Settings{
btnRestartArgs.Click += btnRestartArgs_Click;
}
public override void OnClosing(){
SysConfig.Save();
}
private void btnClearCache_Click(object sender, EventArgs e){
btnClearCache.Enabled = false;
BrowserCache.SetClearOnExit();
@@ -60,18 +66,17 @@ namespace TweetDuck.Core.Other.Settings{
}
private void checkHardwareAcceleration_CheckedChanged(object sender, EventArgs e){
Program.SystemConfig.HardwareAcceleration = checkHardwareAcceleration.Checked;
Program.SystemConfig.Save();
PromptRestart();
SysConfig.HardwareAcceleration = checkHardwareAcceleration.Checked;
PromptRestart(); // calls OnClosing
}
private void checkBrowserGCReload_CheckedChanged(object sender, EventArgs e){
Config.EnableBrowserGCReload = checkBrowserGCReload.Checked;
SysConfig.EnableBrowserGCReload = checkBrowserGCReload.Checked;
numMemoryThreshold.Enabled = checkBrowserGCReload.Checked;
}
private void numMemoryThreshold_ValueChanged(object sender, EventArgs e){
Config.BrowserMemoryThreshold = (int)numMemoryThreshold.Value;
SysConfig.BrowserMemoryThreshold = (int)numMemoryThreshold.Value;
}
private void btnEditCefArgs_Click(object sender, EventArgs e){
@@ -82,7 +87,7 @@ namespace TweetDuck.Core.Other.Settings{
};
form.FormClosed += (sender2, args2) => {
NativeMethods.SetFormDisabled(ParentForm, false);
RestoreParentForm();
if (form.DialogResult == DialogResult.OK){
Config.CustomCefArgs = form.CefArgs;
@@ -104,7 +109,7 @@ namespace TweetDuck.Core.Other.Settings{
};
form.FormClosed += (sender2, args2) => {
NativeMethods.SetFormDisabled(ParentForm, false);
RestoreParentForm();
if (form.DialogResult == DialogResult.OK){
Config.CustomBrowserCSS = form.BrowserCSS;
@@ -138,5 +143,11 @@ namespace TweetDuck.Core.Other.Settings{
}
}
}
private void RestoreParentForm(){
if (ParentForm != null){ // when the parent is closed first, ParentForm is null in FormClosed event
NativeMethods.SetFormDisabled(ParentForm, false);
}
}
}
}

View File

@@ -34,7 +34,7 @@ namespace TweetDuck.Core.Other.Settings{
private void tbCustomSound_TextChanged(object sender, EventArgs e){
bool isEmpty = string.IsNullOrEmpty(tbCustomSound.Text);
tbCustomSound.ForeColor = isEmpty || File.Exists(tbCustomSound.Text) ? SystemColors.WindowText : Color.Maroon;
tbCustomSound.ForeColor = isEmpty || File.Exists(tbCustomSound.Text) ? SystemColors.WindowText : Color.Red;
btnPlaySound.Enabled = !isEmpty;
btnResetSound.Enabled = !isEmpty;
}

View File

@@ -8,7 +8,6 @@ namespace TweetDuck.Core.Utils{
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
static class NativeMethods{
public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
public static readonly IntPtr HOOK_HANDLED = new IntPtr(-1);
public const int HWND_TOPMOST = -1;
@@ -65,16 +64,7 @@ namespace TweetDuck.Core.Utils{
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool PostMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern uint RegisterWindowMessage(string messageName);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);

View File

@@ -2,6 +2,7 @@
using CefSharp;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using TweetDuck.Core.Other;
@@ -33,7 +34,7 @@ namespace TweetDuck.Core.Utils{
}
private static string ExtractImageBaseLink(string url){
int dot = url.LastIndexOf('.');
int dot = url.LastIndexOf('/');
return dot == -1 ? url : StringUtils.ExtractBefore(url, ':', dot);
}
@@ -41,7 +42,7 @@ namespace TweetDuck.Core.Utils{
if (quality == ImageQuality.Orig){
string result = ExtractImageBaseLink(url);
if (result != url){
if (result != url || url.Contains("//pbs.twimg.com/media/")){
result += ":orig";
}
@@ -52,23 +53,34 @@ namespace TweetDuck.Core.Utils{
}
}
public static void DownloadImage(string url, ImageQuality quality){
DownloadImages(new string[]{ url }, quality);
public static void DownloadImage(string url, string username, ImageQuality quality){
DownloadImages(new string[]{ url }, username, quality);
}
public static void DownloadImages(string[] urls, ImageQuality quality){
public static void DownloadImages(string[] urls, string username, ImageQuality quality){
if (urls.Length == 0){
return;
}
string file = BrowserUtils.GetFileNameFromUrl(ExtractImageBaseLink(urls[0]));
string firstImageLink = GetImageLink(urls[0], quality);
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
string file = BrowserUtils.GetFileNameFromUrl(ExtractImageBaseLink(firstImageLink));
string ext = Path.GetExtension(file); // includes dot
string[] fileNameParts = qualityIndex == -1 ? new string[]{
Path.ChangeExtension(file, null)
} : new string[]{
username,
Path.ChangeExtension(file, null),
firstImageLink.Substring(qualityIndex+1)
};
using(SaveFileDialog dialog = new SaveFileDialog{
AutoUpgradeEnabled = true,
OverwritePrompt = urls.Length == 1,
Title = "Save image",
FileName = file,
FileName = $"{string.Join(" ", fileNameParts.Where(part => part.Length > 0))}{ext}",
Filter = (urls.Length == 1 ? "Image" : "Images")+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){
if (dialog.ShowDialog() == DialogResult.OK){
@@ -77,14 +89,14 @@ namespace TweetDuck.Core.Utils{
}
if (urls.Length == 1){
BrowserUtils.DownloadFileAsync(GetImageLink(urls[0], quality), dialog.FileName, null, OnFailure);
BrowserUtils.DownloadFileAsync(firstImageLink, dialog.FileName, null, OnFailure);
}
else{
string pathBase = Path.ChangeExtension(dialog.FileName, null);
string pathExt = Path.GetExtension(dialog.FileName);
for(int index = 0; index < urls.Length; index++){
BrowserUtils.DownloadFileAsync(GetImageLink(urls[index], quality), pathBase+(index+1)+pathExt, null, OnFailure);
BrowserUtils.DownloadFileAsync(GetImageLink(urls[index], quality), $"{pathBase} {index+1}{pathExt}", null, OnFailure);
}
}
}

View File

@@ -12,8 +12,8 @@ namespace TweetDuck.Data.Serialization{
private static readonly ITypeConverter BasicSerializerObj = new BasicTypeConverter();
public delegate bool OnReadUnknownPropertyHandler(T obj, string property, string value);
public OnReadUnknownPropertyHandler OnReadUnknownProperty { get; set; }
public delegate void HandleUnknownPropertiesHandler(T obj, Dictionary<string, string> data);
public HandleUnknownPropertiesHandler HandleUnknownProperties { get; set; }
private readonly Dictionary<string, PropertyInfo> props;
private readonly Dictionary<Type, ITypeConverter> converters;
@@ -51,6 +51,8 @@ namespace TweetDuck.Data.Serialization{
}
public void Read(string file, T obj){
Dictionary<string, string> unknownProperties = new Dictionary<string, string>(4);
using(StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))){
if (reader.Peek() <= 1){
throw new FormatException("Input appears to be a binary file.");
@@ -78,11 +80,19 @@ namespace TweetDuck.Data.Serialization{
throw new SerializationException($"Invalid file format, cannot convert value: {value} (property: {property})");
}
}
else if (!(OnReadUnknownProperty?.Invoke(obj, property, value) ?? false)){
throw new SerializationException($"Invalid file format, unknown property: {property}+");
else{
unknownProperties[property] = value;
}
}
}
if (unknownProperties.Count > 0){
HandleUnknownProperties?.Invoke(obj, unknownProperties);
if (unknownProperties.Count > 0){
throw new SerializationException($"Invalid file format, unknown properties: {string.Join(", ", unknownProperties.Keys)}+");
}
}
}
private class BasicTypeConverter : ITypeConverter{

View File

@@ -17,7 +17,20 @@ namespace TweetDuck.Plugins.Enums{
}
}
public static string GetScriptFile(this PluginEnvironment environment){
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
return environment == PluginEnvironment.Browser;
}
public static string GetScriptIdentifier(this PluginEnvironment environment){
switch(environment){
case PluginEnvironment.None: return "root:plugins";
case PluginEnvironment.Browser: return "root:plugins.browser";
case PluginEnvironment.Notification: return "root:plugins.notification";
default: return null;
}
}
public static string GetPluginScriptFile(this PluginEnvironment environment){
switch(environment){
case PluginEnvironment.Browser: return "browser.js";
case PluginEnvironment.Notification: return "notification.js";
@@ -25,7 +38,7 @@ namespace TweetDuck.Plugins.Enums{
}
}
public static string GetScriptVariables(this PluginEnvironment environment){
public static string GetPluginScriptVariables(this PluginEnvironment environment){
switch(environment){
case PluginEnvironment.Browser: return "$,$TD,$TDP,TD";
case PluginEnvironment.Notification: return "$TD,$TDP";

View File

@@ -84,7 +84,7 @@ namespace TweetDuck.Plugins{
public string GetScriptPath(PluginEnvironment environment){
if (Environments.HasFlag(environment)){
string file = environment.GetScriptFile();
string file = environment.GetPluginScriptFile();
return file != null ? Path.Combine(pathRoot, file) : string.Empty;
}
else{
@@ -152,7 +152,7 @@ namespace TweetDuck.Plugins{
private static bool LoadEnvironments(string path, Plugin plugin, out string error){
foreach(string file in Directory.EnumerateFiles(path, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName)){
plugin.Environments |= PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetScriptFile(), StringComparison.Ordinal));
plugin.Environments |= PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
}
if (plugin.Environments == PluginEnvironment.None){

View File

@@ -9,12 +9,14 @@ using TweetDuck.Resources;
namespace TweetDuck.Plugins{
sealed class PluginManager{
public const string PluginBrowserScriptFile = "plugins.browser.js";
public const string PluginNotificationScriptFile = "plugins.notification.js";
public const string PluginGlobalScriptFile = "plugins.js";
private const int InvalidToken = 0;
private static readonly Dictionary<PluginEnvironment, string> PluginSetupScripts = new Dictionary<PluginEnvironment, string>(4){
{ PluginEnvironment.None, ScriptLoader.LoadResource("plugins.js") },
{ PluginEnvironment.Browser, ScriptLoader.LoadResource("plugins.browser.js") },
{ PluginEnvironment.Notification, ScriptLoader.LoadResource("plugins.notification.js") }
};
public string PathOfficialPlugins => Path.Combine(rootPath, "official");
public string PathCustomPlugins => Path.Combine(rootPath, "user");
@@ -97,7 +99,17 @@ namespace TweetDuck.Plugins{
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
}
public void ExecutePlugins(IFrame frame, PluginEnvironment environment, bool includeDisabled){
public void ExecutePlugins(IFrame frame, PluginEnvironment environment){
if (HasAnyPlugin(environment)){
ScriptLoader.ExecuteScript(frame, PluginSetupScripts[environment], environment.GetScriptIdentifier());
ScriptLoader.ExecuteScript(frame, PluginSetupScripts[PluginEnvironment.None], PluginEnvironment.None.GetScriptIdentifier());
ExecutePluginScripts(frame, environment);
}
}
private void ExecutePluginScripts(IFrame frame, PluginEnvironment environment){
bool includeDisabled = environment.IncludesDisabledPlugins();
if (includeDisabled){
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GenerateConfig(Config), "gen:pluginconfig");
}

View File

@@ -8,7 +8,7 @@ namespace TweetDuck.Plugins{
public static string GeneratePlugin(string pluginIdentifier, string pluginContents, int pluginToken, PluginEnvironment environment){
return PluginGen
.Replace("%params", environment.GetScriptVariables())
.Replace("%params", environment.GetPluginScriptVariables())
.Replace("%id", pluginIdentifier)
.Replace("%token", pluginToken.ToString())
.Replace("%contents", pluginContents);

View File

@@ -1,4 +1,4 @@
using CefSharp;
using CefSharp;
using System;
using System.Diagnostics;
using System.Globalization;
@@ -13,17 +13,16 @@ using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Settings.Export;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Events;
using TweetDuck.Updates;
using TweetLib.Communication;
namespace TweetDuck{
static class Program{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.8.4";
public const string VersionFull = "1.8.4";
public const string VersionTag = "1.8.6";
public const string VersionFull = "1.8.6";
public static readonly Version Version = new Version(VersionTag);
public static readonly bool IsPortable = File.Exists("makeportable");
@@ -62,6 +61,10 @@ namespace TweetDuck{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
#if DEBUG
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
#endif
Reporter = new Reporter(ErrorLogFilePath);
Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :(");
}
@@ -71,8 +74,8 @@ namespace TweetDuck{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
SubProcessMessage = NativeMethods.RegisterWindowMessage("TweetDuckSubProcess");
WindowRestoreMessage = Comms.RegisterMessage("TweetDuckRestore");
SubProcessMessage = Comms.RegisterMessage("TweetDuckSubProcess");
if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
@@ -114,9 +117,9 @@ namespace TweetDuck{
return;
}
}
ReloadConfig();
SystemConfig = SystemConfig.Load(SystemConfigFilePath);
ReloadConfig();
if (Arguments.HasFlag(Arguments.ArgImportCookies)){
ExportManager.ImportCookies();
@@ -148,18 +151,13 @@ namespace TweetDuck{
Application.ApplicationExit += (sender, args) => ExitCleanup();
PluginManager plugins = new PluginManager(PluginPath, PluginConfigFilePath);
plugins.Reloaded += plugins_Reloaded;
plugins.Executed += plugins_Executed;
plugins.Reload();
UpdaterSettings updaterSettings = new UpdaterSettings{
AllowPreReleases = Arguments.HasFlag(Arguments.ArgDebugUpdates),
DismissedUpdate = UserConfig.DismissedUpdate,
InstallerDownloadFolder = InstallerPath
};
FormBrowser mainForm = new FormBrowser(plugins, updaterSettings);
FormBrowser mainForm = new FormBrowser(updaterSettings);
Application.Run(mainForm);
if (mainForm.UpdateInstallerPath != null){
@@ -174,18 +172,6 @@ namespace TweetDuck{
}
}
private static void plugins_Reloaded(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
}
}
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
}
}
private static string GetDataStoragePath(){
string custom = Arguments.GetValue(Arguments.ArgDataFolder, null);

View File

@@ -6,6 +6,6 @@ PM> Install-Package CefSharp.WinForms -Version 57.0.0
PM> Install-Package Microsoft.VC120.CRT.JetBrains
```
After building, run either `_postbuild.bat` if you want to package the files yourself, or `bld/RUN BUILD.bat` to generate installer files using Inno Setup (make sure the Inno Setup binaries are on your PATH).
When building the `Release` configuration, the folder will only contain files intended for distribution (no debug symbols or other unnecessary files). You may package the files yourself, or use `bld/RUN BUILD.bat` to generate installer files using Inno Setup (make sure the Inno Setup binaries are on your PATH).
Built files are then available in **bin/x86**, installer files are generated in **bld/Output**. If you decide to release a custom version publicly, please make it clear that it is not the original TweetDuck.
Built files are available in **bin/x86**, installer files are generated in **bld/Output**. If you decide to release a custom version publicly, please make it clear that it is not the original TweetDuck.

View File

@@ -8,7 +8,7 @@ Edit layout & design
chylex
[version]
1.1.1
1.1.2
[website]
https://tweetduck.chylex.com

View File

@@ -14,7 +14,7 @@ enabled(){
revertIcons: true,
smallComposeTextSize: false,
optimizeAnimations: true,
avatarRadius: 10
avatarRadius: 2
};
this.firstTimeLoad = null;
@@ -459,7 +459,8 @@ enabled(){
.icon-list-filled:before{content:"\\f014";font-family:tweetdeckold}
.icon-user-filled:before{content:"\\f035";font-family:tweetdeckold}
.column-type-icon { bottom: 26px !important }
.drawer .btn .icon, .app-header .btn .icon { line-height: 1em !important }
.column-header .column-type-icon { bottom: 26px !important }
.is-options-open .column-type-icon { bottom: 25px !important }
.tweet-footer { margin-top: 6px !important }`;

View File

@@ -122,7 +122,7 @@
</div>
<div class="td-avatar-shape-item-outer" data-td-key="avatarRadius" data-td-value="10">
<div class="td-avatar-shape" style="border-radius:10%"></div>
<label>Default</label>
<label>Legacy</label>
</div>
<div class="td-avatar-shape-item-outer" data-td-key="avatarRadius" data-td-value="30">
<div class="td-avatar-shape" style="border-radius:30%"></div>

View File

@@ -9,7 +9,7 @@ Emoji keyboard
chylex
[version]
1.2
1.3
[website]
https://tweetduck.chylex.com

View File

@@ -1,4 +1,6 @@
enabled(){
this.ENABLE_CUSTOM_KEYBOARD = false;
this.selectedSkinTone = "";
this.currentKeywords = [];
@@ -30,16 +32,28 @@ enabled(){
this.css = window.TDPF_createCustomStyle(this);
this.css.insert(".emoji-keyboard { position: absolute; width: 15.35em; background-color: white; border-radius: 1px; font-size: 24px; z-index: 9999 }");
this.css.insert(".emoji-keyboard-list { height: 10.14em; padding: 0.1em; box-sizing: border-box; overflow-y: auto }");
this.css.insert(".emoji-keyboard-list .separator { height: 26px }");
this.css.insert(".emoji-keyboard-list img { padding: 0.1em !important; width: 1em; height: 1em; vertical-align: -0.1em; cursor: pointer }");
this.css.insert(".emoji-keyboard-search { height: auto; padding: 4px 10px 8px; background-color: #292f33; border-radius: 1px 1px 0 0 }");
this.css.insert(".emoji-keyboard-search input { width: 100%; border-radius: 1px; }");
this.css.insert(".emoji-keyboard-skintones { height: 1.3em; text-align: center; background-color: #292f33; border-radius: 0 0 1px 1px }");
this.css.insert(".emoji-keyboard-skintones div { width: 0.8em; height: 0.8em; margin: 0.25em 0.1em; border-radius: 50%; display: inline-block; box-sizing: border-box; cursor: pointer }");
this.css.insert(".emoji-keyboard-skintones .sel { border: 2px solid rgba(0, 0, 0, 0.35); box-shadow: 0 0 2px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.4) inset }");
this.css.insert(".emoji-keyboard-skintones :hover { border: 2px solid rgba(0, 0, 0, 0.25); box-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.25) inset }");
this.css.insert("#emoji-keyboard-tweet-input { padding: 0 !important; line-height: 18px }");
this.css.insert("#emoji-keyboard-tweet-input img { padding: 0.1em !important; width: 1em; height: 1em; vertical-align: -0.25em }");
this.css.insert("#emoji-keyboard-tweet-input:empty::before { content: \"What's happening?\"; display: inline-block; color: #ced8de }");
this.css.insert(".js-docked-compose .compose-text-container.td-emoji-keyboard-swap .js-compose-text { position: absolute; z-index: -9999; left: 0; opacity: 0 }");
this.css.insert(".compose-text-container:not(.td-emoji-keyboard-swap) #emoji-keyboard-tweet-input { display: none; }");
this.css.insert(".js-compose-text { font-family: \"Twitter Color Emoji\", Helvetica, Arial, Verdana, sans-serif; }");
// 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>';
@@ -47,10 +61,10 @@ enabled(){
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");
var maybeDockedComposePanel = $(".js-docked-compose");
if (dockedComposePanel.length){
dockedComposePanel.find(".cf.margin-t--12.margin-b--30").first().append(buttonHTML);
if (maybeDockedComposePanel.length){
maybeDockedComposePanel.find(".cf.margin-t--12.margin-b--30").first().append(buttonHTML);
}
// keyboard generation
@@ -58,7 +72,7 @@ enabled(){
this.currentKeyboard = null;
this.currentSpanner = null;
var hideKeyboard = () => {
var hideKeyboard = (refocus) => {
$(this.currentKeyboard).remove();
this.currentKeyboard = null;
@@ -70,7 +84,15 @@ enabled(){
this.composePanelScroller.trigger("scroll");
$(".emoji-keyboard-popup-btn").removeClass("is-selected");
$(".js-compose-text").first().focus();
if (refocus){
if ($(".compose-text-container", ".js-docked-compose").hasClass("td-emoji-keyboard-swap")){
$("#emoji-keyboard-tweet-input").focus();
}
else{
$(".js-compose-text", ".js-docked-compose").focus();
}
}
};
var generateEmojiHTML = skinTone => {
@@ -124,7 +146,7 @@ enabled(){
updateFilters();
};
this.generateKeyboard = (input, left, top) => {
this.generateKeyboard = (left, top) => {
var outer = document.createElement("div");
outer.classList.add("emoji-keyboard");
outer.style.left = left+"px";
@@ -134,18 +156,10 @@ enabled(){
keyboard.classList.add("emoji-keyboard-list");
keyboard.addEventListener("click", function(e){
if (e.target.tagName === "IMG"){
var val = input.val();
var inserted = e.target.getAttribute("alt");
var posStart = input[0].selectionStart;
var posEnd = input[0].selectionEnd;
input.val(val.slice(0, posStart)+inserted+val.slice(posStart));
input.trigger("change");
input.focus();
input[0].selectionStart = posStart+inserted.length;
input[0].selectionEnd = posEnd+inserted.length;
let ele = e.target;
if (ele.tagName === "IMG"){
insertEmoji(ele.getAttribute("src"), ele.getAttribute("alt"));
}
e.stopPropagation();
@@ -204,7 +218,43 @@ enabled(){
return button.offset().top+button.outerHeight()+me.composePanelScroller.scrollTop()+8;
};
// event handlers
var focusWithCaretAtEnd = () => {
let range = document.createRange();
range.selectNodeContents(me.composeInputNew[0]);
range.collapse(false);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
};
var insertEmoji = (src, alt) => {
if (this.ENABLE_CUSTOM_KEYBOARD){
replaceEditor(true);
if (!hasSelectionInEditor()){
focusWithCaretAtEnd();
}
document.execCommand("insertHTML", false, `<img src="${src}" alt="${alt}">`);
}
else{
let input = $(".js-compose-text", ".js-docked-compose");
let val = input.val();
let posStart = input[0].selectionStart;
let posEnd = input[0].selectionEnd;
input.val(val.slice(0, posStart)+alt+val.slice(posEnd));
input.trigger("change");
input.focus();
input[0].selectionStart = posStart+alt.length;
input[0].selectionEnd = posStart+alt.length;
}
};
// general event handlers
this.emojiKeyboardButtonClickEvent = function(e){
if (me.currentKeyboard){
@@ -212,7 +262,7 @@ enabled(){
$(this).blur();
}
else{
me.generateKeyboard($(".js-compose-text").first(), $(this).offset().left, getKeyboardTop());
me.generateKeyboard($(this).offset().left, getKeyboardTop());
$(this).addClass("is-selected");
}
@@ -225,15 +275,19 @@ enabled(){
}
};
this.composerSendingEvent = function(e){
hideKeyboard();
};
this.documentClickEvent = function(e){
if (me.currentKeyboard && !e.target.classList.contains("js-compose-text")){
if (me.currentKeyboard && $(e.target).closest(".compose-text-container").length === 0){
hideKeyboard();
}
};
this.documentKeyEvent = function(e){
if (me.currentKeyboard && e.keyCode === 27){ // escape
hideKeyboard();
hideKeyboard(true);
e.stopPropagation();
}
};
@@ -242,10 +296,154 @@ enabled(){
if (me.currentKeyboard){
me.currentKeyboard.style.top = getKeyboardTop()+"px";
}
}
};
// new editor event handlers
var prevOldInputVal = "";
var isEditorActive = false;
var hasSelectionInEditor = function(){
let sel = window.getSelection();
return sel.anchorNode && $(sel.anchorNode).closest("#emoji-keyboard-tweet-input").length;
};
var migrateEditorText = function(){
let selStart = me.composeInputOrig[0].selectionStart;
let selEnd = me.composeInputOrig[0].selectionEnd;
let val = me.composeInputOrig.val();
let splitStart = val.substring(0, selStart).split("\n");
let splitEnd = val.substring(0, selEnd).split("\n");
me.composeInputNew.text(val);
me.composeInputNew.html(me.composeInputNew.text().replace(/^(.*?)$/gm, "<div>$1</div>").replace(/<div><\/div>/g, "<div><br></div>"));
let sel = window.getSelection();
let range = document.createRange();
if (me.composeInputNew[0].children.length === 0){
focusWithCaretAtEnd();
}
else{
me.composeInputNew.focus();
range.setStart(me.composeInputNew[0].children[splitStart.length-1].firstChild, splitStart.length > 1 ? selStart-val.lastIndexOf("\n", selStart)-1 : selStart);
range.setEnd(me.composeInputNew[0].children[splitEnd.length-1].firstChild, splitEnd.length > 1 ? selEnd-val.lastIndexOf("\n", selEnd)-1 : selEnd);
}
sel.removeAllRanges();
sel.addRange(range);
};
var replaceEditor = function(useCustom){
if (useCustom && !isEditorActive){
isEditorActive = true;
}
else if (!useCustom && isEditorActive){
isEditorActive = false;
}
else return;
$(".compose-text-container", ".js-docked-compose").toggleClass("td-emoji-keyboard-swap", isEditorActive);
if (isEditorActive){
migrateEditorText();
}
else{
me.composeInputOrig.focus();
}
};
this.composeOldInputFocusEvent = function(){
if (!isEditorActive){
return;
}
let val = $(this).val();
if (val.length === 0){
replaceEditor(false);
}
else if (val != prevOldInputVal){
setTimeout(migrateEditorText, 1);
}
else{
focusWithCaretAtEnd();
}
prevOldInputVal = val;
};
var allowedShortcuts = [ 65 /* A */, 67 /* C */, 86 /* V */, 89 /* Y */, 90 /* Z */ ];
this.composeInputKeyEvent = function(e){
if (e.type === "keydown" && (e.ctrlKey || e.metaKey)){
if (e.keyCode === 13){ // enter
$(".js-send-button", ".js-docked-compose").click();
}
else if (e.keyCode >= 48 && !allowedShortcuts.includes(e.keyCode)){
e.preventDefault();
return;
}
}
if (e.keyCode !== 27){ // escape
e.stopPropagation();
}
};
this.composeInputUpdateEvent = function(){
let clone = $(this).clone();
clone.children("div").each(function(){
let ele = $(this)[0];
ele.outerHTML = "\n"+ele.innerHTML;
});
clone.children("img").each(function(){
let ele = $(this)[0];
ele.outerHTML = ele.getAttribute("alt");
});
me.composeInputOrig.val(prevOldInputVal = clone.text());
me.composeInputOrig.trigger("change");
if (prevOldInputVal.length === 0){
replaceEditor(false);
}
/* TODO if (!emoji.length){
let sel = window.getSelection();
let selStart = -1, selEnd = -1;
if ($(sel.anchorNode).closest("#emoji-keyboard-tweet-input").length && sel.rangeCount === 1){
let range = sel.getRangeAt(0);
// TODO figure out offset
}
replaceEditor(false);
me.composeInputOrig.focus();
if (selStart !== -1){
me.composeInputOrig[0].selectionStart = selStart;
me.composeInputOrig[0].selectionEnd = selEnd;
}
}*/
};
this.composeInputPasteEvent = function(e){ // contenteditable with <img alt> handles copying just fine
e.preventDefault();
let text = e.originalEvent.clipboardData.getData("text/plain");
text && document.execCommand("insertText", false, text);
};
// TODO handle @ and hashtags
}
ready(){
this.composeDrawer = $("[data-drawer='compose']");
this.composePanelScroller = $(".js-compose-scroller", ".js-docked-compose").first().children().first();
this.composePanelScroller.on("scroll", this.composerScrollEvent);
@@ -253,6 +451,20 @@ ready(){
$(document).on("click", this.documentClickEvent);
$(document).on("keydown", this.documentKeyEvent);
$(document).on("uiComposeImageAdded", this.uploadFilesEvent);
this.composeDrawer.on("uiComposeTweetSending", this.composerSendingEvent);
// Editor
this.composeInputOrig = $(".js-compose-text", ".js-docked-compose").first();
this.composeInputNew = $('<div id="emoji-keyboard-tweet-input" contenteditable="true" class="compose-text txt-size--14 scroll-v scroll-styled-v scroll-styled-h scroll-alt td-detect-image-paste"></div>').insertAfter(this.composeInputOrig);
if (this.ENABLE_CUSTOM_KEYBOARD){
this.composeInputOrig.on("focus", this.composeOldInputFocusEvent);
this.composeInputNew.on("keydown keypress keyup", this.composeInputKeyEvent);
this.composeInputNew.on("input", this.composeInputUpdateEvent);
this.composeInputNew.on("paste", this.composeInputPasteEvent);
}
// HTML generation
@@ -273,12 +485,14 @@ ready(){
// declaration inserters
let mapUnicode = pt => convUnicode(parseInt(pt, 16));
let addDeclaration1 = decl => {
this.emojiData1.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join(""));
this.emojiData1.push(decl.split(" ").map(mapUnicode).join(""));
};
let addDeclaration2 = (tone, decl) => {
let gen = decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join("");
let gen = decl.split(" ").map(mapUnicode).join("");
if (tone === null){
for(let skinTone of this.skinToneList){
@@ -291,7 +505,7 @@ ready(){
};
let addDeclaration3 = decl => {
this.emojiData3.push(decl.split(" ").map(pt => convUnicode(parseInt(pt, 16))).join(""));
this.emojiData3.push(decl.split(" ").map(mapUnicode).join(""));
};
// line reading
@@ -365,6 +579,9 @@ disabled(){
$(this.currentSpanner).remove();
}
this.composeInputNew.remove();
this.composeInputOrig.off("focus", this.composeOldInputFocusEvent);
this.composePanelScroller.off("scroll", this.composerScrollEvent);
$(".emoji-keyboard-popup-btn").off("click", this.emojiKeyboardButtonClickEvent);
@@ -373,5 +590,7 @@ disabled(){
$(document).off("click", this.documentClickEvent);
$(document).off("keydown", this.documentKeyEvent);
$(document).off("uiComposeImageAdded", this.uploadFilesEvent);
this.composeDrawer.off("uiComposeTweetSending", this.composerSendingEvent);
TD.mustaches["compose/docked_compose.mustache"] = this.prevComposeMustache;
}

View File

@@ -8,7 +8,7 @@ Templates
chylex
[version]
1.0.1
1.0.2
[website]
https://tweetduck.chylex.com

View File

@@ -267,7 +267,7 @@ enabled(){
<ul></ul>
<div class="templates-modal-bottom">
<button data-action="close" class="btn" style="background-color:#d2d2d2;border-color:#ccd0d2"><i class="icon icon-close icon-small padding-rs"></i><span class="label">Close</span></button>
<button data-action="close" class="btn"><i class="icon icon-close icon-small padding-rs"></i><span class="label">Close</span></button>
<button data-action="new-template" class="btn btn-positive"><i class="icon icon-plus icon-small padding-rs"></i><span class="label">New Template</span></button>
</div>
</div>

12
Resources/PostBuild.ps1 Normal file
View File

@@ -0,0 +1,12 @@
Param([Parameter(Mandatory = $True, Position = 1)][string] $dir)
$ErrorActionPreference = "Stop"
Set-Location $dir
ForEach($file in Get-ChildItem -Include *.js, *.html -Recurse){
$lines = Get-Content -Path $file.FullName
$lines = ($lines | % { $_.TrimStart() }) -Replace '^(.*?)((?<=^|[;{}()] )//\s.*)?$', '$1'
$lines | Where { $_ -ne '' } | Set-Content -Path $file.FullName
Write-Host "Processed" $file.FullName.Substring($dir.Length)
}

View File

@@ -97,30 +97,30 @@
// Function: Event callback for a new tweet.
//
var onNewTweet = (function(){
let recentMessages = new Set();
let recentTweets = new Set();
let recentTweetTimer = null;
let startRecentTweetTimer = () => {
if (recentTweetTimer){
window.clearTimeout(recentTweetTimer);
}
recentTweetTimer = window.setTimeout(() => {
recentTweetTimer = null;
recentTweets.clear();
}, 10000);
let resetRecentTweets = () => {
recentTweetTimer = null;
recentTweets.clear();
};
let checkRecentTweet = id => {
if (recentTweets.size > 50){
recentTweets.clear();
}
else if (recentTweets.has(id)){
let startRecentTweetTimer = () => {
recentTweetTimer && window.clearTimeout(recentTweetTimer);
recentTweetTimer = window.setTimeout(resetRecentTweets, 20000);
};
let checkTweetCache = (set, id) => {
if (set.has(id)){
return true;
}
recentTweets.add(id);
startRecentTweetTimer();
if (set.size > 50){
set.clear();
}
set.add(id);
return false;
};
@@ -129,12 +129,20 @@
};
return function(column, tweet){
if (checkRecentTweet(tweet.id)){
if (tweet instanceof TD.services.TwitterConversation || tweet instanceof TD.services.TwitterConversationMessageEvent){
if (checkTweetCache(recentMessages, tweet.id)){
return;
}
}
else if (checkTweetCache(recentTweets, tweet.id)){
return;
}
startRecentTweetTimer();
if (column.model.getHasNotification()){
let previews = $TDX.notificationMediaPreviews;
let sensitive = (tweet.getRelatedTweet() && tweet.getRelatedTweet().possiblySensitive || (tweet.quotedTweet && tweet.quotedTweet.possiblySensitive));
let previews = $TDX.notificationMediaPreviews && !sensitive;
let html = $(tweet.render({
withFooter: false,
@@ -165,6 +173,9 @@
}
}
}
else if (tweet instanceof TD.services.TwitterActionOnTweet){
html.find(".js-media").remove();
}
html.find("a[href='#']").each(function(){ // remove <a> tags around links that don't lead anywhere (such as account names the tweet replied to)
this.outerHTML = this.innerHTML;
@@ -174,12 +185,19 @@
if (type === "follow"){
html.find(".js-user-actions-menu").parent().remove();
html.find(".account-bio").removeClass("padding-t--5").css("padding-top", "2px");
}
else if (type.includes("list_member")){
html.find(".activity-header").first().css("margin-top", "2px");
html.find(".activity-header").css("margin-top", "2px");
html.find(".avatar").first().css("margin-bottom", "0");
}
if (sensitive){
html.find(".media-badge").each(function(){
$(this)[0].lastChild.textContent += " (possibly sensitive)";
});
}
let source = tweet.getRelatedTweet();
let duration = source ? source.text.length+(source.quotedTweet ? source.quotedTweet.text.length : 0) : tweet.text.length;
let tweetUrl = source ? source.getChirpURL() : "";
@@ -388,12 +406,12 @@
return !!highlightedColumnObj;
};
var updateHighlightedTweet = function(ele, obj, link, embeddedLink, imageList){
var updateHighlightedTweet = function(ele, obj, link, embeddedLink, author, imageList){
highlightedTweetEle = ele;
highlightedTweetObj = obj;
if (lastTweet !== link){
$TD.setLastHighlightedTweet(link, embeddedLink, imageList);
$TD.setLastHighlightedTweet(link, embeddedLink, author, imageList);
lastTweet = link;
}
};
@@ -423,18 +441,19 @@
if (tweet.chirpType === TD.services.ChirpBase.TWEET){
let link = tweet.getChirpURL();
let embedded = tweet.quotedTweet ? tweet.quotedTweet.getChirpURL() : "";
let username = tweet.getMainUser().screenName;
let images = tweet.hasImage() ? tweet.getMedia().filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https+":small").join(";") : "";
// TODO maybe handle embedded images too?
updateHighlightedTweet(me, tweet, link || "", embedded || "", images);
updateHighlightedTweet(me, tweet, link || "", embedded || "", username, images);
}
else{
updateHighlightedTweet(me, tweet, "", "", "");
updateHighlightedTweet(me, tweet, "", "", "", "");
}
}
}
else if (e.type === "mouseleave"){
updateHighlightedTweet(null, null, "", "", "");
updateHighlightedTweet(null, null, "", "", "", "");
}
});
})();
@@ -515,10 +534,15 @@
onAppReady.push(function(){
var uploader = $._data(document, "events")["uiComposeAddImageClick"][0].handler.context;
app.delegate(".js-compose-text,.js-reply-tweetbox", "paste", function(e){
app.delegate(".js-compose-text,.js-reply-tweetbox,.td-detect-image-paste", "paste", function(e){
for(let item of e.originalEvent.clipboardData.items){
if (item.type.startsWith("image/")){
$(this).closest(".rpl").find(".js-reply-popout").click(); // popout direct messages
if (!$(this).closest(".rpl").find(".js-reply-popout").click().length){ // popout direct messages
if ($(".js-add-image-button").is(".is-disabled")){ // tweetdeck does not check upload count properly
return;
}
}
uploader.addFilesToUpload([ item.getAsFile() ]);
$(".js-compose-text", ".js-docked-compose").focus();
@@ -698,14 +722,18 @@
addRule(".keyboard-shortcut-list { vertical-align: top; }"); // fix keyboard navigation alignment
addRule(".account-inline .username { vertical-align: 10%; }"); // move usernames a bit higher
addRule(".character-count-compose { width: 40px !important; }"); // fix strangely wide character count element
addRule(".is-video a:not([href*='youtu']) .icon-bg-dot, .is-gif .icon-bg-dot { color: #9f51cf; }"); // change play icon on mp4s
addRule(".column-nav-link .attribution { position: absolute; }"); // fix cut off account names
addRule(".txt-base-smallest .sprite-verified-mini { width: 13px !important; height: 13px !important; background-position: -223px -99px !important; }"); // fix cut off badge icon when zoomed in
addRule(".btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .media-item { border-radius: 1px !important; }"); // square-ify buttons, inputs, dialogs, menus, and media previews
addRule(".dropdown-menu, .list-item-last { border-radius: 0 !important; }"); // square-ify dropdowns
addRule(".btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .media-item, .media-preview, .tooltip-inner { border-radius: 1px !important; }"); // square-ify buttons, inputs, dialogs, menus, media previews
addRule(".compose-text-container, .dropdown-menu, .list-item-last, .quoted-tweet { border-radius: 0 !important; }"); // square-ify dropdowns, quoted tweets, and account selectors
addRule(".prf-header { border-radius: 0; }"); // fix user account header border
addRule(".accs li, .accs img { border-radius: 0 !important; }"); // square-ify retweet account selector
addRule(".accs-header { padding-left: 0 !important; }"); // fix retweet account selector heading
addRule(".scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb, .antiscroll-scrollbar { border-radius: 0; }"); // square-ify scroll bars
addRule(".antiscroll-scrollbar-vertical { margin-top: 0; }"); // square-ify scroll bars
addRule(".antiscroll-scrollbar-horizontal { margin-left: 0; }"); // square-ify scroll bars
@@ -729,6 +757,10 @@
addRule(".activity-header { align-items: center !important; margin-bottom: 4px; }"); // tweak alignment of avatar and text in notifications
addRule(".activity-header .tweet-timestamp { line-height: unset; }"); // fix timestamp position in notifications
addRule(".account-bio.padding-t--5 { padding-top: 2px !important; }"); // decrease padding on follow notifications
addRule("html[data-td-theme='light'] .stream-item:not(:hover) .js-user-actions-menu { color: #000; border-color: #000; opacity: 0.25; }"); // make follow notification button nicer
addRule("html[data-td-theme='dark'] .stream-item:not(:hover) .js-user-actions-menu { color: #fff; border-color: #fff; opacity: 0.25; }"); // make follow notification button nicer
addRule(".app-columns-container::-webkit-scrollbar-track { border-left: 0; }"); // remove weird border in the column container scrollbar
addRule(".app-columns-container { bottom: 0 !important; }"); // move column container scrollbar to bottom to fit updated style
@@ -736,8 +768,10 @@
addRule(".js-column-header .column-header-link { padding: 0; }"); // fix column header tooltip hover box
addRule(".js-column-header .column-header-link .icon { padding: 9px 4px; width: calc(1em + 8px); height: 100%; box-sizing: border-box; }"); // fix column header tooltip hover box
addRule(".is-video a:not([href*='youtu']), .is-gif .js-media-gif-container { cursor: alias; }"); // change cursor on unsupported videos
addRule(".is-video a:not([href*='youtu']) .icon-bg-dot, .is-gif .icon-bg-dot { color: #bd3d37; }"); // change play icon color on unsupported videos
addRule("#td-compose-drawer-pin { margin: 17px 4px 0 0; transition: transform 0.1s ease; fill: #fff; float: right; cursor: pointer; }"); // replace 'stay open' checkbox with a pin icon
addRule(".js-docked-compose footer { display: none; }"); // replace 'stay open' checkbox with a pin icon
addRule(".compose-content { bottom: 0 !important; }"); // replace 'stay open' checkbox with a pin icon
addRule(".js-docked-compose .js-drawer-close { margin: 20px 0 0 !important; }"); // fix close drawer button because twitter is fucking incompetent
window.TDGF_reinjectCustomCSS = function(styles){
$("#tweetduck-custom-css").remove();
@@ -767,20 +801,46 @@
}
//
// Block: Setup unsupported video element hook.
// Block: Setup video player hooks.
//
(function(){
var cancelModal = false;
var playVideo = function(url){
$('<div id="td-video-player-overlay" class="ovl" style="display:block"></div>').on("click contextmenu", function(){
$TD.playVideo(null);
}).appendTo(app);
$TD.playVideo(url);
};
app.delegate(".js-gif-play", "click", function(e){
let src = !e.ctrlKey && $(this).closest(".js-media-gif-container").find("video").attr("src");
if (src){
playVideo(src);
}
else{
let parent = $(e.target).closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
$TD.openBrowser(link.attr("href"));
}
e.stopPropagation();
});
TD.mustaches["status/media_thumb.mustache"] = TD.mustaches["status/media_thumb.mustache"].replace("is-gif", "is-gif is-paused");
TD.mustaches["media/native_video.mustache"] = '<div class="js-media-gif-container media-item nbfc is-video" style="background-image:url({{imageSrc}})"><video class="js-media-gif media-item-gif full-width block {{#isPossiblySensitive}}is-invisible{{/isPossiblySensitive}}" loop src="{{videoUrl}}"></video><a class="js-gif-play pin-all is-actionable">{{> media/video_overlay}}</a></div>';
if (!ensurePropertyExists(TD, "components", "MediaGallery", "prototype", "_loadTweet") ||
!ensurePropertyExists(TD, "components", "BaseModal", "prototype", "setAndShowContainer") ||
!ensurePropertyExists(TD, "ui", "Column", "prototype", "playGifIfNotManuallyPaused"))return;
var cancelModal = false;
TD.components.MediaGallery.prototype._loadTweet = appendToFunction(TD.components.MediaGallery.prototype._loadTweet, function(){
let media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service !== "youtube"){
$TD.openBrowser(this.clickedLink);
playVideo(media.chooseVideoVariant().url);
cancelModal = true;
}
});
@@ -793,15 +853,6 @@
});
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){
let parent = $(e.target).closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
$TD.openBrowser(link.attr("href"));
e.stopPropagation();
});
})();
//
@@ -820,6 +871,34 @@
media.SERVICES["youtube"] = media.YOUTUBE_RE;
}
//
// Block: Add a pin icon to make tweet compose drawer stay open.
//
onAppReady.push(function(){
let ele = $(`<svg id="td-compose-drawer-pin" viewBox="0 0 24 24" class="icon js-show-tip" data-original-title="Stay open" data-tooltip-position="left">
<path d="M9.884,16.959l3.272,0.001l-0.82,4.568l-1.635,0l-0.817,-4.569Z"/>
<rect x="8.694" y="7.208" width="5.652" height="7.445"/>
<path d="M16.877,17.448c0,-1.908 -1.549,-3.456 -3.456,-3.456l-3.802,0c-1.907,0 -3.456,1.548 -3.456,3.456l10.714,0Z"/>
<path d="M6.572,5.676l2.182,2.183l5.532,0l2.182,-2.183l0,-1.455l-9.896,0l0,1.455Z"/>
</svg>`
).appendTo(".js-docked-compose .js-compose-header");
ele.click(function(){
if (TD.settings.getComposeStayOpen()){
ele.css("transform", "rotate(0deg)");
TD.settings.setComposeStayOpen(false);
}
else{
ele.css("transform", "rotate(90deg)");
TD.settings.setComposeStayOpen(true);
}
});
if (TD.settings.getComposeStayOpen()){
ele.css("transform", "rotate(90deg)");
}
});
//
// Block: Fix DM reply input box not getting focused after opening a conversation.
//
@@ -845,8 +924,19 @@
}
//
// Block: Memory cleanup check and execution.
// Block: Custom reload function with memory cleanup.
//
window.TDGF_reload = function(){
let session = TD.storage.feedController.getAll()
.filter(feed => !!feed.getTopSortIndex())
.reduce((obj, feed) => (obj[feed.privateState.key] = feed.getTopSortIndex(), obj), {});
$TD.setSessionData("gc", JSON.stringify(session)).then(() => {
window.gc && window.gc();
window.location.reload();
});
};
window.TDGF_tryRunCleanup = function(){
// all textareas are empty
if ($("textarea").is(function(){
@@ -873,11 +963,62 @@
}
// cleanup
window.gc && window.gc();
window.location.reload();
window.TDGF_reload();
return true;
};
if (window.TD_SESSION && window.TD_SESSION.gc){
var state;
try{
state = JSON.parse(window.TD_SESSION.gc);
}catch(err){
$TD.crashDebug("Invalid session gc data: "+window.TD_SESSION.gc);
state = {};
}
var showMissedNotifications = function(){
let tweets = [];
let columns = {};
let tmp = new TD.services.ChirpBase;
for(let column of Object.values(TD.controller.columnManager.getAll())){
for(let feed of column.getFeeds()){
if (feed.privateState.key in state){
tmp.sortIndex = state[feed.privateState.key];
for(let tweet of [].concat.apply([], column.updateArray.map(function(chirp){
return chirp.getUnreadChirps(tmp);
}))){
tweets.push(tweet);
columns[tweet.id] = column;
}
}
}
}
tweets.sort(TD.util.chirpReverseColumnSort);
for(let tweet of tweets){
onNewTweet(columns[tweet.id], tweet);
}
};
$(document).one("dataColumnsLoaded", function(){
let columns = Object.values(TD.controller.columnManager.getAll());
let remaining = columns.length;
for(let column of columns){
column.ui.getChirpContainer().one("dataColumnFeedUpdated", () => {
if (--remaining === 0){
setTimeout(showMissedNotifications, 1);
}
});
}
});
}
//
// Block: Disable TweetDeck metrics.
//
@@ -902,6 +1043,8 @@
onAppReady.forEach(func => func());
onAppReady = null;
delete window.TD_SESSION;
if (window.TD_PLUGINS){
window.TD_PLUGINS.onReady();
}

View File

@@ -98,7 +98,7 @@
//
window.TDPF_requestReload = function(){
if (!isReloading){
window.setTimeout(() => location.reload(), 1);
window.setTimeout(window.TDGF_reload, 1);
isReloading = true;
}
};

View File

@@ -125,18 +125,26 @@
font-size: 23px;
}
#tweetduck-changelog h2 + br {
display: none;
}
#tweetduck-changelog h3 {
margin: 0 0 5px 7px;
font-size: 18px;
}
#tweetduck-changelog p {
margin: 0 0 2px 30px;
margin: 8px 8px 0 6px;
}
#tweetduck-changelog p.li {
margin: 0 8px 2px 30px;
display: list-item;
}
#tweetduck-changelog p.l2 {
margin-left: 50px;
margin-left: 50px !important;
}
#tweetduck-changelog a {
@@ -268,14 +276,16 @@
return md.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/^##? (.*?)$/gm, "<h2>$1</h2>")
.replace(/^### (.*?)$/gm, "<h3>$1</h3>")
.replace(/^- (.*?)$/gm, "<p>$1</p>")
.replace(/^ - (.*?)$/gm, "<p class='l2'>$1</p>")
.replace(/^- (.*?)$/gm, "<p class='li'>$1</p>")
.replace(/^ - (.*?)$/gm, "<p class='li l2'>$1</p>")
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.*?)\*/g, "<em>$1</em>")
.replace(/`(.*?)`/g, "<code>$1</code>")
.replace(/\[(.*?)\]\((.*?)\)/g, "<a href='$2'>$1</a>")
.replace(/\n\r?\n/g, "<br>");
.replace(/\[(.*?)\]\((.*?)\)/g, "<a href='$2' target='_blank'>$1</a>")
.replace(/^([a-z0-9].*?)$/gmi, "<p>$1</p>")
.replace(/\n\r?\n\r?/g, "<br>");
};
//

View File

@@ -88,6 +88,7 @@
<Compile Include="Core\Controls\NumericUpDownEx.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Core\FormManager.cs" />
<Compile Include="Core\Handling\General\BrowserProcessHandler.cs" />
<Compile Include="Core\Handling\ContextMenuBase.cs" />
<Compile Include="Core\Handling\ContextMenuBrowser.cs" />
@@ -97,6 +98,7 @@
<Compile Include="Core\FormBrowser.Designer.cs">
<DependentUpon>FormBrowser.cs</DependentUpon>
</Compile>
<Compile Include="Core\Handling\KeyboardHandlerBrowser.cs" />
<Compile Include="Core\Handling\KeyboardHandlerNotification.cs" />
<Compile Include="Core\Handling\RequestHandlerBrowser.cs" />
<Compile Include="Core\Handling\General\RequestHandlerBase.cs" />
@@ -142,6 +144,7 @@
<Compile Include="Core\Other\FormPlugins.Designer.cs">
<DependentUpon>FormPlugins.cs</DependentUpon>
</Compile>
<Compile Include="Core\Other\Management\VideoPlayer.cs" />
<Compile Include="Core\Other\Settings\Dialogs\DialogSettingsCSS.cs">
<SubType>Form</SubType>
</Compile>
@@ -166,7 +169,7 @@
<Compile Include="Core\Other\Settings\Dialogs\DialogSettingsRestart.Designer.cs">
<DependentUpon>DialogSettingsRestart.cs</DependentUpon>
</Compile>
<Compile Include="Core\Utils\BrowserProcesses.cs" />
<Compile Include="Core\Other\Management\BrowserProcesses.cs" />
<Compile Include="Core\Utils\StringUtils.cs" />
<Compile Include="Core\Utils\TwitterUtils.cs" />
<Compile Include="Data\CombinedFileStream.cs" />
@@ -211,7 +214,7 @@
<Compile Include="Data\Serialization\SingleTypeConverter.cs" />
<Compile Include="Data\TwoKeyDictionary.cs" />
<Compile Include="Data\WindowState.cs" />
<Compile Include="Core\Utils\MemoryUsageTracker.cs" />
<Compile Include="Core\Other\Management\MemoryUsageTracker.cs" />
<Compile Include="Core\Utils\WindowsUtils.cs" />
<Compile Include="Core\Bridge\TweetDeckBridge.cs" />
<Compile Include="Core\Other\FormSettings.cs">
@@ -296,15 +299,11 @@
<ItemGroup>
<EmbeddedResource Include="Core\FormBrowser.resx">
<DependentUpon>FormBrowser.cs</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Core\Other\FormAbout.resx">
<DependentUpon>FormAbout.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Core\Other\FormPlugins.resx">
<DependentUpon>FormPlugins.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Core\Other\FormSettings.resx">
<DependentUpon>FormSettings.cs</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Updates\FormUpdateDownload.resx">
<DependentUpon>FormUpdateDownload.cs</DependentUpon>
@@ -325,6 +324,7 @@
<None Include="Resources\icon-small.ico" />
<None Include="Resources\icon-tray-new.ico" />
<None Include="Resources\icon-tray.ico" />
<None Include="Resources\PostBuild.ps1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\Plugins\" />
@@ -349,11 +349,18 @@
<Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
<Name>TweetDuck.Browser</Name>
</ProjectReference>
<ProjectReference Include="video\TweetDuck.Video\TweetDuck.Video.csproj">
<Project>{278b2d11-402d-44b6-b6a1-8fa67db65565}</Project>
<Name>TweetDuck.Video</Name>
</ProjectReference>
<ProjectReference Include="lib\TweetLib.Communication\TweetLib.Communication.csproj">
<Project>{72473763-4b9d-4fb6-a923-9364b2680f06}</Project>
<Name>TweetLib.Communication</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>del "$(TargetPath).config"
xcopy "$(ProjectDir)LICENSE.md" "$(TargetDir)" /Y
<PostBuildEvent>xcopy "$(ProjectDir)LICENSE.md" "$(TargetDir)" /Y
del "$(TargetDir)LICENSE.txt"
ren "$(TargetDir)LICENSE.md" "LICENSE.txt"
xcopy "$(ProjectDir)bld\Resources\CEFSHARP-LICENSE.txt" "$(TargetDir)" /Y
@@ -371,14 +378,16 @@ rmdir "$(ProjectDir)bin\Debug"
rmdir "$(ProjectDir)bin\Release"
rmdir "$(TargetDir)plugins\official\.debug" /S /Q
del "$(TargetDir)plugins\official\emoji-keyboard\emoji-instructions.txt"
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>
)
powershell -ExecutionPolicy Unrestricted -File "$(ProjectDir)Resources\PostBuild.ps1" "$(TargetDir)\"</PostBuildEvent>
</PropertyGroup>
<Import Project="packages\cef.redist.x64.3.2987.1601\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2987.1601\build\cef.redist.x64.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
@@ -390,14 +399,18 @@ if $(ConfigurationName) == Debug (
<Error Condition="!Exists('packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.props'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.targets'))" />
</Target>
<Import Project="packages\cef.redist.x64.3.2987.1601\build\cef.redist.x64.targets" Condition="Exists('packages\cef.redist.x64.3.2987.1601\build\cef.redist.x64.targets')" />
<Import Project="packages\cef.redist.x86.3.2987.1601\build\cef.redist.x86.targets" Condition="Exists('packages\cef.redist.x86.3.2987.1601\build\cef.redist.x86.targets')" />
<Import Project="packages\CefSharp.Common.57.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.57.0.0\build\CefSharp.Common.targets')" />
<Import Project="packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
<Target Name="AfterBuild" Condition="$(ConfigurationName) == Release">
<Exec Command="del &quot;$(TargetDir)*.pdb&quot;" />
<Exec Command="del &quot;$(TargetDir)*.xml&quot;" />
<Delete Files="$(TargetDir)CefSharp.BrowserSubprocess.exe" />
<Delete Files="$(TargetDir)devtools_resources.pak" />
<Delete Files="$(TargetDir)widevinecdmadapter.dll" />
</Target>
<Target Name="AfterBuild">
</Target>
-->
<PropertyGroup>
<PreBuildEvent>powershell Get-Process TweetDuck.Browser -ErrorAction SilentlyContinue ^| Where-Object {$_.Path -eq '$(TargetDir)TweetDuck.Browser.exe'} ^| Stop-Process; Exit 0</PreBuildEvent>
</PropertyGroup>
</Project>

View File

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26430.12
VisualStudioVersion = 15.0.26430.16
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck", "TweetDuck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}"
EndProject
@@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "tests\UnitTest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Audio", "lib\TweetLib.Audio\TweetLib.Audio.csproj", "{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "video\TweetDuck.Video\TweetDuck.Video.csproj", "{278B2D11-402D-44B6-B6A1-8FA67DB65565}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
@@ -32,6 +36,14 @@ Global
{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}.Debug|x86.Build.0 = Debug|x86
{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}.Release|x86.ActiveCfg = Release|x86
{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}.Release|x86.Build.0 = Release|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.ActiveCfg = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.Build.0 = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.ActiveCfg = Release|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.Build.0 = Release|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,6 +0,0 @@
del "bin\x86\Release\*.xml"
del "bin\x86\Release\*.pdb"
del "bin\x86\Release\CefSharp.BrowserSubprocess.exe"
del "bin\x86\Release\devtools_resources.pak"
del "bin\x86\Release\d3dcompiler_43.dll"
del "bin\x86\Release\widevinecdmadapter.dll"

View File

@@ -21,7 +21,6 @@ DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
OutputBaseFilename={#MyAppName}
VersionInfoVersion={#MyAppVersion}
LicenseFile=.\Resources\LICENSE
SetupIconFile=.\Resources\icon.ico
Uninstallable=TDIsUninstallable
UninstallDisplayName={#MyAppName}

View File

@@ -21,7 +21,6 @@ DefaultDirName={sd}\{#MyAppName}
DefaultGroupName={#MyAppName}
OutputBaseFilename={#MyAppName}.Portable
VersionInfoVersion={#MyAppVersion}
LicenseFile=.\Resources\LICENSE
SetupIconFile=.\Resources\icon.ico
Uninstallable=no
UsePreviousAppDir=no

View File

@@ -23,7 +23,6 @@ DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
OutputBaseFilename={#MyAppName}.Update
VersionInfoVersion={#MyAppVersion}
LicenseFile=.\Resources\LICENSE
SetupIconFile=.\Resources\icon.ico
Uninstallable=TDIsUninstallable
UninstallDisplayName={#MyAppName}
@@ -59,6 +58,9 @@ Type: filesandordirs; Name: "{app}\scripts"
Type: filesandordirs; Name: "{localappdata}\{#MyAppName}\Cache"
Type: filesandordirs; Name: "{localappdata}\{#MyAppName}\GPUCache"
[InstallDelete]
Type: files; Name: "{app}\plugins\official\emoji-keyboard\emoji-instructions.txt"
[Code]
function TDIsUninstallable: Boolean; forward;
function TDGetRunArgs(Param: String): String; forward;

View File

@@ -0,0 +1,34 @@
using System;
using TweetLib.Communication.Utils;
namespace TweetLib.Communication{
public static class Comms{
public static void SendMessage(IntPtr hWnd, uint msg, uint wParam, int lParam){
NativeMethods.SendMessage(hWnd, msg, new UIntPtr(wParam), new IntPtr(lParam));
}
public static void SendMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam){
NativeMethods.SendMessage(hWnd, msg, wParam, lParam);
}
public static void PostMessage(IntPtr hWnd, uint msg, uint wParam, int lParam){
NativeMethods.PostMessage(hWnd, msg, new UIntPtr(wParam), new IntPtr(lParam));
}
public static void PostMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam){
NativeMethods.PostMessage(hWnd, msg, wParam, lParam);
}
public static void BroadcastMessage(uint msg, uint wParam, int lParam){
NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, msg, new UIntPtr(wParam), new IntPtr(lParam));
}
public static void BroadcastMessage(uint msg, UIntPtr wParam, IntPtr lParam){
NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, msg, wParam, lParam);
}
public static uint RegisterMessage(string name){
return NativeMethods.RegisterWindowMessage(name);
}
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
namespace TweetLib.Communication{
public abstract class DuplexPipe : IDisposable{
private const string Separator = "\x1F";
public static Server CreateServer(){
return new Server();
}
public static Client CreateClient(string token){
int space = token.IndexOf(' ');
return new Client(token.Substring(0, space), token.Substring(space+1));
}
protected readonly PipeStream pipeIn;
protected readonly PipeStream pipeOut;
private readonly Thread readerThread;
private readonly StreamWriter writerStream;
public event EventHandler<PipeReadEventArgs> DataIn;
protected DuplexPipe(PipeStream pipeIn, PipeStream pipeOut){
this.pipeIn = pipeIn;
this.pipeOut = pipeOut;
this.readerThread = new Thread(ReaderThread){
IsBackground = true
};
this.readerThread.Start();
this.writerStream = new StreamWriter(this.pipeOut);
}
private void ReaderThread(){
using(StreamReader read = new StreamReader(pipeIn)){
string data;
while((data = read.ReadLine()) != null){
DataIn?.Invoke(this, new PipeReadEventArgs(data));
}
}
}
public void Write(string key){
writerStream.WriteLine(key);
writerStream.Flush();
}
public void Write(string key, string data){
writerStream.WriteLine(string.Concat(key, Separator, data));
writerStream.Flush();
}
public void Dispose(){
try{
readerThread.Abort();
}catch{
// /shrug
}
pipeIn.Dispose();
writerStream.Dispose();
}
public sealed class Server : DuplexPipe{
private AnonymousPipeServerStream ServerPipeIn => (AnonymousPipeServerStream)pipeIn;
private AnonymousPipeServerStream ServerPipeOut => (AnonymousPipeServerStream)pipeOut;
internal Server() : base(new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable), new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)){}
public string GenerateToken(){
return ServerPipeIn.GetClientHandleAsString()+" "+ServerPipeOut.GetClientHandleAsString();
}
public void DisposeToken(){
ServerPipeIn.DisposeLocalCopyOfClientHandle();
ServerPipeOut.DisposeLocalCopyOfClientHandle();
}
}
public sealed class Client : DuplexPipe{
internal Client(string handleOut, string handleIn) : base(new AnonymousPipeClientStream(PipeDirection.In, handleIn), new AnonymousPipeClientStream(PipeDirection.Out, handleOut)){}
}
public sealed class PipeReadEventArgs : EventArgs{
public string Key { get; }
public string Data { get; }
internal PipeReadEventArgs(string line){
int separatorIndex = line.IndexOf(Separator, StringComparison.Ordinal);
if (separatorIndex == -1){
Key = line;
Data = string.Empty;
}
else{
Key = line.Substring(0, separatorIndex);
Data = line.Substring(separatorIndex+1);
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{72473763-4B9D-4FB6-A923-9364B2680F06}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetLib.Communication</RootNamespace>
<AssemblyName>TweetLib.Communication</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>none</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="DuplexPipe.cs" />
<Compile Include="Utils\NativeMethods.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Comms.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -1,9 +1,12 @@
using System;
using System.Runtime.InteropServices;
namespace TweetDuck.Browser{
namespace TweetLib.Communication.Utils{
static class NativeMethods{
public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool PostMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);

View File

@@ -1,7 +1,8 @@
using System;
using System;
using System.Diagnostics;
using CefSharp;
using CefSharp.BrowserSubprocess;
using TweetLib.Communication;
namespace TweetDuck.Browser{
static class Program{
@@ -26,7 +27,7 @@ namespace TweetDuck.Browser{
base.OnBrowserCreated(wrapper);
using(Process me = Process.GetCurrentProcess()){
NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, NativeMethods.RegisterWindowMessage("TweetDuckSubProcess"), new UIntPtr((uint)me.Id), new IntPtr(wrapper.BrowserId));
Comms.BroadcastMessage(Comms.RegisterMessage("TweetDuckSubProcess"), (uint)me.Id, wrapper.BrowserId);
}
}
}

View File

@@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.1.0")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@@ -31,10 +31,15 @@
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="NativeMethods.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\lib\TweetLib.Communication\TweetLib.Communication.csproj">
<Project>{72473763-4b9d-4fb6-a923-9364b2680f06}</Project>
<Name>TweetLib.Communication</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>call "$(DevEnvDir)..\..\VC\Auxiliary\Build\vcvars32.bat"

View File

@@ -26,9 +26,15 @@ namespace UnitTests.Core{
[TestMethod]
public void TestImageQualityLink(){
Assert.AreEqual("https://pbs.twimg.com/profile_images/123", TwitterUtils.GetImageLink("https://pbs.twimg.com/profile_images/123", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/profile_images/123", TwitterUtils.GetImageLink("https://pbs.twimg.com/profile_images/123", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.GetImageLink("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.GetImageLink("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg", TwitterUtils.GetImageLink("https://pbs.twimg.com/media/123.jpg", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:orig", TwitterUtils.GetImageLink("https://pbs.twimg.com/media/123.jpg", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:small", TwitterUtils.GetImageLink("https://pbs.twimg.com/media/123.jpg:small", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:orig", TwitterUtils.GetImageLink("https://pbs.twimg.com/media/123.jpg:small", TwitterUtils.ImageQuality.Orig));

View File

@@ -0,0 +1,17 @@
using System.ComponentModel;
using System.Windows.Forms;
using WMPLib;
namespace TweetDuck.Video.Controls{
[DesignTimeVisible(true)]
[Clsid("{6bf52a52-394a-11d3-b153-00c04f79faa6}")]
class ControlWMP : AxHost{
public WindowsMediaPlayer Ocx { get; private set; }
public ControlWMP() : base("6bf52a52-394a-11d3-b153-00c04f79faa6"){}
protected override void AttachInterfaces(){
Ocx = (WindowsMediaPlayer)GetOcx();
}
}
}

View File

@@ -0,0 +1,57 @@
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Video.Controls{
sealed class SeekBar : ProgressBar{
private readonly SolidBrush brushFore;
private readonly SolidBrush brushHover;
private readonly SolidBrush brushOverlap;
public SeekBar(){
brushFore = new SolidBrush(Color.White);
brushHover = new SolidBrush(Color.White);
brushOverlap = new SolidBrush(Color.White);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
protected override void OnPaint(PaintEventArgs e){
if (brushFore.Color != ForeColor){
brushFore.Color = ForeColor;
brushHover.Color = Color.FromArgb(128, ForeColor);
brushOverlap.Color = Color.FromArgb(80+ForeColor.R*11/16, 80+ForeColor.G*11/16, 80+ForeColor.B*11/16);
}
Rectangle rect = e.ClipRectangle;
Point cursor = PointToClient(Cursor.Position);
int width = rect.Width;
int progress = (int)(width*((double)Value/Maximum));
rect.Width = progress;
e.Graphics.FillRectangle(brushFore, rect);
if (cursor.X >= 0 && cursor.Y >= 0 && cursor.X <= width && cursor.Y <= rect.Height){
if (progress >= cursor.X){
rect.Width = cursor.X;
e.Graphics.FillRectangle(brushOverlap, rect);
}
else{
rect.X = progress;
rect.Width = cursor.X-rect.X;
e.Graphics.FillRectangle(brushHover, rect);
}
}
}
protected override void Dispose(bool disposing){
base.Dispose(disposing);
if (disposing){
brushFore.Dispose();
brushHover.Dispose();
brushOverlap.Dispose();
}
}
}
}

View File

@@ -0,0 +1,142 @@
namespace TweetDuck.Video {
partial class FormPlayer {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.timerSync = new System.Windows.Forms.Timer(this.components);
this.trackBarVolume = new System.Windows.Forms.TrackBar();
this.tablePanel = new System.Windows.Forms.TableLayoutPanel();
this.progressSeek = new TweetDuck.Video.Controls.SeekBar();
this.labelTime = new System.Windows.Forms.Label();
this.timerData = new System.Windows.Forms.Timer(this.components);
((System.ComponentModel.ISupportInitialize)(this.trackBarVolume)).BeginInit();
this.tablePanel.SuspendLayout();
this.SuspendLayout();
//
// timerSync
//
this.timerSync.Interval = 15;
this.timerSync.Tick += new System.EventHandler(this.timerSync_Tick);
//
// trackBarVolume
//
this.trackBarVolume.AutoSize = false;
this.trackBarVolume.BackColor = System.Drawing.SystemColors.Control;
this.trackBarVolume.Dock = System.Windows.Forms.DockStyle.Fill;
this.trackBarVolume.Location = new System.Drawing.Point(158, 5);
this.trackBarVolume.Margin = new System.Windows.Forms.Padding(3, 5, 3, 3);
this.trackBarVolume.Maximum = 100;
this.trackBarVolume.Name = "trackBarVolume";
this.trackBarVolume.Size = new System.Drawing.Size(94, 26);
this.trackBarVolume.SmallChange = 5;
this.trackBarVolume.TabIndex = 2;
this.trackBarVolume.TickFrequency = 10;
this.trackBarVolume.TickStyle = System.Windows.Forms.TickStyle.None;
this.trackBarVolume.Value = 50;
this.trackBarVolume.ValueChanged += new System.EventHandler(this.trackBarVolume_ValueChanged);
this.trackBarVolume.MouseDown += new System.Windows.Forms.MouseEventHandler(this.trackBarVolume_MouseDown);
this.trackBarVolume.MouseUp += new System.Windows.Forms.MouseEventHandler(this.trackBarVolume_MouseUp);
//
// tablePanel
//
this.tablePanel.BackColor = System.Drawing.SystemColors.Control;
this.tablePanel.ColumnCount = 3;
this.tablePanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tablePanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 75F));
this.tablePanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 100F));
this.tablePanel.Controls.Add(this.trackBarVolume, 2, 0);
this.tablePanel.Controls.Add(this.progressSeek, 0, 0);
this.tablePanel.Controls.Add(this.labelTime, 1, 0);
this.tablePanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.tablePanel.Location = new System.Drawing.Point(0, 86);
this.tablePanel.Name = "tablePanel";
this.tablePanel.RowCount = 1;
this.tablePanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tablePanel.Size = new System.Drawing.Size(255, 34);
this.tablePanel.TabIndex = 1;
//
// progressSeek
//
this.progressSeek.BackColor = System.Drawing.Color.White;
this.progressSeek.Dock = System.Windows.Forms.DockStyle.Fill;
this.progressSeek.ForeColor = System.Drawing.Color.LimeGreen;
this.progressSeek.Location = new System.Drawing.Point(9, 10);
this.progressSeek.Margin = new System.Windows.Forms.Padding(9, 10, 9, 11);
this.progressSeek.Maximum = 5000;
this.progressSeek.Name = "progressSeek";
this.progressSeek.Size = new System.Drawing.Size(62, 13);
this.progressSeek.Style = System.Windows.Forms.ProgressBarStyle.Continuous;
this.progressSeek.TabIndex = 0;
this.progressSeek.MouseDown += new System.Windows.Forms.MouseEventHandler(this.progressSeek_MouseDown);
//
// labelTime
//
this.labelTime.Dock = System.Windows.Forms.DockStyle.Fill;
this.labelTime.Location = new System.Drawing.Point(80, 2);
this.labelTime.Margin = new System.Windows.Forms.Padding(0, 2, 0, 5);
this.labelTime.Name = "labelTime";
this.labelTime.Size = new System.Drawing.Size(75, 27);
this.labelTime.TabIndex = 1;
this.labelTime.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// timerData
//
this.timerData.Interval = 500;
this.timerData.Tick += new System.EventHandler(this.timerData_Tick);
//
// FormPlayer
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Black;
this.ClientSize = new System.Drawing.Size(255, 120);
this.ControlBox = false;
this.Controls.Add(this.tablePanel);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Location = new System.Drawing.Point(-32000, -32000);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "FormPlayer";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.Text = "TweetDuck Video";
this.Load += new System.EventHandler(this.FormPlayer_Load);
((System.ComponentModel.ISupportInitialize)(this.trackBarVolume)).EndInit();
this.tablePanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Timer timerSync;
private System.Windows.Forms.TrackBar trackBarVolume;
private System.Windows.Forms.TableLayoutPanel tablePanel;
private Controls.SeekBar progressSeek;
private System.Windows.Forms.Label labelTime;
private System.Windows.Forms.Timer timerData;
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Drawing;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using TweetDuck.Video.Controls;
using TweetLib.Communication;
using WMPLib;
namespace TweetDuck.Video{
partial class FormPlayer : Form{
protected override bool ShowWithoutActivation => true;
private readonly IntPtr ownerHandle;
private readonly string videoUrl;
private readonly DuplexPipe pipe;
private readonly ControlWMP player;
private bool wasCursorInside;
private bool isPaused;
private bool isDragging;
private WindowsMediaPlayer Player => player.Ocx;
public FormPlayer(IntPtr handle, int volume, string url, string token){
InitializeComponent();
this.ownerHandle = handle;
this.videoUrl = url;
this.pipe = DuplexPipe.CreateClient(token);
this.pipe.DataIn += pipe_DataIn;
player = new ControlWMP{
Dock = DockStyle.Fill
};
player.BeginInit();
Controls.Add(player);
player.EndInit();
Player.enableContextMenu = false;
Player.uiMode = "none";
Player.settings.autoStart = false;
Player.settings.enableErrorDialogs = false;
Player.settings.setMode("loop", true);
Player.PlayStateChange += player_PlayStateChange;
Player.MediaError += player_MediaError;
trackBarVolume.Value = volume; // changes player volume too if non-default
Application.AddMessageFilter(new MessageFilter(this));
}
// Events
private void FormPlayer_Load(object sender, EventArgs e){
Player.URL = videoUrl;
}
private void pipe_DataIn(object sender, DuplexPipe.PipeReadEventArgs e){
switch(e.Key){
case "pause":
TogglePause();
break;
case "die":
timerSync.Stop();
Visible = false;
pipe.Write("rip");
Close();
break;
}
}
private void player_PlayStateChange(int newState){
WMPPlayState state = (WMPPlayState)newState;
if (state == WMPPlayState.wmppsReady){
Player.controls.play();
}
else if (state == WMPPlayState.wmppsPlaying){
Player.PlayStateChange -= player_PlayStateChange;
timerSync.Start();
NativeMethods.SetWindowOwner(Handle, ownerHandle);
Cursor.Current = Cursors.Default;
}
}
private void player_MediaError(object pMediaObject){
Console.Out.WriteLine(((IWMPMedia2)pMediaObject).Error.errorDescription);
Marshal.ReleaseComObject(pMediaObject);
Environment.Exit(Program.CODE_MEDIA_ERROR);
}
private void timerSync_Tick(object sender, EventArgs e){
if (NativeMethods.GetWindowRect(ownerHandle, out NativeMethods.RECT rect)){
int width = rect.Right-rect.Left+1;
int height = rect.Bottom-rect.Top+1;
IWMPMedia media = Player.currentMedia;
IWMPControls controls = Player.controls;
bool isCursorInside = ClientRectangle.Contains(PointToClient(Cursor.Position));
ClientSize = new Size(Math.Min(media.imageSourceWidth, width*3/4), Math.Min(media.imageSourceHeight, height*3/4));
Location = new Point(rect.Left+(width-ClientSize.Width)/2, rect.Top+(height-ClientSize.Height+SystemInformation.CaptionHeight)/2);
tablePanel.Visible = isCursorInside || isDragging;
if (tablePanel.Visible){
labelTime.Text = $"{controls.currentPositionString} / {media.durationString}";
int value = (int)Math.Round(progressSeek.Maximum*controls.currentPosition/media.duration);
if (value >= progressSeek.Maximum){
progressSeek.Value = progressSeek.Maximum;
progressSeek.Value = progressSeek.Maximum-1;
progressSeek.Value = progressSeek.Maximum;
}
else{
progressSeek.Value = value+1;
progressSeek.Value = value;
}
}
if (controls.currentPosition > media.duration){ // pausing near the end of the video causes WMP to play beyond the end of the video wtf
controls.stop();
controls.currentPosition = 0;
controls.play();
}
if (isCursorInside && !wasCursorInside){
wasCursorInside = true;
}
else if (!isCursorInside && wasCursorInside){
wasCursorInside = false;
if (!Player.fullScreen){
NativeMethods.SetForegroundWindow(ownerHandle);
}
}
Marshal.ReleaseComObject(media);
Marshal.ReleaseComObject(controls);
}
else{
Environment.Exit(Program.CODE_OWNER_GONE);
}
}
private void timerData_Tick(object sender, EventArgs e){
timerData.Stop();
pipe.Write("vol", trackBarVolume.Value.ToString(CultureInfo.InvariantCulture));
}
private void progressSeek_MouseDown(object sender, MouseEventArgs e){
if (e.Button == MouseButtons.Left){
IWMPMedia media = Player.currentMedia;
IWMPControls controls = Player.controls;
controls.currentPosition = media.duration*progressSeek.PointToClient(Cursor.Position).X/progressSeek.Width;
Marshal.ReleaseComObject(media);
Marshal.ReleaseComObject(controls);
}
}
private void trackBarVolume_ValueChanged(object sender, EventArgs e){
IWMPSettings settings = Player.settings;
settings.volume = trackBarVolume.Value;
Marshal.ReleaseComObject(settings);
if (timerSync.Enabled){
timerData.Stop();
timerData.Start();
}
}
private void trackBarVolume_MouseDown(object sender, MouseEventArgs e){
isDragging = true;
}
private void trackBarVolume_MouseUp(object sender, MouseEventArgs e){
isDragging = false;
}
// Controls & messages
private void TogglePause(){
IWMPControls controls = Player.controls;
if (isPaused){
controls.play();
}
else{
controls.pause();
}
isPaused = !isPaused;
Marshal.ReleaseComObject(controls);
}
internal class MessageFilter : IMessageFilter{
private readonly FormPlayer form;
private bool IsCursorOverVideo{
get{
Point cursor = form.PointToClient(Cursor.Position);
return cursor.Y < form.tablePanel.Location.Y;
}
}
public MessageFilter(FormPlayer form){
this.form = form;
}
bool IMessageFilter.PreFilterMessage(ref Message m){
if (m.Msg == 0x0201){ // WM_LBUTTONDOWN
if (IsCursorOverVideo){
form.TogglePause();
return true;
}
}
else if (m.Msg == 0x0203){ // WM_LBUTTONDBLCLK
if (IsCursorOverVideo){
form.TogglePause();
form.Player.fullScreen = !form.Player.fullScreen;
return true;
}
}
else if (m.Msg == 0x0100 && m.WParam.ToInt32() == 0x20){ // WM_KEYDOWN, VK_SPACE
form.TogglePause();
return true;
}
else if (m.Msg == 0x020B && ((m.WParam.ToInt32() >> 16) & 0xFFFF) == 1){ // WM_XBUTTONDOWN
NativeMethods.SetForegroundWindow(form.ownerHandle);
Environment.Exit(Program.CODE_USER_REQUESTED);
}
return false;
}
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Runtime.InteropServices;
namespace TweetDuck.Video{
static class NativeMethods{
private const int GWL_HWNDPARENT = -8;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
public struct RECT{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public static void SetWindowOwner(IntPtr child, IntPtr owner){
SetWindowLong(child, GWL_HWNDPARENT, owner);
/*
* "You must not call SetWindowLong with the GWL_HWNDPARENT index to change the parent of a child window"
*
* ...which I'm not sure they're saying because this is completely unsupported and causes demons to come out of sewers
* ...or because GWL_HWNDPARENT actually changes the OWNER and is therefore NOT changing the parent of a child window
*
* ...so technically, this is following the documentation to the word.
*/
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Globalization;
using System.Windows.Forms;
using TweetLib.Communication;
namespace TweetDuck.Video{
static class Program{
// referenced in VideoPlayer
// set by task manager -- public const int CODE_PROCESS_KILLED = 1;
public const int CODE_INVALID_ARGS = 2;
public const int CODE_LAUNCH_FAIL = 3;
public const int CODE_MEDIA_ERROR = 4;
public const int CODE_OWNER_GONE = 5;
public const int CODE_USER_REQUESTED = 6;
[STAThread]
private static int Main(string[] args){
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
IntPtr ownerHandle;
int defaultVolume;
string videoUrl;
string pipeToken;
try{
ownerHandle = new IntPtr(int.Parse(args[0], NumberStyles.Integer, CultureInfo.InvariantCulture));
defaultVolume = int.Parse(args[1], NumberStyles.Integer, CultureInfo.InvariantCulture);
videoUrl = new Uri(args[2], UriKind.Absolute).AbsoluteUri;
pipeToken = args[3];
}catch{
return CODE_INVALID_ARGS;
}
try{
Application.Run(new FormPlayer(ownerHandle, defaultVolume, videoUrl, pipeToken));
}catch(Exception e){
Console.Out.WriteLine(e.Message);
return CODE_LAUNCH_FAIL;
}
return 0;
}
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{278B2D11-402D-44B6-B6A1-8FA67DB65565}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>TweetDuck.Video</RootNamespace>
<AssemblyName>TweetDuck.Video</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>none</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controls\ControlWMP.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Controls\SeekBar.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="FormPlayer.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="FormPlayer.Designer.cs">
<DependentUpon>FormPlayer.cs</DependentUpon>
</Compile>
<Compile Include="NativeMethods.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<COMReference Include="WMPLib">
<Guid>{6BF52A50-394A-11D3-B153-00C04F79FAA6}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<Content Include="Resources\icon.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\lib\TweetLib.Communication\TweetLib.Communication.csproj">
<Project>{72473763-4b9d-4fb6-a923-9364b2680f06}</Project>
<Name>TweetLib.Windows</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>