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

Compare commits

..

41 Commits

Author SHA1 Message Date
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
33 changed files with 968 additions and 98 deletions

View File

@@ -61,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;

View File

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

@@ -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");
@@ -114,6 +117,7 @@ namespace TweetDuck.Core{
notificationScreenshotManager?.Dispose();
soundNotification?.Dispose();
videoPlayer?.Dispose();
};
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
@@ -132,16 +136,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);
@@ -207,12 +201,7 @@ namespace TweetDuck.Core{
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();
@@ -328,7 +317,19 @@ namespace TweetDuck.Core{
}
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
ReloadToTweetDeck();
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){
@@ -337,11 +338,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){
@@ -392,12 +389,28 @@ namespace TweetDuck.Core{
BrowserProcesses.Link(m.LParam.ToInt32(), processId);
}
return;
}
else if (m.Msg == Program.VideoPlayerMessage){
int volume = m.WParam.ToInt32();
if (Handle == m.LParam && volume != Config.VideoPlayerVolume){
Config.VideoPlayerVolume = volume;
Config.Save();
}
return;
}
}
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;
}
@@ -443,7 +456,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);
@@ -476,13 +489,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));
}
}
@@ -506,6 +519,28 @@ 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 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

@@ -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;
@@ -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,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,114 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
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 Process currentProcess;
private string lastUrl;
public VideoPlayer(Form owner){
this.owner = owner;
this.owner.FormClosing += owner_FormClosing;
}
public void Launch(string url){
Close();
lastUrl = url;
try{
if ((currentProcess = Process.Start(new ProcessStartInfo{
FileName = PlayerExe,
Arguments = $"{owner.Handle} {Program.UserConfig.VideoPlayerVolume} \"{url}\"",
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
}
}catch(Exception e){
Program.Reporter.HandleException("Video Playback Error", "Error launching video player.", true, e);
}
}
public void Close(){
if (currentProcess != null){
currentProcess.Exited -= process_Exited;
try{
currentProcess.Kill();
}catch{
// kill me instead then
}
currentProcess.Dispose();
currentProcess = null;
owner.InvokeAsyncSafe(TriggerProcessExitEventUnsafe);
}
}
public void Dispose(){
ProcessExited = null;
Close();
}
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;
owner.InvokeAsyncSafe(TriggerProcessExitEventUnsafe);
}
private void TriggerProcessExitEventUnsafe(){
ProcessExited?.Invoke(this, new EventArgs());
}
}
}

View File

@@ -87,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;
@@ -109,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;
@@ -143,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

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

@@ -13,8 +13,6 @@ 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;
namespace TweetDuck{
@@ -22,8 +20,8 @@ namespace TweetDuck{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.8.5";
public const string VersionFull = "1.8.5.0";
public const string VersionTag = "1.8.5.1";
public const string VersionFull = "1.8.5.1";
public static readonly Version Version = new Version(VersionTag);
public static readonly bool IsPortable = File.Exists("makeportable");
@@ -46,6 +44,7 @@ namespace TweetDuck{
public static uint WindowRestoreMessage;
public static uint SubProcessMessage;
public static uint VideoPlayerMessage;
private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
private static bool HasCleanedUp;
@@ -73,6 +72,7 @@ namespace TweetDuck{
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
SubProcessMessage = NativeMethods.RegisterWindowMessage("TweetDuckSubProcess");
VideoPlayerMessage = NativeMethods.RegisterWindowMessage("TweetDuckVideoPlayer");
if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
@@ -148,18 +148,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 +169,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

@@ -714,11 +714,12 @@
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, media previews
addRule(".btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .media-item, .tooltip-inner { border-radius: 1px !important; }"); // square-ify buttons, inputs, dialogs, menus, media previews
addRule(".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
@@ -758,8 +759,9 @@
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
window.TDGF_reinjectCustomCSS = function(styles){
$("#tweetduck-custom-css").remove();
@@ -789,20 +791,47 @@
}
//
// 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);
$(this).remove();
}).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;
}
});
@@ -815,15 +844,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();
});
})();
//
@@ -842,6 +862,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.
//

View File

@@ -125,6 +125,10 @@
font-size: 23px;
}
#tweetduck-changelog h2 + br {
display: none;
}
#tweetduck-changelog h3 {
margin: 0 0 5px 7px;
font-size: 18px;
@@ -281,7 +285,7 @@
.replace(/`(.*?)`/g, "<code>$1</code>")
.replace(/\[(.*?)\]\((.*?)\)/g, "<a href='$2'>$1</a>")
.replace(/^([a-z0-9].*?)$/gmi, "<p>$1</p>")
.replace(/\n\r?\n/g, "<br>");
.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" />
@@ -142,6 +143,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 +168,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 +213,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">
@@ -345,6 +347,10 @@
<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>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>

View File

@@ -10,6 +10,8 @@ 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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
@@ -32,6 +34,10 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

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

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

@@ -0,0 +1,17 @@
using System.ComponentModel;
using System.Windows.Forms;
using WMPLib;
namespace TweetDuck.Video{
[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,33 @@
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Video{
sealed class FlatProgressBar : ProgressBar{
private readonly SolidBrush brush;
public FlatProgressBar(){
brush = new SolidBrush(Color.White);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
protected override void OnPaint(PaintEventArgs e){
if (brush.Color != ForeColor){
brush.Color = ForeColor;
}
Rectangle rect = e.ClipRectangle;
rect.Width = (int)(rect.Width*((double)Value/Maximum));
e.Graphics.FillRectangle(brush, rect);
}
protected override void Dispose(bool disposing){
base.Dispose(disposing);
if (disposing){
brush.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.FlatProgressBar();
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 FlatProgressBar progressSeek;
private System.Windows.Forms.Label labelTime;
private System.Windows.Forms.Timer timerData;
}
}

View File

@@ -0,0 +1,223 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using WMPLib;
namespace TweetDuck.Video{
partial class FormPlayer : Form{
protected override bool ShowWithoutActivation => true;
private readonly IntPtr ownerHandle;
private readonly string videoUrl;
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){
InitializeComponent();
this.ownerHandle = handle;
this.videoUrl = url;
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 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();
NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, Program.VideoPlayerMessage, new UIntPtr((uint)trackBarVolume.Value), ownerHandle);
}
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 || (m.Msg == 0x0100 && m.WParam.ToInt32() == 0x20)){ // WM_LBUTTONDBLCLK, WM_KEYDOWN, VK_SPACE
if (IsCursorOverVideo){
form.TogglePause();
form.Player.fullScreen = !form.Player.fullScreen;
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,46 @@
using System;
using System.Runtime.InteropServices;
namespace TweetDuck.Video{
static class NativeMethods{
public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
private const int GWL_HWNDPARENT = -8;
[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 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;
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;
private static uint? message;
public static uint VideoPlayerMessage => message ?? (message = NativeMethods.RegisterWindowMessage("TweetDuckVideoPlayer")).Value;
[STAThread]
private static int Main(string[] args){
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
IntPtr ownerHandle;
int defaultVolume;
string videoUrl;
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;
}catch{
return CODE_INVALID_ARGS;
}
try{
Application.Run(new FormPlayer(ownerHandle, defaultVolume, videoUrl));
}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,78 @@
<?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="ControlWMP.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="FlatProgressBar.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>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>