mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 19:32:10 +02:00
Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
5e9ed5d713 | |||
78e492c764 | |||
59c2a3642b | |||
40ca923745 | |||
03af6cecaa | |||
3992e447f4 | |||
14a9edeb73 | |||
92f1e9f7ec | |||
19c294c53e | |||
fe88ea5c05 | |||
c9d551213a | |||
1e86a33ceb | |||
551dd229f5 | |||
5ecf3c4147 | |||
91bb2f4df0 | |||
ae3a0ae83d | |||
63ce7523de | |||
9e3b92bfc1 | |||
bc1767fb84 | |||
f917096cc7 | |||
308926a2ae | |||
76f2b1a454 | |||
d899e4b38b | |||
e1422e35cc | |||
2c00c6bb81 | |||
7e56ba6408 | |||
8ceb70e67d | |||
37d5efef1d | |||
924065c26e | |||
58cc7ea10d | |||
f93e275ddf | |||
06d2a5f715 | |||
3a7455eafe | |||
8b676fe6ce | |||
54d12686af | |||
f231256402 | |||
410ead66f8 | |||
c833a810af | |||
50f1336b1d | |||
60ed0b8cde | |||
cc55a81c1b | |||
f832e04e9e | |||
fc760b9a0c | |||
9addff0521 | |||
dcaa3aab19 | |||
628785c68c | |||
a5aa396fda | |||
f53a9f05e3 | |||
7749b14156 | |||
c15f339718 | |||
775f590bfa | |||
76408ea56f | |||
a391d8ee83 | |||
48c38f6e1d | |||
37c5fba162 | |||
23e99b1d44 | |||
8432240a47 | |||
a4bab743d6 | |||
60766789ab | |||
ca014f881c | |||
886eabe26c | |||
65b7167b5f | |||
abbdde851e | |||
54ac54aba6 | |||
184340f400 | |||
93dd6813e8 | |||
b689b08711 | |||
1479a097d6 | |||
e4967ea46d | |||
3f28f18fb4 | |||
1b90e0f65e | |||
756ed649e6 | |||
fbc423e2a7 | |||
f04cdb6a13 |
@@ -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{
|
||||
|
@@ -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;
|
||||
|
||||
@@ -77,9 +96,6 @@ namespace TweetDuck.Configuration{
|
||||
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
|
||||
|
||||
public bool IsCustomNotificationPositionSet => CustomNotificationPosition != ControlExtensions.InvisibleLocation;
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
@@ -204,19 +198,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 +317,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 +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){
|
||||
@@ -391,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){
|
||||
if (videoPlayer != null && videoPlayer.Running){
|
||||
videoPlayer.Close();
|
||||
}
|
||||
else{
|
||||
browser.ExecuteScriptAsync("TDGF_onMouseClickExtra", (m.WParam.ToInt32() >> 16) & 0xFFFF);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -428,7 +442,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 +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);
|
||||
@@ -457,8 +471,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 +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));
|
||||
}
|
||||
}
|
||||
@@ -505,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
25
Core/FormManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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:
|
||||
|
@@ -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{
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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>();
|
||||
|
@@ -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
|
114
Core/Other/Management/VideoPlayer.cs
Normal file
114
Core/Other/Management/VideoPlayer.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
22
Core/Other/Settings/TabSettingsAdvanced.Designer.cs
generated
22
Core/Other/Settings/TabSettingsAdvanced.Designer.cs
generated
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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{
|
||||
|
@@ -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";
|
||||
|
@@ -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){
|
||||
|
@@ -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");
|
||||
}
|
||||
|
@@ -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);
|
||||
|
29
Program.cs
29
Program.cs
@@ -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.4";
|
||||
public const string VersionFull = "1.8.4";
|
||||
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);
|
||||
@@ -115,8 +115,8 @@ namespace TweetDuck{
|
||||
}
|
||||
}
|
||||
|
||||
ReloadConfig();
|
||||
SystemConfig = SystemConfig.Load(SystemConfigFilePath);
|
||||
ReloadConfig();
|
||||
|
||||
if (Arguments.HasFlag(Arguments.ArgImportCookies)){
|
||||
ExportManager.ImportCookies();
|
||||
@@ -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);
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -8,7 +8,7 @@ Edit layout & design
|
||||
chylex
|
||||
|
||||
[version]
|
||||
1.1.1
|
||||
1.1.2
|
||||
|
||||
[website]
|
||||
https://tweetduck.chylex.com
|
||||
|
@@ -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 }`;
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -9,7 +9,7 @@ Emoji keyboard
|
||||
chylex
|
||||
|
||||
[version]
|
||||
1.2
|
||||
1.2.2
|
||||
|
||||
[website]
|
||||
https://tweetduck.chylex.com
|
||||
|
@@ -225,6 +225,10 @@ enabled(){
|
||||
}
|
||||
};
|
||||
|
||||
this.composerSendingEvent = function(e){
|
||||
hideKeyboard();
|
||||
};
|
||||
|
||||
this.documentClickEvent = function(e){
|
||||
if (me.currentKeyboard && !e.target.classList.contains("js-compose-text")){
|
||||
hideKeyboard();
|
||||
@@ -246,6 +250,8 @@ enabled(){
|
||||
}
|
||||
|
||||
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 +259,7 @@ ready(){
|
||||
$(document).on("click", this.documentClickEvent);
|
||||
$(document).on("keydown", this.documentKeyEvent);
|
||||
$(document).on("uiComposeImageAdded", this.uploadFilesEvent);
|
||||
this.composeDrawer.on("uiComposeTweetSending", this.composerSendingEvent);
|
||||
|
||||
// HTML generation
|
||||
|
||||
@@ -373,5 +380,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;
|
||||
}
|
||||
|
@@ -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(() => {
|
||||
let resetRecentTweets = () => {
|
||||
recentTweetTimer = null;
|
||||
recentTweets.clear();
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
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,9 +129,16 @@
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -165,6 +172,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;
|
||||
@@ -388,12 +398,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 +433,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, "", "", "", "");
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -518,7 +529,12 @@
|
||||
app.delegate(".js-compose-text,.js-reply-tweetbox", "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 +714,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, .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
|
||||
|
||||
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
|
||||
@@ -730,14 +750,18 @@
|
||||
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("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
|
||||
|
||||
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();
|
||||
@@ -767,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;
|
||||
}
|
||||
});
|
||||
@@ -793,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();
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
@@ -820,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.
|
||||
//
|
||||
@@ -845,8 +915,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 +954,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 +1034,8 @@
|
||||
onAppReady.forEach(func => func());
|
||||
onAppReady = null;
|
||||
|
||||
delete window.TD_SESSION;
|
||||
|
||||
if (window.TD_PLUGINS){
|
||||
window.TD_PLUGINS.onReady();
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@
|
||||
//
|
||||
window.TDPF_requestReload = function(){
|
||||
if (!isReloading){
|
||||
window.setTimeout(() => location.reload(), 1);
|
||||
window.setTimeout(window.TDGF_reload, 1);
|
||||
isReloading = true;
|
||||
}
|
||||
};
|
||||
|
@@ -125,17 +125,25 @@
|
||||
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 {
|
||||
#tweetduck-changelog .l2 {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
@@ -268,14 +276,16 @@
|
||||
return md.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.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(/^([a-z0-9].*?)$/gmi, "<p>$1</p>")
|
||||
.replace(/\n\r?\n\r?/g, "<br>");
|
||||
};
|
||||
|
||||
//
|
||||
|
@@ -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">
|
||||
@@ -296,15 +298,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>
|
||||
@@ -349,11 +347,14 @@
|
||||
<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>
|
||||
<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,6 +372,7 @@ 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
|
||||
@@ -378,7 +380,6 @@ if $(ConfigurationName) == Debug (
|
||||
xcopy "$(ProjectDir)Resources\Plugins\.debug\*" "$(TargetDir)plugins\user\.debug\" /E /Y
|
||||
)</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 +391,15 @@ 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 "$(TargetDir)*.pdb"" />
|
||||
<Exec Command="del "$(TargetDir)*.xml"" />
|
||||
<Delete Files="$(TargetDir)CefSharp.BrowserSubprocess.exe" />
|
||||
<Delete Files="$(TargetDir)devtools_resources.pak" />
|
||||
<Delete Files="$(TargetDir)widevinecdmadapter.dll" />
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
@@ -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,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
|
||||
|
@@ -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"
|
@@ -21,7 +21,6 @@ DefaultDirName={pf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
OutputBaseFilename={#MyAppName}
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
LicenseFile=.\Resources\LICENSE
|
||||
SetupIconFile=.\Resources\icon.ico
|
||||
Uninstallable=TDIsUninstallable
|
||||
UninstallDisplayName={#MyAppName}
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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")]
|
||||
|
@@ -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));
|
||||
|
||||
|
17
video/TweetDuck.Video/ControlWMP.cs
Normal file
17
video/TweetDuck.Video/ControlWMP.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
33
video/TweetDuck.Video/FlatProgressBar.cs
Normal file
33
video/TweetDuck.Video/FlatProgressBar.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
142
video/TweetDuck.Video/FormPlayer.Designer.cs
generated
Normal file
142
video/TweetDuck.Video/FormPlayer.Designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
|
223
video/TweetDuck.Video/FormPlayer.cs
Normal file
223
video/TweetDuck.Video/FormPlayer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
video/TweetDuck.Video/NativeMethods.cs
Normal file
46
video/TweetDuck.Video/NativeMethods.cs
Normal 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.
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
45
video/TweetDuck.Video/Program.cs
Normal file
45
video/TweetDuck.Video/Program.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
35
video/TweetDuck.Video/Properties/AssemblyInfo.cs
Normal file
35
video/TweetDuck.Video/Properties/AssemblyInfo.cs
Normal 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")]
|
BIN
video/TweetDuck.Video/Resources/icon.ico
Normal file
BIN
video/TweetDuck.Video/Resources/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
78
video/TweetDuck.Video/TweetDuck.Video.csproj
Normal file
78
video/TweetDuck.Video/TweetDuck.Video.csproj
Normal 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>
|
Reference in New Issue
Block a user