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

Compare commits

...

91 Commits
1.10 ... 1.11.1

Author SHA1 Message Date
c74c168c96 Release 1.11.1 2017-11-10 14:59:48 +01:00
40b53fa40c Fix broken update notification CSS after a TweetDeck update 2017-11-10 14:52:32 +01:00
3481cc0349 Fix broken CSS in browser and plugins after a TweetDeck update 2017-11-10 14:44:47 +01:00
09abd889e9 Add ID to <html> for selector priority 2017-11-10 14:14:50 +01:00
330bbfbb31 Fix broken CSS in notifications after a TweetDeck update 2017-11-10 13:35:29 +01:00
6b7b690476 Remove Log method from the bridge object 2017-11-09 21:43:22 +01:00
cea72801a7 Fix video player process sometimes taking too long to close 2017-11-09 19:29:01 +01:00
04369e22a7 Add option to disable animated avatars (general GIF toggle)
Note: this breaks when disable-extensions is used, so it was changed to
disable-pdf-extension instead
2017-11-09 18:18:22 +01:00
f1b16eab9a Add a global function (including one for plugins) to reload columns 2017-11-09 18:15:58 +01:00
13646d9c90 Increase quality of media previews in desktop notifications 2017-11-09 17:08:39 +01:00
17d762ce91 Make spell check option not require a restart 2017-11-09 16:51:40 +01:00
edb40adaa1 Release 1.11 2017-11-09 14:40:09 +01:00
bc0809994a Fix import & restart overwriting some imported settings such as zoom, add a TODO for system config import/export 2017-11-08 12:23:58 +01:00
a3e3d517b0 Fix high DPI for dialogs displayed before the main window 2017-11-08 11:39:32 +01:00
d8b63a54ca Update the guide 2017-11-08 10:56:05 +01:00
b81e7583eb Implement feature usage analytics 2017-11-08 08:15:45 +01:00
51f9ba3642 Add option to always show character count to edit-design plugin 2017-11-08 06:40:36 +01:00
296626f7c7 Implement a notification about previously enabled anonymous data sending 2017-11-07 21:37:41 +01:00
5b2daf9746 Refactor method order and return types in config file classes 2017-11-07 18:36:52 +01:00
9a6b615174 Cleanup FileSerializer Write/Read calls & change exception for empty files 2017-11-07 18:10:38 +01:00
18f8d5b269 Tweak key format in analytics request 2017-11-04 14:07:24 +01:00
2867a875c9 "Fix" an uncommon video player crash when closing short videos
Closes #177
2017-11-04 14:03:28 +01:00
ee2f5ae8cb Disable default TweetDeck update notification 2017-11-04 03:30:50 +01:00
bd5c301fb9 Refactor old icon style declaration in edit-design and add !important 2017-11-02 16:45:26 +01:00
6df68629f7 Implement system config export/import/reset (without UI) 2017-11-02 13:34:51 +01:00
be08fd4445 Remove UserConfig.ZoomMultiplier and old migration code 2017-11-02 13:20:19 +01:00
6f1afb94fb Add a global function to reprioritize events and refactor stuff 2017-11-02 11:02:00 +01:00
7401b8a52d Fix search input not being unfocused after opening search externally 2017-11-02 10:10:04 +01:00
c83b62ebaa Make searching while holding Ctrl or middle-clicking search icon open search externally 2017-11-02 09:46:33 +01:00
108cf8923e Implement analytics (#176)
* Implement analytics report generation w/ user config and basic system info

* Add HW and plugin info to analytics report generation

* Add a way of displaying the full analytics report

* Fix issues in analytics report and include design theme

* Ensure tab config is saved when switching tabs

* Fix compilation error in TabSettingsFeedback and safeguard nulls

* Add locale to analytics report

* Work on analytics (utils, last collection label, dependency refactoring)

* Add analytics state file and implement sending reports every week

* Send an analytics report after each update
2017-11-02 03:08:43 +01:00
4e26fd9d56 Random refactoring 2017-11-01 04:02:44 +01:00
8c9168a4bf Temporarily move notification with Alt and restore by middle-clicking title bar
Closes #175
2017-10-31 23:40:56 +01:00
97da0b79e4 Fix compile error after removing TimerBarHeight constant 2017-10-31 23:34:00 +01:00
d7e5f6876b Increase notification timer height with higher DPI 2017-10-31 23:10:08 +01:00
1b92b112e2 Refactor FormNotificationBase.GetBorderStyle 2017-10-31 12:50:34 +01:00
ca55119531 Rewrite CanMoveWindow and CanResizeWindow in notification classes 2017-10-31 12:36:46 +01:00
d9da14b5dc Make the example notification its own class 2017-10-31 12:01:53 +01:00
512b5666ac Fix 'Escape' key not clearing notification tweet queue & refactor HideNotification 2017-10-31 11:31:01 +01:00
64977964e8 Bump project versions 2017-10-31 10:53:42 +01:00
2bc13e0de6 Remove subprocess dependency on communication lib & remove Comms class 2017-10-31 10:51:33 +01:00
b90c5f17cf Fix misaligned Plugins form controls when reloading plugins after scrolling down 2017-10-31 07:18:38 +01:00
7d8d0bd43b Refactor awful plugin loading and management code 2017-10-31 07:13:17 +01:00
Alexander
54c1137927 Fix warning about possible null reference in audio playback error event (#174)
* Fix V3083 warnings from PVS-Studio Static Analyzer
2017-10-29 19:32:18 +01:00
e6655219ee Add a context menu item to apply ROT13 to text selection in inputs 2017-10-29 04:39:28 +01:00
5896f8e35a Allow only px values in custom items in edit-design plugin 2017-10-25 22:12:28 +02:00
934cba7251 Fix link expansion & tooltips not working with long domains (ellipsis in front)
Closes #172
2017-10-25 19:23:02 +02:00
9cc1a11bef Fix a rare issue where GIFs open in browser due to a missing source URL 2017-10-23 00:25:23 +02:00
c1bc956d6d Move TrayIcon to a different namespace 2017-10-21 15:46:52 +02:00
351b174b86 Refactor TweetDeck browser into a separate class 2017-10-21 15:35:46 +02:00
0b4aaf80dc Remove unnecessary usings and code 2017-10-21 14:53:50 +02:00
c10c185817 Release 1.10.3 2017-10-19 00:43:06 +02:00
327ef1cbee Restart TweetDuck if user declines UAC when updating and improve error handling 2017-10-19 00:30:37 +02:00
15eb823c7f Replace OpenExternalBrowserUnsafe with the new OpenAssociatedProgram 2017-10-19 00:24:40 +02:00
54613e5242 Handle errors when opening images in associated viewer 2017-10-19 00:21:53 +02:00
df1352cbe3 Update README.md 2017-10-18 23:50:23 +02:00
0559afd972 Make middle-click tweet actions work in detail view 2017-10-18 16:50:34 +02:00
afffca020e Add context menu item to view images in photo viewer 2017-10-18 13:50:20 +02:00
d663cc3f64 Add username to default video download filename & tweak playback error message 2017-10-17 19:39:35 +02:00
110d41e393 Add function to trigger video playback for plugins 2017-10-17 13:16:19 +02:00
1a8823f592 Fix clipboard html stripping crashing with no text data
Closes #171
2017-10-17 11:22:38 +02:00
6374a852b0 Merge branch 'master' of https://github.com/chylex/TweetDuck 2017-10-16 16:37:24 +02:00
a10c7dd7c3 Fix README heading links 2017-10-16 16:36:30 +02:00
547c7ea417 Update README heading links (still bork on GitHub) 2017-10-16 16:33:41 +02:00
760607995a Update README and post build log message 2017-10-16 16:30:55 +02:00
4704197c09 Add option for larger quote font size to edit-design & update modal layout 2017-10-16 15:38:19 +02:00
093ac1ac40 Make middle-click instant quote work with reply-account plugin 2017-10-16 09:32:52 +02:00
9ed8b0d904 Fix used account in updated middle-click actions 2017-10-15 23:48:56 +02:00
7346ce370d Add middle-click actions (reply popout, RT quote, fav modal) & fix popout in temp columns 2017-10-15 22:59:56 +02:00
adefdadc19 Fix border-radius when quoting a tweet 2017-10-15 22:29:44 +02:00
703bce2d00 Fix PostBuild.ps1 errors not causing failed build & refactor 2017-10-13 13:09:50 +02:00
97928ecd84 Check emoji-ordering.txt for carriage return on build 2017-10-13 12:54:15 +02:00
be9ea7f64a Fuck everything about gitattributes 2017-10-13 12:39:03 +02:00
ec2aaa8789 Add exact emoji name match detection to emoji keyboard 2017-10-13 12:20:54 +02:00
ab14b72526 Force LF in emoji-ordering.txt and other txt/js files 2017-10-13 12:16:39 +02:00
d8e304f3c1 Release 1.10.2 2017-10-12 13:01:47 +02:00
ea53ce361f Bump edit-design plugin version 2017-10-12 10:58:12 +02:00
2fce80b347 Fix custom font size in edit-design & time font in example notification 2017-10-11 20:42:42 +02:00
373c0b1cc3 Fix separator before 'Open dev tools' in example notification context menu 2017-10-11 20:34:50 +02:00
e5e1b7e608 Fix TweetDeck being unable to detect OS name 2017-10-11 20:33:39 +02:00
7e9221c9e0 Merge branch 'master' of https://github.com/chylex/TweetDuck 2017-10-11 20:24:09 +02:00
6b849f854e Rewrite font size & theme handling, add <html> attributes to notifications 2017-10-11 20:22:32 +02:00
831f6bc744 Add support section to readme 2017-10-10 12:25:52 +02:00
d282a7a537 Fix border-radius in media upload previews 2017-10-08 05:43:54 +02:00
fb2f1e3031 Release 1.10.1 2017-10-08 05:27:38 +02:00
00a0da3df3 Fix detail view screenshots & tweak screenshot timings for iframes separately 2017-10-08 01:42:53 +02:00
8c447b1ffb Maybe fix dragged tweet links not being recognized from certain drag sources 2017-09-29 18:04:44 +02:00
a4841175e8 Fix more border-radius, "Ready to Tweet" alignment, and docked reply close icon position 2017-09-29 16:52:39 +02:00
9b139132a1 Disable reply middle-click in temporary columns & fix random reloads from middle-clicks 2017-09-29 14:58:58 +02:00
4a404ecabc Fix weird button styles in inline replies in modal dialogs 2017-09-29 12:48:00 +02:00
aee758b559 Update reply-account docs & fix error with temporary columns and advanced selector 2017-09-29 12:47:13 +02:00
be060d0386 Add new info about emoji usage to the guide 2017-09-28 17:29:34 +02:00
95 changed files with 2432 additions and 1106 deletions

5
.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
*.txt text eof=lf
*.cs diff=csharp

View File

@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Threading;
using TweetDuck.Core.Utils;
using TweetLib.Communication;
namespace TweetDuck.Configuration{
sealed class LockManager{
@@ -136,13 +135,11 @@ namespace TweetDuck.Configuration{
// Locking process
public bool RestoreLockingProcess(int failTimeout){
if (lockingProcess != null){
if (lockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
Comms.BroadcastMessage(Program.WindowRestoreMessage, (uint)lockingProcess.Id, 0);
if (lockingProcess != null && lockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)lockingProcess.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckLockingProcessExited() || (lockingProcess.MainWindowHandle != IntPtr.Zero && lockingProcess.Responding), failTimeout, RetryDelay)){
return true;
}
if (WindowsUtils.TrySleepUntil(() => CheckLockingProcessExited() || (lockingProcess.MainWindowHandle != IntPtr.Zero && lockingProcess.Responding), failTimeout, RetryDelay)){
return true;
}
}
@@ -166,15 +163,12 @@ namespace TweetDuck.Configuration{
lockingProcess = null;
return true;
}
}catch(Exception ex){
if (ex is InvalidOperationException || ex is Win32Exception){
if (lockingProcess != null){
bool hasExited = CheckLockingProcessExited();
lockingProcess.Dispose();
return hasExited;
}
}catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){
if (lockingProcess != null){
bool hasExited = CheckLockingProcessExited();
lockingProcess.Dispose();
return hasExited;
}
else throw;
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using TweetDuck.Core.Utils;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration{
@@ -32,14 +31,11 @@ namespace TweetDuck.Configuration{
this.file = file;
}
public bool Save(){
public void Save(){
try{
WindowsUtils.CreateDirectoryForFile(file);
Serializer.Write(file, this);
return true;
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not save the system configuration file.", true, e);
return false;
}
}
@@ -47,9 +43,7 @@ namespace TweetDuck.Configuration{
SystemConfig config = new SystemConfig(file);
try{
Serializer.Read(file, config);
}catch(FileNotFoundException){
}catch(DirectoryNotFoundException){
Serializer.ReadIfExists(file, config);
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not open the system configuration file. If you continue, you will lose system specific configuration such as Hardware Acceleration.", true, e);
}

View File

@@ -1,26 +1,16 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using TweetDuck.Core;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration{
sealed class UserConfig{
private static readonly FileSerializer<UserConfig> Serializer = new FileSerializer<UserConfig>{ HandleUnknownProperties = HandleUnknownProperties };
private static void HandleUnknownProperties(UserConfig obj, Dictionary<string, string> data){
data.Remove("EnableBrowserGCReload");
data.Remove("BrowserMemoryThreshold");
if (data.Count == 0){
obj.Save();
}
}
private static readonly FileSerializer<UserConfig> Serializer = new FileSerializer<UserConfig>();
static UserConfig(){
Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
@@ -44,8 +34,9 @@ namespace TweetDuck.Configuration{
// CONFIGURATION DATA
public bool FirstRun { get; set; } = true;
public bool AllowDataCollection { get; set; } = false;
public bool FirstRun { get; set; } = true;
public bool AllowDataCollection { get; set; } = false;
public bool ShowDataCollectionNotification { get; set; } = true;
public WindowState BrowserWindow { get; set; } = new WindowState();
public WindowState PluginsWindow { get; set; } = new WindowState();
@@ -54,9 +45,10 @@ namespace TweetDuck.Configuration{
public bool SwitchAccountSelectors { get; set; } = true;
public bool OpenSearchInFirstColumn { get; set; } = true;
public bool BestImageQuality { get; set; } = true;
public bool EnableAnimatedImages { get; set; } = true;
public bool EnableSpellCheck { get; set; } = false;
public int VideoPlayerVolume { get; set; } = 50;
private int _zoomLevel = 100;
private int _zoomLevel = 100;
private bool _muteNotifications;
private TrayIcon.Behavior _trayBehavior = TrayIcon.Behavior.Disabled;
@@ -125,8 +117,6 @@ namespace TweetDuck.Configuration{
}
}
public double ZoomMultiplier => _zoomLevel/100.0;
public TrayIcon.Behavior TrayBehavior{
get => _trayBehavior;
@@ -152,10 +142,8 @@ namespace TweetDuck.Configuration{
this.file = file;
}
public bool Save(){
public void Save(){
try{
WindowsUtils.CreateDirectoryForFile(file);
if (File.Exists(file)){
string backupFile = GetBackupFile(file);
File.Delete(backupFile);
@@ -163,29 +151,23 @@ namespace TweetDuck.Configuration{
}
Serializer.Write(file, this);
return true;
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not save the configuration file.", true, e);
return false;
}
}
public bool Reload(){
public void Reload(){
try{
LoadInternal(false);
return true;
}catch(FileNotFoundException){
try{
Serializer.Write(file, new UserConfig(file));
LoadInternal(false);
return true;
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not regenerate configuration file.", true, e);
return false;
}
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not reload configuration file.", true, e);
return false;
}
}

View File

@@ -11,8 +11,8 @@ using TweetDuck.Resources;
namespace TweetDuck.Core.Bridge{
sealed class TweetDeckBridge{
public static string FontSizeClass;
public static string NotificationHeadContents;
public static string FontSize { get; private set; }
public static string NotificationHeadLayout { get; private set; }
public static string LastHighlightedTweetUrl = string.Empty;
public static string LastHighlightedQuoteUrl = string.Empty;
@@ -25,7 +25,7 @@ namespace TweetDuck.Core.Bridge{
private static readonly Dictionary<string, string> SessionData = new Dictionary<string, string>(2);
public static void ResetStaticProperties(){
FontSizeClass = NotificationHeadContents = null;
FontSize = NotificationHeadLayout = null;
LastHighlightedTweetUrl = LastHighlightedQuoteUrl = LastHighlightedTweetAuthors = LastHighlightedTweetImages = string.Empty;
}
@@ -56,12 +56,11 @@ namespace TweetDuck.Core.Bridge{
});
}
public void LoadFontSizeClass(string fsClass){
form.InvokeAsyncSafe(() => FontSizeClass = fsClass);
}
public void LoadNotificationHeadContents(string headContents){
form.InvokeAsyncSafe(() => NotificationHeadContents = headContents);
public void LoadNotificationLayout(string fontSize, string headLayout){
form.InvokeAsyncSafe(() => {
FontSize = fontSize;
NotificationHeadLayout = headLayout;
});
}
public void SetLastRightClickInfo(string type, string link){
@@ -118,8 +117,8 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
}
public void PlayVideo(string url){
form.InvokeAsyncSafe(() => form.PlayVideo(url));
public void PlayVideo(string url, string username){
form.InvokeAsyncSafe(() => form.PlayVideo(url, username));
}
public void FixClipboard(){
@@ -149,13 +148,9 @@ namespace TweetDuck.Core.Bridge{
public void CrashDebug(string message){
#if DEBUG
Log(message);
System.Diagnostics.Debug.WriteLine(message);
System.Diagnostics.Debugger.Break();
#endif
}
public void Log(string data){
System.Diagnostics.Debug.WriteLine(data);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Drawing;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Core.Controls{
@@ -13,7 +14,7 @@ namespace TweetDuck.Core.Controls{
}
public void SetValueInstant(int value){
ControlExtensions.SetValueInstant(this, value);
ControlExtensions.SetValueInstant(this, Math.Max(Minimum, Math.Min(Maximum, value)));
}
protected override void OnPaint(PaintEventArgs e){

View File

@@ -24,7 +24,7 @@
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.trayIcon = new TweetDuck.Core.TrayIcon(this.components);
this.trayIcon = new TweetDuck.Core.Other.TrayIcon(this.components);
this.toolTip = new System.Windows.Forms.ToolTip(this.components);
this.timerResize = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout();
@@ -57,7 +57,7 @@
#endregion
private TrayIcon trayIcon;
private TweetDuck.Core.Other.TrayIcon trayIcon;
private System.Windows.Forms.ToolTip toolTip;
private System.Windows.Forms.Timer timerResize;
}

View File

@@ -1,23 +1,19 @@
using CefSharp;
using CefSharp.WinForms;
using System;
using System;
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Screenshot;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Management;
using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
using TweetDuck.Updates;
using TweetLib.Audio;
@@ -44,20 +40,19 @@ namespace TweetDuck.Core{
public string UpdateInstallerPath { get; private set; }
private readonly ChromiumWebBrowser browser;
private readonly TweetDeckBrowser browser;
private readonly PluginManager plugins;
private readonly UpdateHandler updates;
private readonly FormNotificationTweet notification;
private readonly ContextMenu contextMenu;
private readonly MemoryUsageTracker memoryUsageTracker;
private bool isLoaded;
private bool isBrowserReady;
private FormWindowState prevState;
private TweetScreenshotManager notificationScreenshotManager;
private SoundNotification soundNotification;
private VideoPlayer videoPlayer;
private AnalyticsManager analytics;
public FormBrowser(UpdaterSettings updaterSettings){
InitializeComponent();
@@ -67,59 +62,26 @@ namespace TweetDuck.Core{
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");
this.notification = new FormNotificationTweet(this, plugins){
#if DEBUG
CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt
#else
CanMoveWindow = () => false
#endif
};
this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show();
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
DialogHandler = new FileDialogHandler(),
DragHandler = new DragHandlerBrowser(),
MenuHandler = new ContextMenuBrowser(this),
JsDialogHandler = new JavaScriptDialogHandler(),
KeyboardHandler = new KeyboardHandlerBrowser(this),
LifeSpanHandler = new LifeSpanHandler(),
RequestHandler = new RequestHandlerBrowser()
};
this.browser = new TweetDeckBrowser(this, plugins, new TweetDeckBridge(this, notification));
this.browser.PageLoaded += browser_PageLoaded;
#if DEBUG
this.browser.ConsoleMessage += BrowserUtils.HandleConsoleMessage;
#endif
this.browser.LoadingStateChanged += browser_LoadingStateChanged;
this.browser.FrameLoadStart += browser_FrameLoadStart;
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.browser.LoadError += browser_LoadError;
this.browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge(this, notification));
this.browser.RegisterAsyncJsObject("$TDP", plugins.Bridge);
browser.BrowserSettings.BackgroundColor = (uint)TwitterUtils.BackgroundColor.ToArgb();
browser.Dock = DockStyle.None;
browser.Location = ControlExtensions.InvisibleLocation;
Controls.Add(browser);
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
Disposed += (sender, args) => {
memoryUsageTracker.Dispose();
browser.Dispose();
contextMenu.Dispose();
notificationScreenshotManager?.Dispose();
soundNotification?.Dispose();
videoPlayer?.Dispose();
analytics?.Dispose();
};
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
@@ -128,10 +90,7 @@ namespace TweetDuck.Core{
UpdateTrayIcon();
Config.MuteToggled += Config_MuteToggled;
Config.ZoomLevelChanged += Config_ZoomLevelChanged;
this.updates = new UpdateHandler(browser, updaterSettings);
this.updates = browser.CreateUpdateHandler(updaterSettings);
this.updates.UpdateAccepted += updates_UpdateAccepted;
this.updates.UpdateDismissed += updates_UpdateDismissed;
@@ -154,13 +113,9 @@ namespace TweetDuck.Core{
Config.BrowserWindow.Restore(this, true);
prevState = WindowState;
isLoaded = true;
}
private void OnBrowserReady(){
if (!isBrowserReady){
browser.Location = Point.Empty;
browser.Dock = DockStyle.Fill;
isBrowserReady = true;
if (Config.AllowDataCollection){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
}
@@ -168,69 +123,7 @@ namespace TweetDuck.Core{
trayIcon.Visible = Config.TrayBehavior.ShouldDisplayIcon();
}
// active event handlers
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
if (!e.IsLoading){
foreach(string word in TwitterUtils.DictionaryWords){
browser.AddWordToDictionary(word);
}
BeginInvoke(new Action(OnBrowserReady));
browser.LoadingStateChanged -= browser_LoadingStateChanged;
}
}
private void browser_FrameLoadStart(object sender, FrameLoadStartEventArgs e){
if (e.Frame.IsMain){
memoryUsageTracker.Stop();
if (Config.ZoomLevel != 100){
BrowserUtils.SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
}
if (TwitterUtils.IsTwitterWebsite(e.Frame)){
ScriptLoader.ExecuteFile(e.Frame, "twitter.js");
}
}
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && TwitterUtils.IsTweetDeckWebsite(e.Frame)){
e.Frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorFix);
UpdateProperties(PropertyBridge.Environment.Browser);
TweetDeckBridge.RestoreSessionData(e.Frame);
ScriptLoader.ExecuteFile(e.Frame, "code.js");
InjectBrowserCSS();
ReinjectCustomCSS(Config.CustomBrowserCSS);
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Browser);
TweetDeckBridge.ResetStaticProperties();
if (Program.SystemConfig.EnableBrowserGCReload){
memoryUsageTracker.Start(this, e.Browser, Program.SystemConfig.BrowserMemoryThreshold);
}
if (Config.FirstRun){
ScriptLoader.ExecuteFile(e.Frame, "introduction.js");
}
}
}
private void browser_LoadError(object sender, LoadErrorEventArgs e){
if (e.ErrorCode == CefErrorCode.Aborted){
return;
}
if (!e.FailedUrl.StartsWith("http://td/", StringComparison.Ordinal)){
string errorPage = ScriptLoader.LoadResource("pages/error.html", true);
if (errorPage != null){
browser.LoadHtml(errorPage.Replace("{err}", BrowserUtils.GetErrorName(e.ErrorCode)), "http://td/error");
}
}
}
// event handlers
private void timerResize_Tick(object sender, EventArgs e){
FormBrowser_ResizeEnd(this, e); // also stops timer
@@ -300,14 +193,6 @@ namespace TweetDuck.Core{
}
}
private void Config_MuteToggled(object sender, EventArgs e){
UpdateProperties(PropertyBridge.Environment.Browser);
}
private void Config_ZoomLevelChanged(object sender, EventArgs e){
BrowserUtils.SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
}
private void Config_TrayBehaviorChanged(object sender, EventArgs e){
UpdateTrayIcon();
}
@@ -323,6 +208,38 @@ namespace TweetDuck.Core{
ForceClose();
}
private void browser_PageLoaded(object sender, EventArgs e){
if (Config.ShowDataCollectionNotification){
this.InvokeAsyncSafe(() => {
if (!Config.FirstRun && Config.AllowDataCollection){
FormMessage form = new FormMessage("Anonymous Data Update", "Hi! You can now review your anonymous data report, and opt-out if you've changed your mind. Collected data will be used to focus development on most commonly used features. If you want to opt-out but still support the project, any feedback and donations are appreciated.", MessageBoxIcon.Information);
form.AddButton("OK", ControlType.Accept | ControlType.Focused);
Button btnReviewSettings = new Button{
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
Font = SystemFonts.MessageBoxFont,
Location = new Point(9, 12),
Margin = new Padding(0, 0, 48, 0),
Size = new Size(160, 26),
Text = "Review Feedback Options",
UseVisualStyleBackColor = true
};
btnReviewSettings.Click += (sender2, args2) => {
form.Close();
OpenSettings(typeof(TabSettingsFeedback));
};
form.AddActionControl(btnReviewSettings);
ShowChildForm(form);
}
Config.ShowDataCollectionNotification = false;
Config.Save();
});
}
}
private 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);
@@ -339,10 +256,6 @@ namespace TweetDuck.Core{
}
}
private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
browser.ExecuteScriptAsync("window.TDPF_setPluginState", e.Plugin, e.IsEnabled);
}
private void updates_UpdateAccepted(object sender, UpdateEventArgs e){
this.InvokeAsyncSafe(() => {
FormManager.CloseAllDialogs();
@@ -400,12 +313,13 @@ namespace TweetDuck.Core{
}
}
if (isBrowserReady && m.Msg == NativeMethods.WM_PARENTNOTIFY && (m.WParam.ToInt32() & 0xFFFF) == NativeMethods.WM_XBUTTONDOWN){
if (browser.Ready && 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);
browser.OnMouseClickExtra(m.WParam);
TriggerAnalyticsEvent(AnalyticsFile.Event.BrowserExtraMouseButton);
}
return;
@@ -414,11 +328,7 @@ namespace TweetDuck.Core{
base.WndProc(ref m);
}
// notification helpers
public FormNotificationMain CreateNotificationForm(bool enableContextMenu){
return new FormNotificationMain(this, plugins, enableContextMenu);
}
// bridge methods
public void PauseNotification(){
notification.PauseNotification();
@@ -428,22 +338,28 @@ namespace TweetDuck.Core{
notification.ResumeNotification();
}
// javascript calls
public void InjectBrowserCSS(){
browser.ExecuteScriptAsync("TDGF_injectBrowserCSS", ScriptLoader.LoadResource("styles/browser.css").TrimEnd());
}
public void ReinjectCustomCSS(string css){
browser.ExecuteScriptAsync("TDGF_reinjectCustomCSS", css?.Replace(Environment.NewLine, " ") ?? string.Empty);
}
public void UpdateProperties(PropertyBridge.Environment environment){
browser.ExecuteScriptAsync(PropertyBridge.GenerateScript(environment));
browser.ReinjectCustomCSS(css);
}
public void ReloadToTweetDeck(){
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUtils.TweetDeckURL}'");
browser.ReloadToTweetDeck();
}
public void TriggerTweetScreenshot(){
browser.TriggerTweetScreenshot();
}
public void ReloadColumns(){
browser.ReloadColumns();
}
public void ApplyROT13(){
browser.ApplyROT13();
}
public void TriggerAnalyticsEvent(AnalyticsFile.Event e){
analytics?.TriggerEvent(e);
}
// callback handlers
@@ -452,7 +368,12 @@ namespace TweetDuck.Core{
if (Config.FirstRun){
Config.FirstRun = false;
Config.AllowDataCollection = allowDataCollection;
Config.ShowDataCollectionNotification = false;
Config.Save();
if (allowDataCollection && analytics == null){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
}
if (showGuide){
@@ -472,7 +393,7 @@ namespace TweetDuck.Core{
if (!FormManager.TryBringToFront<FormSettings>()){
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
FormSettings form = new FormSettings(this, plugins, updates, startTab);
FormSettings form = new FormSettings(this, plugins, updates, analytics, startTab);
form.FormClosed += (sender, args) => {
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
@@ -486,11 +407,16 @@ namespace TweetDuck.Core{
trayIcon.HasNotifications = false;
}
if (Program.SystemConfig.EnableBrowserGCReload){
memoryUsageTracker.Start(this, browser.GetBrowser(), Program.SystemConfig.BrowserMemoryThreshold);
browser.RefreshMemoryTracker();
if (Config.AllowDataCollection){
if (analytics == null){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
}
else{
memoryUsageTracker.Stop();
else if (analytics != null){
analytics.Dispose();
analytics = null;
}
if (form.ShouldReloadBrowser){
@@ -498,25 +424,28 @@ namespace TweetDuck.Core{
plugins.Reload(); // also reloads the browser
}
else{
UpdateProperties(PropertyBridge.Environment.Browser);
browser.UpdateProperties();
}
notification.RequiresResize = true;
form.Dispose();
};
TriggerAnalyticsEvent(AnalyticsFile.Event.OpenOptions);
ShowChildForm(form);
}
}
public void OpenAbout(){
if (!FormManager.TryBringToFront<FormAbout>()){
TriggerAnalyticsEvent(AnalyticsFile.Event.OpenAbout);
ShowChildForm(new FormAbout());
}
}
public void OpenPlugins(){
if (!FormManager.TryBringToFront<FormPlugins>()){
TriggerAnalyticsEvent(AnalyticsFile.Event.OpenPlugins);
ShowChildForm(new FormPlugins(plugins));
}
}
@@ -539,9 +468,11 @@ namespace TweetDuck.Core{
soundNotification.SetVolume(Config.NotificationSoundVolume);
soundNotification.Play(Config.NotificationSoundPath);
TriggerAnalyticsEvent(AnalyticsFile.Event.SoundNotification);
}
public void PlayVideo(string url){
public void PlayVideo(string url, string username){
if (string.IsNullOrEmpty(url)){
videoPlayer?.Close();
return;
@@ -551,16 +482,12 @@ namespace TweetDuck.Core{
videoPlayer = new VideoPlayer(this);
videoPlayer.ProcessExited += (sender, args) => {
browser.GetBrowser().GetHost().SendFocusEvent(true);
HideVideoOverlay();
browser.HideVideoOverlay(true);
};
}
videoPlayer.Launch(url);
}
public void HideVideoOverlay(){
browser.ExecuteScriptAsync("$('#td-video-player-overlay').remove()");
videoPlayer.Launch(url, username);
TriggerAnalyticsEvent(AnalyticsFile.Event.VideoPlay);
}
public bool ProcessBrowserKey(Keys key){
@@ -575,15 +502,14 @@ namespace TweetDuck.Core{
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl){
Activate();
using(IFrame frame = browser.GetBrowser().MainFrame){
if (!TwitterUtils.IsTweetDeckWebsite(frame)){
FormMessage.Error("View Tweet Detail", "TweetDeck is not currently loaded.", FormMessage.OK);
return;
}
if (!browser.IsTweetDeckWebsite){
FormMessage.Error("View Tweet Detail", "TweetDeck is not currently loaded.", FormMessage.OK);
return;
}
notification.FinishCurrentNotification();
browser.ExecuteScriptAsync("window.TDGF_showTweetDetail", columnId, chirpId, fallbackUrl);
browser.ShowTweetDetail(columnId, chirpId, fallbackUrl);
TriggerAnalyticsEvent(AnalyticsFile.Event.TweetDetail);
}
public void OnTweetScreenshotReady(string html, int width, int height){
@@ -592,6 +518,7 @@ namespace TweetDuck.Core{
}
notificationScreenshotManager.Trigger(html, width, height);
TriggerAnalyticsEvent(AnalyticsFile.Event.TweetScreenshot);
}
public void DisplayTooltip(string text){
@@ -604,9 +531,5 @@ namespace TweetDuck.Core{
toolTip.Show(text, this, position);
}
}
public void TriggerTweetScreenshot(){
browser.ExecuteScriptAsync("TDGF_triggerScreenshot()");
}
}
}

View File

@@ -8,6 +8,7 @@ using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using System.Collections.Generic;
using System.Linq;
using TweetDuck.Core.Other;
namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{
@@ -31,10 +32,11 @@ namespace TweetDuck.Core.Handling{
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501;
private const CefMenuCommand MenuCopyUsername = (CefMenuCommand)26502;
private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand)26503;
private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand)26504;
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand)26505;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand)26506;
private const CefMenuCommand MenuViewImage = (CefMenuCommand)26503;
private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand)26504;
private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand)26505;
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand)26506;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand)26507;
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand)26599;
private string[] lastHighlightedTweetAuthors;
@@ -79,6 +81,7 @@ namespace TweetDuck.Core.Handling{
model.AddSeparator();
}
else if ((parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) || hasTweetImage){
model.AddItem(MenuViewImage, "View image in photo viewer");
model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
model.AddItem(MenuSaveMedia, TextSave("image"));
@@ -114,9 +117,35 @@ namespace TweetDuck.Core.Handling{
SetClipboardText(browserControl.AsControl(), TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
break;
case MenuViewImage:
string url = GetMediaLink(parameters);
string file = Path.Combine(BrowserCache.CacheFolder, TwitterUtils.GetImageFileName(url));
void ViewFile(){
string ext = Path.GetExtension(file);
if (TwitterUtils.ValidImageExtensions.Contains(ext)){
WindowsUtils.OpenAssociatedProgram(file);
}
else{
FormMessage.Error("Image Download", "Invalid file extension "+ext, FormMessage.OK);
}
}
if (File.Exists(file)){
ViewFile();
}
else{
BrowserUtils.DownloadFileAsync(TwitterUtils.GetMediaLink(url, ImageQuality), file, ViewFile, ex => {
FormMessage.Error("Image Download", "An error occurred while downloading the image: "+ex.Message, FormMessage.OK);
});
}
break;
case MenuSaveMedia:
if (IsVideo){
TwitterUtils.DownloadVideo(GetMediaLink(parameters));
TwitterUtils.DownloadVideo(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault());
}
else{
TwitterUtils.DownloadImage(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality);

View File

@@ -2,6 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling{
@@ -17,6 +18,7 @@ namespace TweetDuck.Core.Handling{
private const CefMenuCommand MenuOpenQuotedTweetUrl = (CefMenuCommand)26612;
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand)26613;
private const CefMenuCommand MenuScreenshotTweet = (CefMenuCommand)26614;
private const CefMenuCommand MenuInputApplyROT13 = (CefMenuCommand)26615;
private const string TitleReloadBrowser = "Reload browser";
private const string TitleMuteNotifications = "Mute notifications";
@@ -41,6 +43,11 @@ namespace TweetDuck.Core.Handling{
RemoveSeparatorIfLast(model);
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
if (parameters.TypeFlags.HasFlag(ContextMenuType.Editable)){
model.AddSeparator();
model.AddItem(MenuInputApplyROT13, "Apply ROT13");
}
model.AddSeparator();
}
@@ -89,6 +96,8 @@ namespace TweetDuck.Core.Handling{
}
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(() => form.TriggerAnalyticsEvent(AnalyticsFile.Event.BrowserContextMenu));
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
@@ -136,6 +145,10 @@ namespace TweetDuck.Core.Handling{
case MenuCopyQuotedTweetUrl:
SetClipboardText(form, lastHighlightedQuoteUrl);
return true;
case MenuInputApplyROT13:
form.InvokeAsyncSafe(form.ApplyROT13);
return true;
}
return false;
@@ -153,6 +166,7 @@ namespace TweetDuck.Core.Handling{
menu.Popup += (sender, args) => {
menu.MenuItems[1].Checked = Program.UserConfig.MuteNotifications;
form.TriggerAnalyticsEvent(AnalyticsFile.Event.BrowserContextMenu);
};
return menu;

View File

@@ -1,6 +1,7 @@
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuNotification : ContextMenuBase{
@@ -48,13 +49,16 @@ namespace TweetDuck.Core.Handling{
}
if (HasDevTools){
model.AddSeparator();
AddSeparator(model);
AddDebugMenuItems(model);
}
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(() => form.ContextMenuOpen = true);
form.InvokeAsyncSafe(() => {
form.ContextMenuOpen = true;
form.TriggerAnalyticsEvent(AnalyticsFile.Event.NotificationContextMenu);
});
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){

View File

@@ -4,11 +4,18 @@ using CefSharp;
namespace TweetDuck.Core.Handling{
sealed class DragHandlerBrowser : IDragHandler{
public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask){
void TriggerDragStart(string type, string data = null){
browserControl.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", type, data);
}
if (dragData.IsLink){
browserControl.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", "link", dragData.LinkUrl);
TriggerDragStart("link", dragData.LinkUrl);
}
else if (dragData.IsFragment){
TriggerDragStart("text", dragData.FragmentText.Trim());
}
else{
browserControl.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", "unknown");
TriggerDragStart("unknown");
}
return false;

View File

@@ -1,14 +1,24 @@
using System;
using System.Threading.Tasks;
using CefSharp;
namespace TweetDuck.Core.Handling.General{
sealed class BrowserProcessHandler : IBrowserProcessHandler{
void IBrowserProcessHandler.OnContextInitialized(){
public static Task UpdatePrefs(){
return Cef.UIThreadTaskFactory.StartNew(UpdatePrefsInternal);
}
private static void UpdatePrefsInternal(){
using(IRequestContext ctx = Cef.GetGlobalRequestContext()){
ctx.SetPreference("browser.enable_spellchecking", Program.UserConfig.EnableSpellCheck, out string _);
ctx.SetPreference("settings.a11y.animation_policy", Program.UserConfig.EnableAnimatedImages ? "allowed" : "none", out string _);
}
}
void IBrowserProcessHandler.OnContextInitialized(){
UpdatePrefsInternal();
}
void IBrowserProcessHandler.OnScheduleMessagePumpWork(long delay){}
void IDisposable.Dispose(){}
}

View File

@@ -2,6 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Handling {
sealed class KeyboardHandlerNotification : IKeyboardHandler{
@@ -11,19 +12,26 @@ namespace TweetDuck.Core.Handling {
this.notification = notification;
}
private void TriggerKeyboardShortcutAnalytics(){
notification.InvokeAsyncSafe(() => notification.TriggerAnalyticsEvent(AnalyticsFile.Event.NotificationKeyboardShortcut));
}
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){
if (type == KeyType.RawKeyDown && !browser.FocusedFrame.Url.StartsWith("chrome-devtools://")){
switch((Keys)windowsKeyCode){
case Keys.Enter:
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
TriggerKeyboardShortcutAnalytics();
return true;
case Keys.Escape:
notification.InvokeAsyncSafe(() => notification.HideNotification(true));
notification.InvokeAsyncSafe(notification.HideNotification);
TriggerKeyboardShortcutAnalytics();
return true;
case Keys.Space:
notification.InvokeAsyncSafe(() => notification.FreezeTimer = !notification.FreezeTimer);
TriggerKeyboardShortcutAnalytics();
return true;
}
}

View File

@@ -0,0 +1,46 @@
using System.Windows.Forms;
using TweetDuck.Plugins;
using TweetDuck.Resources;
namespace TweetDuck.Core.Notification.Example{
sealed class FormNotificationExample : FormNotificationMain{
public override bool RequiresResize => true;
protected override bool CanDragWindow => Program.UserConfig.NotificationPosition == TweetNotification.Position.Custom;
protected override FormBorderStyle NotificationBorderStyle{
get{
if (Program.UserConfig.NotificationSize == TweetNotification.Size.Custom){
switch(base.NotificationBorderStyle){
case FormBorderStyle.FixedSingle: return FormBorderStyle.Sizable;
case FormBorderStyle.FixedToolWindow: return FormBorderStyle.SizableToolWindow;
}
}
return base.NotificationBorderStyle;
}
}
private readonly TweetNotification exampleNotification;
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false){
string exampleTweetHTML = ScriptLoader.LoadResource("pages/example.html", true);
#if DEBUG
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
#endif
exampleNotification = TweetNotification.Example(exampleTweetHTML, 95);
}
public void ShowExampleNotification(bool reset){
if (reset){
LoadTweet(exampleNotification);
}
else{
PrepareAndDisplayWindow();
}
UpdateTitle();
}
}
}

View File

@@ -8,6 +8,7 @@ using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Management;
using TweetDuck.Core.Utils;
@@ -15,7 +16,7 @@ namespace TweetDuck.Core.Notification{
partial class FormNotificationBase : Form{
protected static int FontSizeLevel{
get{
switch(TweetDeckBridge.FontSizeClass){
switch(TweetDeckBridge.FontSize){
case "largest": return 4;
case "large": return 3;
case "small": return 1;
@@ -25,7 +26,7 @@ namespace TweetDuck.Core.Notification{
}
}
protected Point PrimaryLocation{
protected virtual Point PrimaryLocation{
get{
UserConfig config = Program.UserConfig;
Screen screen;
@@ -66,31 +67,39 @@ namespace TweetDuck.Core.Notification{
}
public bool IsNotificationVisible => Location != ControlExtensions.InvisibleLocation;
protected virtual bool CanDragWindow => true;
public new Point Location{
get => base.Location;
get{
return base.Location;
}
set{
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
FormBorderStyle = GetBorderStyle(CanResizeWindow);
FormBorderStyle = NotificationBorderStyle;
}
}
public bool CanResizeWindow{
get => FormBorderStyle == FormBorderStyle.Sizable || FormBorderStyle == FormBorderStyle.SizableToolWindow;
set => FormBorderStyle = GetBorderStyle(value);
protected virtual FormBorderStyle NotificationBorderStyle{
get{
if (WindowsUtils.ShouldAvoidToolWindow && Visible){ // Visible = workaround for alt+tab
return FormBorderStyle.FixedSingle;
}
else{
return FormBorderStyle.FixedToolWindow;
}
}
}
public Func<bool> CanMoveWindow { get; set; } = () => true;
protected override bool ShowWithoutActivation => true;
protected double SizeScale => dpiScale*Program.UserConfig.ZoomMultiplier;
protected float DpiScale { get; }
protected double SizeScale => DpiScale*Program.UserConfig.ZoomLevel/100.0;
protected readonly FormBrowser owner;
protected readonly ChromiumWebBrowser browser;
private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification();
private readonly float dpiScale;
private TweetNotification currentNotification;
private int pauseCounter;
@@ -120,13 +129,13 @@ namespace TweetDuck.Core.Notification{
this.browser.Dock = DockStyle.None;
this.browser.ClientSize = ClientSize;
this.browser.IsBrowserInitializedChanged += Browser_IsBrowserInitializedChanged;
this.browser.IsBrowserInitializedChanged += browser_IsBrowserInitializedChanged;
#if DEBUG
this.browser.ConsoleMessage += BrowserUtils.HandleConsoleMessage;
#endif
this.dpiScale = this.GetDPIScale();
DpiScale = this.GetDPIScale();
DefaultResourceHandlerFactory handlerFactory = (DefaultResourceHandlerFactory)browser.ResourceHandlerFactory;
handlerFactory.RegisterHandler(TwitterUtils.TweetDeckURL, this.resourceHandler);
@@ -143,20 +152,24 @@ namespace TweetDuck.Core.Notification{
}
protected override void WndProc(ref Message m){
if (m.Msg == 0x0112 && (m.WParam.ToInt32() & 0xFFF0) == 0xF010 && !CanMoveWindow()){ // WM_SYSCOMMAND, SC_MOVE
if (m.Msg == 0x0112 && (m.WParam.ToInt32() & 0xFFF0) == 0xF010 && !CanDragWindow){ // WM_SYSCOMMAND, SC_MOVE
return;
}
base.WndProc(ref m);
}
public void TriggerAnalyticsEvent(AnalyticsFile.Event e){
owner.TriggerAnalyticsEvent(e);
}
// event handlers
private void owner_FormClosed(object sender, FormClosedEventArgs e){
Close();
}
private void Browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e){
private void browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e){
if (e.IsBrowserInitialized){
Initialized?.Invoke(this, EventArgs.Empty);
@@ -167,11 +180,8 @@ namespace TweetDuck.Core.Notification{
// notification methods
public virtual void HideNotification(bool loadBlank){
if (loadBlank){
browser.Load("about:blank");
}
public virtual void HideNotification(){
browser.Load("about:blank");
Location = ControlExtensions.InvisibleLocation;
currentNotification = null;
}
@@ -233,14 +243,5 @@ namespace TweetDuck.Core.Notification{
toolTip.Show(text, this, position);
}
}
private FormBorderStyle GetBorderStyle(bool sizable){
if (WindowsUtils.ShouldAvoidToolWindow && Visible){ // Visible = workaround for alt+tab
return sizable ? FormBorderStyle.Sizable : FormBorderStyle.FixedSingle;
}
else{
return sizable ? FormBorderStyle.SizableToolWindow : FormBorderStyle.FixedToolWindow;
}
}
}
}

View File

@@ -49,7 +49,7 @@
this.progressBarTimer.Margin = new System.Windows.Forms.Padding(0);
this.progressBarTimer.Maximum = 1000;
this.progressBarTimer.Name = "progressBarTimer";
this.progressBarTimer.Size = new System.Drawing.Size(284, TimerBarHeight);
this.progressBarTimer.Size = new System.Drawing.Size(284, 4);
this.progressBarTimer.TabIndex = 1;
//
// FormNotification

View File

@@ -5,6 +5,7 @@ using System.Windows.Forms;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins;
@@ -12,14 +13,14 @@ using TweetDuck.Plugins.Enums;
using TweetDuck.Resources;
namespace TweetDuck.Core.Notification{
partial class FormNotificationMain : FormNotificationBase{
abstract partial class FormNotificationMain : FormNotificationBase{
private const string NotificationScriptFile = "notification.js";
private const int TimerBarHeight = 4;
private static readonly string NotificationScriptIdentifier = ScriptLoader.GetRootIdentifier(NotificationScriptFile);
private static readonly string NotificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
private readonly PluginManager plugins;
private readonly int timerBarHeight;
protected int timeLeft, totalTime;
protected bool pausedDuringNotification;
@@ -31,9 +32,9 @@ namespace TweetDuck.Core.Notification{
private bool? prevDisplayTimer;
private int? prevFontSize;
public bool RequiresResize{
public virtual bool RequiresResize{
get{
return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Program.UserConfig.DisplayNotificationTimer || prevFontSize != FontSizeLevel || CanResizeWindow;
return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Program.UserConfig.DisplayNotificationTimer || prevFontSize != FontSizeLevel;
}
set{
@@ -72,14 +73,13 @@ namespace TweetDuck.Core.Notification{
}
}
public Size BrowserSize{
get => Program.UserConfig.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height-TimerBarHeight) : ClientSize;
}
public Size BrowserSize => Program.UserConfig.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height-timerBarHeight) : ClientSize;
public FormNotificationMain(FormBrowser owner, PluginManager pluginManager, bool enableContextMenu) : base(owner, enableContextMenu){
protected FormNotificationMain(FormBrowser owner, PluginManager pluginManager, bool enableContextMenu) : base(owner, enableContextMenu){
InitializeComponent();
this.plugins = pluginManager;
this.timerBarHeight = BrowserUtils.Scale(4, DpiScale);
browser.KeyboardHandler = new KeyboardHandlerNotification(this);
@@ -128,6 +128,7 @@ namespace TweetDuck.Core.Notification{
}
blockXButtonUp = true;
this.InvokeAsyncSafe(() => TriggerAnalyticsEvent(AnalyticsFile.Event.NotificationExtraMouseButton));
return NativeMethods.HOOK_HANDLED;
}
else if (eventType == NativeMethods.WM_XBUTTONUP && blockXButtonUp){
@@ -148,7 +149,7 @@ namespace TweetDuck.Core.Notification{
private void FormNotification_FormClosing(object sender, FormClosingEventArgs e){
if (e.CloseReason == CloseReason.UserClosing){
HideNotification(true);
HideNotification();
e.Cancel = true;
}
}
@@ -176,12 +177,14 @@ namespace TweetDuck.Core.Notification{
}
private void timerHideProgress_Tick(object sender, EventArgs e){
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen)return;
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen){
return;
}
timeLeft -= timerProgress.Interval;
int value = BrowserUtils.Scale(1025, (totalTime-timeLeft)/(double)totalTime);
progressBarTimer.SetValueInstant(Math.Min(1000, Math.Max(0, Program.UserConfig.NotificationTimerCountDown ? 1000-value : value)));
int value = BrowserUtils.Scale(progressBarTimer.Maximum+25, (totalTime-timeLeft)/(double)totalTime);
progressBarTimer.SetValueInstant(Program.UserConfig.NotificationTimerCountDown ? progressBarTimer.Maximum-value : value);
if (timeLeft <= 0){
FinishCurrentNotification();
@@ -194,21 +197,10 @@ namespace TweetDuck.Core.Notification{
LoadTweet(notification);
}
public void ShowNotificationForSettings(bool reset){
if (reset){
LoadTweet(TweetNotification.ExampleTweet);
}
else{
PrepareAndDisplayWindow();
}
public override void HideNotification(){
base.HideNotification();
UpdateTitle();
}
public override void HideNotification(bool loadBlank){
base.HideNotification(loadBlank);
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? 1000 : 0;
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum;
timerProgress.Stop();
totalTime = 0;
@@ -251,14 +243,14 @@ namespace TweetDuck.Core.Notification{
protected override void LoadTweet(TweetNotification tweet){
timerProgress.Stop();
totalTime = timeLeft = tweet.GetDisplayDuration(Program.UserConfig.NotificationDurationValue);
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? 1000 : 0;
progressBarTimer.Value = Program.UserConfig.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum;
base.LoadTweet(tweet);
}
protected override void SetNotificationSize(int width, int height){
if (Program.UserConfig.DisplayNotificationTimer){
ClientSize = new Size(width, height+TimerBarHeight);
ClientSize = new Size(width, height+timerBarHeight);
progressBarTimer.Visible = true;
}
else{
@@ -269,7 +261,7 @@ namespace TweetDuck.Core.Notification{
browser.ClientSize = new Size(width, height);
}
private void PrepareAndDisplayWindow(){
protected void PrepareAndDisplayWindow(){
if (RequiresResize){
RequiresResize = false;
SetNotificationSize(BaseClientWidth, BaseClientHeight);

View File

@@ -40,7 +40,6 @@ namespace TweetDuck.Core.Notification {
//
// FormNotificationTweet
//
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormNotificationTweet_FormClosing);
this.ResumeLayout(true);
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Drawing;
using TweetDuck.Plugins;
using System.Windows.Forms;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Notification{
@@ -10,10 +11,24 @@ namespace TweetDuck.Core.Notification{
private const int NonIntrusiveIdleLimit = 30;
private const int TrimMinimum = 32;
protected override Point PrimaryLocation => hasTemporarilyMoved && IsNotificationVisible ? Location : base.PrimaryLocation;
private bool IsCursorOverNotificationArea => new Rectangle(PrimaryLocation, Size).Contains(Cursor.Position);
protected override bool CanDragWindow{
get{
if (ModifierKeys.HasFlag(Keys.Alt)){
hasTemporarilyMoved = true;
return true;
}
else{
return false;
}
}
}
private readonly Queue<TweetNotification> tweetQueue = new Queue<TweetNotification>(4);
private bool needsTrim;
private bool hasTemporarilyMoved;
public FormNotificationTweet(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, true){
InitializeComponent();
@@ -26,18 +41,18 @@ namespace TweetDuck.Core.Notification{
}
}
private void FormNotificationTweet_FormClosing(object sender, FormClosingEventArgs e){
if (e.CloseReason == CloseReason.UserClosing){
tweetQueue.Clear(); // already canceled
TrimQueue();
}
}
protected override void WndProc(ref Message m){
if (m.Msg == 0x00A7){ // WM_NCMBUTTONDOWN
int hitTest = m.WParam.ToInt32();
private void TrimQueue(){
if (needsTrim){
tweetQueue.TrimExcess();
needsTrim = false;
if (hitTest == 2 || hitTest == 20){ // HTCAPTION, HTCLOSE
hasTemporarilyMoved = false;
MoveToVisibleLocation();
return;
}
}
base.WndProc(ref m);
}
// event handlers
@@ -68,11 +83,9 @@ namespace TweetDuck.Core.Notification{
// notification methods
public override void ShowNotification(TweetNotification notification){
if (IsPaused){
tweetQueue.Enqueue(notification);
}
else{
tweetQueue.Enqueue(notification);
tweetQueue.Enqueue(notification);
if (!IsPaused){
UpdateTitle();
if (totalTime == 0){
@@ -81,6 +94,19 @@ namespace TweetDuck.Core.Notification{
}
needsTrim |= tweetQueue.Count >= TrimMinimum;
TriggerAnalyticsEvent(AnalyticsFile.Event.DesktopNotification);
}
public override void HideNotification(){
base.HideNotification();
tweetQueue.Clear();
if (needsTrim){
tweetQueue.TrimExcess();
needsTrim = false;
}
hasTemporarilyMoved = false;
}
public override void FinishCurrentNotification(){
@@ -88,8 +114,7 @@ namespace TweetDuck.Core.Notification{
LoadNextNotification();
}
else{
HideNotification(true);
TrimQueue();
HideNotification();
}
}

View File

@@ -12,6 +12,8 @@ using TweetDuck.Resources;
namespace TweetDuck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotificationBase{
protected override bool CanDragWindow => false;
private readonly PluginManager plugins;
public FormNotificationScreenshotable(Action callback, FormBrowser owner, PluginManager pluginManager) : base(owner, false){
@@ -22,7 +24,7 @@ namespace TweetDuck.Core.Notification.Screenshot{
browser.LoadingStateChanged += (sender, args) => {
if (!args.IsLoading){
using(IFrame frame = args.Browser.MainFrame){
ScriptLoader.ExecuteScript(frame, "window.setTimeout($TD_NotificationScreenshot.trigger, 129)", "gen:screenshot");
ScriptLoader.ExecuteScript(frame, "window.setTimeout($TD_NotificationScreenshot.trigger, document.getElementsByTagName('iframe').length ? 267 : 67)", "gen:screenshot");
}
}
};

View File

@@ -42,10 +42,7 @@ namespace TweetDuck.Core.Notification.Screenshot{
return;
}
screenshot = new FormNotificationScreenshotable(Callback, owner, plugins){
CanMoveWindow = () => false
};
screenshot = new FormNotificationScreenshotable(Callback, owner, plugins);
screenshot.LoadNotificationForScreenshot(new TweetNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty), width, height);
screenshot.Show();
timeout.Start();

View File

@@ -5,25 +5,11 @@ using TweetDuck.Resources;
namespace TweetDuck.Core.Notification{
sealed class TweetNotification{
private const string DefaultFontSizeClass = "medium";
private const string DefaultHeadContents = @"<meta charset='utf-8'><meta http-equiv='X-UA-Compatible' content='chrome=1'><link rel='stylesheet' href='https://ton.twimg.com/tweetdeck-web/web/css/font.5ef884f9f9.css'><link rel='stylesheet' href='https://ton.twimg.com/tweetdeck-web/web/css/app-dark.5631e0dd42.css'><style type='text/css'>body{background:#222426}</style>";
private const string DefaultHeadLayout = @"<html id='tduck' class='os-windows txt-size--14' data-td-font='medium' data-td-theme='dark'><head><meta charset='utf-8'><meta http-equiv='X-UA-Compatible' content='chrome=1'><link rel='stylesheet' href='https://ton.twimg.com/tweetdeck-web/web/css/font.5ef884f9f9.css'><link rel='stylesheet' href='https://ton.twimg.com/tweetdeck-web/web/css/app-dark.5631e0dd42.css'><style type='text/css'>body{background:#222426}</style>";
private static readonly string CustomCSS = ScriptLoader.LoadResource("styles/notification.css");
private static string ExampleTweetHTML;
public static TweetNotification ExampleTweet{
get{
if (ExampleTweetHTML == null){
ExampleTweetHTML = ScriptLoader.LoadResource("pages/example.html", true);
#if DEBUG
ExampleTweetHTML = ExampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
#endif
}
return new TweetNotification(string.Empty, string.Empty, "Home", ExampleTweetHTML, 95, string.Empty, string.Empty, true);
}
public static TweetNotification Example(string html, int characters){
return new TweetNotification(string.Empty, string.Empty, "Home", html, characters, string.Empty, string.Empty, true);
}
public enum Position{
@@ -67,8 +53,7 @@ namespace TweetDuck.Core.Notification{
public string GenerateHtml(string bodyClasses = null, bool enableCustomCSS = true){
StringBuilder build = new StringBuilder();
build.Append("<!DOCTYPE html>");
build.Append("<html class='os-windows txt-base-").Append(TweetDeckBridge.FontSizeClass ?? DefaultFontSizeClass).Append("'>");
build.Append("<head>").Append(TweetDeckBridge.NotificationHeadContents ?? DefaultHeadContents);
build.Append(TweetDeckBridge.NotificationHeadLayout ?? DefaultHeadLayout);
if (enableCustomCSS){
build.Append("<style type='text/css'>").Append(CustomCSS).Append("</style>");
@@ -85,7 +70,7 @@ namespace TweetDuck.Core.Notification{
build.Append(' ').Append(bodyClasses);
}
build.Append('\'').Append(isExample ? " td-example-notification" : "").Append("><div class='column' style='width:100%;height:auto;overflow:initial;'>");
build.Append('\'').Append(isExample ? " td-example-notification" : "").Append("><div class='column' style='width:100%!important;height:auto!important;overflow:initial!important;'>");
build.Append(html);
build.Append("</div></body>");
build.Append("</html>");

View File

@@ -0,0 +1,101 @@
using System;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Core.Other.Analytics{
sealed class AnalyticsFile{
private static readonly FileSerializer<AnalyticsFile> Serializer = new FileSerializer<AnalyticsFile>();
static AnalyticsFile(){
Serializer.RegisterTypeConverter(typeof(DateTime), new SingleTypeConverter<DateTime>{
ConvertToString = value => value.ToBinary().ToString(),
ConvertToObject = value => DateTime.FromBinary(long.Parse(value))
});
}
public enum Event{
OpenOptions, OpenPlugins, OpenAbout, OpenGuide,
DesktopNotification, SoundNotification,
BrowserContextMenu, BrowserExtraMouseButton,
NotificationContextMenu, NotificationExtraMouseButton, NotificationKeyboardShortcut,
TweetScreenshot, TweetDetail, VideoPlay, GCReload
}
// STATE PROPERTIES
public DateTime LastDataCollection { get; set; } = DateTime.MinValue;
public string LastCollectionVersion { get; set; } = null;
public string LastCollectionMessage { get; set; } = null;
// USAGE DATA
public int CountOpenOptions { get; private set; } = 0;
public int CountOpenPlugins { get; private set; } = 0;
public int CountOpenAbout { get; private set; } = 0;
public int CountOpenGuide { get; private set; } = 0;
public int CountDesktopNotifications { get; private set; } = 0;
public int CountSoundNotifications { get; private set; } = 0;
public int CountBrowserContextMenus { get; private set; } = 0;
public int CountBrowserExtraMouseButtons { get; private set; } = 0;
public int CountNotificationContextMenus { get; private set; } = 0;
public int CountNotificationExtraMouseButtons { get; private set; } = 0;
public int CountNotificationKeyboardShortcuts { get; private set; } = 0;
public int CountTweetScreenshots { get; private set; } = 0;
public int CountTweetDetails { get; private set; } = 0;
public int CountVideoPlays { get; private set; } = 0;
public int CountGCReloads { get; private set; } = 0;
// END OF DATA
private readonly string file;
private AnalyticsFile(string file){
this.file = file;
}
public void TriggerEvent(Event e){
switch(e){
case Event.OpenOptions: ++CountOpenOptions; break;
case Event.OpenPlugins: ++CountOpenPlugins; break;
case Event.OpenAbout: ++CountOpenAbout; break;
case Event.OpenGuide: ++CountOpenGuide; break;
case Event.DesktopNotification: ++CountDesktopNotifications; break;
case Event.SoundNotification: ++CountSoundNotifications; break;
case Event.BrowserContextMenu: ++CountBrowserContextMenus; break;
case Event.BrowserExtraMouseButton: ++CountBrowserExtraMouseButtons; break;
case Event.NotificationContextMenu: ++CountNotificationContextMenus; break;
case Event.NotificationExtraMouseButton: ++CountNotificationExtraMouseButtons; break;
case Event.NotificationKeyboardShortcut: ++CountNotificationKeyboardShortcuts; break;
case Event.TweetScreenshot: ++CountTweetScreenshots; break;
case Event.TweetDetail: ++CountTweetDetails; break;
case Event.VideoPlay: ++CountVideoPlays; break;
case Event.GCReload: ++CountGCReloads; break;
}
}
public void Save(){
try{
Serializer.Write(file, this);
}catch(Exception e){
Program.Reporter.HandleException("Analytics File Error", "Could not save the analytics file.", true, e);
}
}
public static AnalyticsFile Load(string file){
AnalyticsFile config = new AnalyticsFile(file);
try{
Serializer.ReadIfExists(file, config);
}catch(Exception e){
Program.Reporter.HandleException("Analytics File Error", "Could not open the analytics file.", true, e);
}
return config;
}
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.Net;
using System.Threading.Tasks;
using System.Timers;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other.Analytics{
sealed class AnalyticsManager : IDisposable{
private static readonly TimeSpan CollectionInterval = TimeSpan.FromDays(7);
private static readonly Uri CollectionUrl = new Uri("https://tweetduck.chylex.com/breadcrumb/report");
public AnalyticsFile File { get; }
private readonly FormBrowser browser;
private readonly PluginManager plugins;
private readonly Timer currentTimer, saveTimer;
public AnalyticsManager(FormBrowser browser, PluginManager plugins, string file){
this.browser = browser;
this.plugins = plugins;
this.File = AnalyticsFile.Load(file);
this.currentTimer = new Timer{ SynchronizingObject = browser };
this.currentTimer.Elapsed += currentTimer_Elapsed;
this.saveTimer = new Timer{ SynchronizingObject = browser, Interval = 60000 };
this.saveTimer.Elapsed += saveTimer_Elapsed;
if (this.File.LastCollectionVersion != Program.VersionTag){
ScheduleReportIn(TimeSpan.FromHours(12), string.Empty);
}
else{
RestartTimer();
}
}
public void Dispose(){
if (saveTimer.Enabled){
File.Save();
}
currentTimer.Dispose();
saveTimer.Dispose();
}
public void TriggerEvent(AnalyticsFile.Event e){
File.TriggerEvent(e);
saveTimer.Enabled = true;
}
private void saveTimer_Elapsed(object sender, ElapsedEventArgs e){
saveTimer.Stop();
File.Save();
}
private void ScheduleReportIn(TimeSpan delay, string message = null){
SetLastDataCollectionTime(DateTime.Now.Subtract(CollectionInterval).Add(delay), message);
}
private void SetLastDataCollectionTime(DateTime dt, string message = null){
File.LastDataCollection = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
File.LastCollectionVersion = Program.VersionTag;
File.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture);
File.Save();
RestartTimer();
}
private void RestartTimer(){
TimeSpan diff = DateTime.Now.Subtract(File.LastDataCollection);
int minutesTillNext = (int)(CollectionInterval.TotalMinutes-Math.Floor(diff.TotalMinutes));
currentTimer.Interval = Math.Max(minutesTillNext, 1)*60000;
currentTimer.Start();
}
private void currentTimer_Elapsed(object sender, ElapsedEventArgs e){
currentTimer.Stop();
TimeSpan diff = DateTime.Now.Subtract(File.LastDataCollection);
if (Math.Floor(diff.TotalMinutes) >= CollectionInterval.TotalMinutes){
SendReport();
}
else{
RestartTimer();
}
}
private void SendReport(){
AnalyticsReportGenerator.ExternalInfo info = AnalyticsReportGenerator.ExternalInfo.From(browser);
Task.Factory.StartNew(() => {
AnalyticsReport report = AnalyticsReportGenerator.Create(File, info, plugins);
BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
}).ContinueWith(task => browser.InvokeAsyncSafe(() => {
if (task.Status == TaskStatus.RanToCompletion){
SetLastDataCollectionTime(DateTime.Now);
}
else if (task.Exception != null){
string message = null;
if (task.Exception.InnerException is WebException e){
switch(e.Status){
case WebExceptionStatus.ConnectFailure:
message = "Connection Error";
break;
case WebExceptionStatus.NameResolutionFailure:
message = "DNS Error";
break;
case WebExceptionStatus.ProtocolError:
HttpWebResponse response = e.Response as HttpWebResponse;
message = "HTTP Error "+(response != null ? $"{(int)response.StatusCode} ({response.StatusDescription})" : "(unknown code)");
break;
}
}
ScheduleReportIn(TimeSpan.FromHours(4), message ?? "Error: "+(task.Exception.InnerException?.Message ?? task.Exception.Message));
}
}));
}
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections;
using System.Collections.Specialized;
using System.Text;
namespace TweetDuck.Core.Other.Analytics{
sealed class AnalyticsReport : IEnumerable{
private OrderedDictionary data = new OrderedDictionary(32);
private int separators;
public void Add(int ignored){ // adding separators to pretty print
data.Add((++separators).ToString(), null);
}
public void Add(string key, string value){
data.Add(key, value);
}
public AnalyticsReport FinalizeReport(){
if (!data.IsReadOnly){
data = data.AsReadOnly();
}
return this;
}
public IEnumerator GetEnumerator(){
return data.GetEnumerator();
}
public NameValueCollection ToNameValueCollection(){
NameValueCollection collection = new NameValueCollection();
foreach(DictionaryEntry entry in data){
if (entry.Value != null){
collection.Add(((string)entry.Key).ToLower().Replace(' ', '_'), (string)entry.Value);
}
}
return collection;
}
public override string ToString(){
StringBuilder build = new StringBuilder();
foreach(DictionaryEntry entry in data){
if (entry.Value == null){
build.AppendLine();
}
else{
build.AppendLine(entry.Key+": "+entry.Value);
}
}
return build.ToString();
}
}
}

View File

@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Microsoft.Win32;
using TweetDuck.Configuration;
using System.Linq;
using System.Management;
using System.Text.RegularExpressions;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other.Analytics{
static class AnalyticsReportGenerator{
public static AnalyticsReport Create(AnalyticsFile file, ExternalInfo info, PluginManager plugins){
Dictionary<string, string> editLayoutDesign = EditLayoutDesignPluginData;
return new AnalyticsReport{
{ "App Version" , Program.VersionTag },
{ "App Type" , Program.IsPortable ? "portable" : "installed" },
0,
{ "System Name" , SystemName },
{ "System Edition" , SystemEdition },
{ "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" },
{ "System Build" , SystemBuild },
{ "System Locale" , Program.Culture.Name.ToLower() },
0,
{ "RAM" , Exact(RamSize) },
{ "GPU" , GpuVendor },
0,
{ "Screen Count" , Exact(Screen.AllScreens.Length) },
{ "Screen Resolution" , info.Resolution ?? "(unknown)" },
{ "Screen DPI" , info.DPI != null ? Exact(info.DPI.Value) : "(unknown)" },
0,
{ "Hardware Acceleration" , Bool(SysConfig.HardwareAcceleration) },
{ "Browser GC Reload" , Bool(SysConfig.EnableBrowserGCReload) },
{ "Browser GC Threshold" , Exact(SysConfig.BrowserMemoryThreshold) },
0,
{ "Expand Links" , Bool(UserConfig.ExpandLinksOnHover) },
{ "Switch Account Selectors" , Bool(UserConfig.SwitchAccountSelectors) },
{ "Search In First Column" , Bool(UserConfig.OpenSearchInFirstColumn) },
{ "Best Image Quality" , Bool(UserConfig.BestImageQuality) },
{ "Spell Check" , Bool(UserConfig.EnableSpellCheck) },
{ "Zoom" , Exact(UserConfig.ZoomLevel) },
0,
{ "Updates" , Bool(UserConfig.EnableUpdateCheck) },
{ "Update Dismissed" , Bool(!string.IsNullOrEmpty(UserConfig.DismissedUpdate)) },
0,
{ "Tray" , TrayMode },
{ "Tray Highlight" , Bool(UserConfig.EnableTrayHighlight) },
0,
{ "Notification Position" , NotificationPosition },
{ "Notification Size" , NotificationSize },
{ "Notification Timer" , NotificationTimer },
{ "Notification Timer Speed" , RoundUp(UserConfig.NotificationDurationValue, 5) },
{ "Notification Scroll Speed" , Exact(UserConfig.NotificationScrollSpeed) },
{ "Notification Column Title" , Bool(UserConfig.DisplayNotificationColumn) },
{ "Notification Media Previews" , Bool(UserConfig.NotificationMediaPreviews) },
{ "Notification Link Skip" , Bool(UserConfig.NotificationSkipOnLinkClick) },
{ "Notification Non-Intrusive" , Bool(UserConfig.NotificationNonIntrusiveMode) },
{ "Notification Idle Pause" , Exact(UserConfig.NotificationIdlePauseSeconds) },
{ "Custom Sound Notification" , string.IsNullOrEmpty(UserConfig.NotificationSoundPath) ? "off" : Path.GetExtension(UserConfig.NotificationSoundPath) },
0,
{ "Program Arguments" , List(ProgramArguments) },
{ "Custom CEF Arguments" , RoundUp((UserConfig.CustomCefArgs ?? string.Empty).Length, 10) },
{ "Custom Browser CSS" , RoundUp((UserConfig.CustomBrowserCSS ?? string.Empty).Length, 50) },
{ "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
0,
{ "Plugins All" , List(plugins.Plugins.Select(plugin => plugin.Identifier)) },
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugin => plugins.Config.IsEnabled(plugin)).Select(plugin => plugin.Identifier)) },
0,
{ "Theme" , Dict(editLayoutDesign, "_theme", "light/def") },
{ "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") },
{ "Font Size" , Dict(editLayoutDesign, "fontSize", "12px/def") },
{ "Large Quote Font" , Dict(editLayoutDesign, "increaseQuoteTextSize", "false/def") },
{ "Small Compose Font" , Dict(editLayoutDesign, "smallComposeTextSize", "false/def") },
{ "Avatar Radius" , Dict(editLayoutDesign, "avatarRadius", "2/def") },
{ "Hide Tweet Actions" , Dict(editLayoutDesign, "hideTweetActions", "true/def") },
{ "Move Tweet Actions" , Dict(editLayoutDesign, "moveTweetActionsToRight", "true/def") },
{ "Theme Color Tweaks" , Dict(editLayoutDesign, "themeColorTweaks", "true/def") },
{ "Revert Icons" , Dict(editLayoutDesign, "revertIcons", "true/def") },
{ "Optimize Animations" , Dict(editLayoutDesign, "optimizeAnimations", "true/def") },
{ "Reply Account Mode" , ReplyAccountConfigFromPlugin },
{ "Template Count" , Exact(TemplateCountFromPlugin) },
0,
{ "Opened Options" , LogRound(file.CountOpenOptions, 4) },
{ "Opened Plugins" , LogRound(file.CountOpenPlugins, 4) },
{ "Opened About" , LogRound(file.CountOpenAbout, 4) },
{ "Opened Guide" , LogRound(file.CountOpenGuide, 4) },
{ "Desktop Notifications" , LogRound(file.CountDesktopNotifications, 5) },
{ "Sound Notifications" , LogRound(file.CountSoundNotifications, 5) },
{ "Browser Context Menus" , LogRound(file.CountBrowserContextMenus, 2) },
{ "Browser Extra Mouse Buttons" , LogRound(file.CountBrowserExtraMouseButtons, 2) },
{ "Notification Context Menus" , LogRound(file.CountNotificationContextMenus, 2) },
{ "Notification Extra Mouse Buttons" , LogRound(file.CountNotificationExtraMouseButtons, 2) },
{ "Notification Keyboard Shortcuts" , LogRound(file.CountNotificationKeyboardShortcuts, 2) },
{ "Tweet Screenshots" , LogRound(file.CountTweetScreenshots, 2) },
{ "Tweet Details" , LogRound(file.CountTweetDetails, 2) },
{ "Video Plays" , LogRound(file.CountVideoPlays, 4) },
{ "GC Reloads" , LogRound(file.CountGCReloads, 4) }
}.FinalizeReport();
}
private static UserConfig UserConfig => Program.UserConfig;
private static SystemConfig SysConfig => Program.SystemConfig;
private static string Bool(bool value) => value ? "on" : "off";
private static string Exact(int value) => value.ToString();
private static string RoundUp(int value, int multiple) => (multiple*(int)Math.Ceiling((double)value/multiple)).ToString();
private static string LogRound(int value, int logBase) => (value <= 0 ? 0 : (int)Math.Pow(logBase, Math.Floor(Math.Log(value, logBase)))).ToString();
private static string Dict(Dictionary<string, string> dict, string key, string def = "(unknown)") => dict.TryGetValue(key, out string value) ? value : def;
private static string List(IEnumerable<string> list) => string.Join("|", list.DefaultIfEmpty("(none)"));
private static string SystemName { get; }
private static string SystemEdition { get; }
private static string SystemBuild { get; }
private static int RamSize { get; }
private static string GpuVendor { get; }
private static string[] ProgramArguments { get; }
static AnalyticsReportGenerator(){
string osName, osEdition, osBuild;
try{
using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false)){
// ReSharper disable once PossibleNullReferenceException
osName = key.GetValue("ProductName") as string;
osEdition = key.GetValue("EditionID") as string;
osBuild = key.GetValue("CurrentBuild") as string;
if (osName != null && osEdition != null){
osName = osName.Replace(osEdition, "").TrimEnd();
}
}
}catch{
osName = osEdition = osBuild = null;
}
SystemName = osName ?? "Windows (unknown)";
SystemEdition = osEdition ?? "(unknown)";
SystemBuild = osBuild ?? "(unknown)";
try{
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory")){
foreach(ManagementBaseObject obj in searcher.Get()){
RamSize += (int)((ulong)obj["Capacity"]/(1024L*1024L));
}
}
}catch{
RamSize = 0;
}
string gpu = null;
try{
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController")){
foreach(ManagementBaseObject obj in searcher.Get()){
string vendor = obj["Caption"] as string;
if (!string.IsNullOrEmpty(vendor)){
gpu = vendor;
}
}
}
}catch{
// rip
}
GpuVendor = gpu ?? "(unknown)";
Dictionary<string, string> args = new Dictionary<string, string>();
Arguments.GetCurrentClean().ToDictionary(args);
ProgramArguments = args.Keys.Select(key => key.TrimStart('-')).ToArray();
}
private static string TrayMode{
get{
switch(UserConfig.TrayBehavior){
case TrayIcon.Behavior.DisplayOnly: return "icon";
case TrayIcon.Behavior.MinimizeToTray: return "minimize";
case TrayIcon.Behavior.CloseToTray: return "close";
case TrayIcon.Behavior.Combined: return "combined";
default: return "off";
}
}
}
private static string NotificationPosition{
get{
switch(UserConfig.NotificationPosition){
case TweetNotification.Position.TopLeft: return "top left";
case TweetNotification.Position.TopRight: return "top right";
case TweetNotification.Position.BottomLeft: return "bottom left";
case TweetNotification.Position.BottomRight: return "bottom right";
default: return "custom";
}
}
}
private static string NotificationSize{
get{
switch(UserConfig.NotificationSize){
case TweetNotification.Size.Auto: return "auto";
default: return RoundUp(UserConfig.CustomNotificationSize.Width, 20)+"x"+RoundUp(UserConfig.CustomNotificationSize.Height, 20);
}
}
}
private static string NotificationTimer{
get{
if (!UserConfig.DisplayNotificationTimer){
return "off";
}
else{
return UserConfig.NotificationTimerCountDown ? "count down" : "count up";
}
}
}
private static Dictionary<string, string> EditLayoutDesignPluginData{
get{
Dictionary<string, string> dict = new Dictionary<string, string>();
try{
string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "edit-design", "config.json"));
foreach(Match match in Regex.Matches(data, "\"(\\w+?)\":(.*?)[,}]")){
dict[match.Groups[1].Value] = match.Groups[2].Value.Trim('"');
}
}catch{
// rip
}
return dict;
}
}
private static int TemplateCountFromPlugin{
get{
try{
string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "templates", "config.json"));
return Math.Min(StringUtils.CountOccurrences(data, "{\"name\":"), StringUtils.CountOccurrences(data, ",\"contents\":"));
}catch{
return 0;
}
}
}
private static string ReplyAccountConfigFromPlugin{
get{
try{
string data = File.ReadAllText(Path.Combine(Program.PluginDataPath, "official", "reply-account", "configuration.js")).Replace(" ", "");
Match matchType = Regex.Match(data, "defaultAccount:\"([#@])(.*?)\"(?:,|$)");
Match matchAdvanced = Regex.Match(data, "useAdvancedSelector:(.*?)(?:,|$)", RegexOptions.Multiline);
if (!matchType.Success){
return "(unknown)";
}
string accType = matchType.Groups[1].Value == "#" ? matchType.Groups[2].Value : "account";
return matchAdvanced.Success && !matchAdvanced.Value.Contains("false") ? "advanced/"+accType : accType;
}catch{
return "(unknown)";
}
}
}
public class ExternalInfo{
public static ExternalInfo From(Form form){
if (form == null){
return new ExternalInfo();
}
else{
Screen screen = Screen.FromControl(form);
int dpi;
using(Graphics graphics = form.CreateGraphics()){
dpi = (int)graphics.DpiY;
}
return new ExternalInfo{
Resolution = screen.Bounds.Width+"x"+screen.Bounds.Height,
DPI = dpi
};
}
}
public string Resolution { get; private set; }
public int? DPI { get; private set; }
private ExternalInfo(){}
}
}
}

View File

@@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Other{
@@ -21,7 +20,7 @@ namespace TweetDuck.Core.Other{
}
private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe(e.Link.LinkData as string);
BrowserUtils.OpenExternalBrowser(e.Link.LinkData as string);
}
private void FormAbout_HelpRequested(object sender, HelpEventArgs hlpevent){

View File

@@ -6,11 +6,12 @@ using CefSharp.WinForms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Other{
sealed partial class FormGuide : Form{
private const string GuideUrl = "https://tweetduck.chylex.com/guide/v1/";
private const string GuideUrl = "https://tweetduck.chylex.com/guide/v2/";
private readonly ChromiumWebBrowser browser;
@@ -24,6 +25,8 @@ namespace TweetDuck.Core.Other{
if (owner != null){
Size = new Size(owner.Size.Width*3/4, owner.Size.Height*3/4);
VisibleChanged += (sender, args) => this.MoveToCenter(owner);
owner.TriggerAnalyticsEvent(AnalyticsFile.Event.OpenGuide);
}
this.browser = new ChromiumWebBrowser(GuideUrl){

View File

@@ -35,8 +35,8 @@ namespace TweetDuck.Core.Other{
}
private void ReloadPluginList(){
flowLayoutPlugins.SuspendLayout();
flowLayoutPlugins.Controls.Clear();
flowLayoutPlugins.SuspendLayout();
foreach(Plugin plugin in pluginManager.Plugins.OrderBy(GetPluginOrderIndex).ThenBy(plugin => plugin.Name)){
flowLayoutPlugins.Controls.Add(new PluginControl(pluginManager, plugin));

View File

@@ -3,6 +3,9 @@ using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Notification.Example;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
@@ -21,7 +24,7 @@ namespace TweetDuck.Core.Other{
public bool ShouldReloadBrowser { get; private set; }
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, Type startTab){
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, AnalyticsManager analytics, Type startTab){
InitializeComponent();
Text = Program.BrandName+" Options";
@@ -33,20 +36,21 @@ namespace TweetDuck.Core.Other{
this.buttonHeight = BrowserUtils.Scale(39, this.GetDPIScale()) | 1;
AddButton("General", () => new TabSettingsGeneral(updates));
AddButton("General", () => new TabSettingsGeneral(this.browser, updates));
AddButton("System Tray", () => new TabSettingsTray());
AddButton("Notifications", () => new TabSettingsNotifications(browser.CreateNotificationForm(false)));
AddButton("Notifications", () => new TabSettingsNotifications(new FormNotificationExample(this.browser, this.plugins)));
AddButton("Sounds", () => new TabSettingsSounds());
AddButton("Feedback", () => new TabSettingsFeedback());
AddButton("Advanced", () => new TabSettingsAdvanced(browser.ReinjectCustomCSS));
AddButton("Feedback", () => new TabSettingsFeedback(analytics, AnalyticsReportGenerator.ExternalInfo.From(this.browser), this.plugins));
AddButton("Advanced", () => new TabSettingsAdvanced(this.browser.ReinjectCustomCSS));
SelectTab(tabs[startTab ?? typeof(TabSettingsGeneral)]);
}
private void FormSettings_FormClosing(object sender, FormClosingEventArgs e){
currentTab.Control.OnClosing();
foreach(SettingsTab tab in tabs.Values){
if (tab.IsInitialized){
tab.Control.OnClosing();
tab.Control.Dispose();
}
}
@@ -56,20 +60,21 @@ namespace TweetDuck.Core.Other{
}
private void btnManageOptions_Click(object sender, EventArgs e){
foreach(SettingsTab tab in tabs.Values){
if (tab.IsInitialized){
tab.Control.OnClosing();
}
}
currentTab.Control.OnClosing();
using(DialogSettingsManage dialog = new DialogSettingsManage(plugins)){
FormClosing -= FormSettings_FormClosing;
if (dialog.ShowDialog() == DialogResult.OK){
FormClosing -= FormSettings_FormClosing;
browser.ResumeNotification();
BrowserProcessHandler.UpdatePrefs();
ShouldReloadBrowser = dialog.ShouldReloadBrowser;
Close();
}
else{
FormClosing += FormSettings_FormClosing;
}
}
}
@@ -113,6 +118,7 @@ namespace TweetDuck.Core.Other{
private void SelectTab(SettingsTab tab){
if (currentTab != null){
currentTab.Button.BackColor = SystemColors.Control;
currentTab.Control.OnClosing();
}
tab.Button.BackColor = tab.Button.FlatAppearance.MouseDownBackColor;
@@ -157,11 +163,7 @@ namespace TweetDuck.Core.Other{
private sealed class SettingsTab{
public Button Button { get; }
public BaseTabSettings Control{
get => control ?? (control = constructor());
set => control = value;
}
public BaseTabSettings Control => control ?? (control = constructor());
public bool IsInitialized => control != null;
private readonly Func<BaseTabSettings> constructor;

View File

@@ -1,8 +1,9 @@
using System;
using System.Diagnostics;
using System.Timers;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Core.Controls;
using Timer = System.Timers.Timer;
namespace TweetDuck.Core.Other.Management{
@@ -12,7 +13,7 @@ namespace TweetDuck.Core.Other.Management{
private readonly string script;
private readonly Timer timer;
private Form owner;
private FormBrowser owner;
private IBrowser browser;
private long threshold;
@@ -25,11 +26,11 @@ namespace TweetDuck.Core.Other.Management{
this.timer.Elapsed += timer_Elapsed;
}
public void Start(Form owner, IBrowser browser, int thresholdMB){
public void Start(ChromiumWebBrowser control, int thresholdMB){
Stop();
this.owner = owner;
this.browser = browser;
this.owner = (FormBrowser)control.Parent; // TODO ugly
this.browser = control.GetBrowser();
this.threshold = thresholdMB*1024L*1024L;
this.timer.SynchronizingObject = owner;
this.timer.Start();
@@ -70,6 +71,7 @@ namespace TweetDuck.Core.Other.Management{
if (response.Success && (response.Result as bool? ?? false)){
SetNeedsCleanup(false);
owner.InvokeAsyncSafe(() => owner.TriggerAnalyticsEvent(Analytics.AnalyticsFile.Event.GCReload));
}
});
}

View File

@@ -8,8 +8,6 @@ using TweetLib.Communication;
namespace TweetDuck.Core.Other.Management{
sealed class VideoPlayer : IDisposable{
private readonly string PlayerExe = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe");
public bool Running{
get{
if (currentProcess == null){
@@ -25,6 +23,7 @@ namespace TweetDuck.Core.Other.Management{
private readonly Form owner;
private string lastUrl;
private string lastUsername;
private Process currentProcess;
private DuplexPipe.Server currentPipe;
@@ -35,20 +34,21 @@ namespace TweetDuck.Core.Other.Management{
this.owner.FormClosing += owner_FormClosing;
}
public void Launch(string url){
public void Launch(string url, string username){
if (Running){
Destroy();
isClosing = false;
}
lastUrl = url;
lastUsername = username;
try{
currentPipe = DuplexPipe.CreateServer();
currentPipe.DataIn += currentPipe_DataIn;
if ((currentProcess = Process.Start(new ProcessStartInfo{
FileName = PlayerExe,
FileName = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe"),
Arguments = $"{owner.Handle} {Program.UserConfig.VideoPlayerVolume} \"{url}\" \"{currentPipe.GenerateToken()}\"",
UseShellExecute = false,
RedirectStandardOutput = true
@@ -84,7 +84,7 @@ namespace TweetDuck.Core.Other.Management{
break;
case "download":
TwitterUtils.DownloadVideo(lastUrl);
TwitterUtils.DownloadVideo(lastUrl, lastUsername);
break;
case "rip":
@@ -157,14 +157,14 @@ namespace TweetDuck.Core.Other.Management{
switch(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)){
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 your 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)){
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 your browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl);
}

View File

@@ -0,0 +1,90 @@
namespace TweetDuck.Core.Other.Settings.Dialogs {
partial class DialogSettingsAnalytics {
/// <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.textBoxReport = new System.Windows.Forms.TextBox();
this.btnClose = new System.Windows.Forms.Button();
this.labelInfo = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// textBoxReport
//
this.textBoxReport.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.textBoxReport.Font = new System.Drawing.Font("Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.textBoxReport.Location = new System.Drawing.Point(12, 41);
this.textBoxReport.Multiline = true;
this.textBoxReport.Name = "textBoxReport";
this.textBoxReport.ReadOnly = true;
this.textBoxReport.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.textBoxReport.Size = new System.Drawing.Size(460, 480);
this.textBoxReport.TabIndex = 1;
//
// btnClose
//
this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnClose.Location = new System.Drawing.Point(416, 527);
this.btnClose.Name = "btnClose";
this.btnClose.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0);
this.btnClose.Size = new System.Drawing.Size(56, 23);
this.btnClose.TabIndex = 2;
this.btnClose.Text = "Close";
this.btnClose.UseVisualStyleBackColor = true;
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
//
// labelInfo
//
this.labelInfo.Location = new System.Drawing.Point(12, 9);
this.labelInfo.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
this.labelInfo.Name = "labelInfo";
this.labelInfo.Size = new System.Drawing.Size(460, 26);
this.labelInfo.TabIndex = 0;
this.labelInfo.Text = "When enabled, this data will be sent over a secure network roughly once every wee" +
"k.\r\nSome numbers in the report were made imprecise on purpose.";
//
// DialogSettingsAnalytics
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(484, 562);
this.Controls.Add(this.labelInfo);
this.Controls.Add(this.btnClose);
this.Controls.Add(this.textBoxReport);
this.MinimumSize = new System.Drawing.Size(450, 340);
this.Name = "DialogSettingsAnalytics";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.TextBox textBoxReport;
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.Label labelInfo;
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsAnalytics : Form{
public string CefArgs => textBoxReport.Text;
public DialogSettingsAnalytics(AnalyticsReport report){
InitializeComponent();
Text = Program.BrandName+" Options - Analytics Report";
textBoxReport.EnableMultilineShortcuts();
textBoxReport.Text = report.ToString().TrimEnd();
textBoxReport.Select(0, 0);
}
private void btnClose_Click(object sender, EventArgs e){
Close();
}
}
}

View File

@@ -35,7 +35,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
}
private void btnOpenWiki_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/wiki");
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki");
}
private void btnApply_Click(object sender, EventArgs e){

View File

@@ -19,7 +19,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
}
private void btnHelp_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("http://peter.sh/experiments/chromium-command-line-switches/");
BrowserUtils.OpenExternalBrowser("http://peter.sh/experiments/chromium-command-line-switches/");
}
private void btnApply_Click(object sender, EventArgs e){

View File

@@ -16,7 +16,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
set{
// this will call events and SetFlag, which also updates the UI
cbConfig.Checked = value.HasFlag(ExportFileFlags.Config);
cbConfig.Checked = value.HasFlag(ExportFileFlags.UserConfig);
cbSession.Checked = value.HasFlag(ExportFileFlags.Session);
cbPluginData.Checked = value.HasFlag(ExportFileFlags.PluginData);
}
@@ -42,7 +42,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
}
private void cbConfig_CheckedChanged(object sender, EventArgs e){
SetFlag(ExportFileFlags.Config, cbConfig.Checked);
SetFlag(ExportFileFlags.UserConfig, cbConfig.Checked);
}
private void cbSession_CheckedChanged(object sender, EventArgs e){
@@ -63,7 +63,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
currentState = State.Reset;
Text = "Restore Defaults";
Flags = ExportFileFlags.Config;
Flags = ExportFileFlags.UserConfig;
}
// Import
@@ -108,10 +108,18 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
case State.Reset:
if (FormMessage.Warning("Reset TweetDuck Options", "This will reset the selected items. Are you sure you want to proceed?", FormMessage.Yes, FormMessage.No)){
if (Flags.HasFlag(ExportFileFlags.Config)){
if (Flags.HasFlag(ExportFileFlags.UserConfig)){
Program.ResetConfig();
}
if (Flags.HasFlag(ExportFileFlags.SystemConfig)){
try{
File.Delete(Program.SystemConfigFilePath);
}catch(Exception ex){
Program.Reporter.HandleException("System Config Reset Error", "Could not delete system config.", true, ex);
}
}
if (Flags.HasFlag(ExportFileFlags.PluginData)){
try{
File.Delete(Program.PluginConfigFilePath);
@@ -124,6 +132,9 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
if (Flags.HasFlag(ExportFileFlags.Session)){
Program.Restart(Arguments.ArgDeleteCookies);
}
else if (Flags.HasFlag(ExportFileFlags.SystemConfig)){
Program.Restart();
}
else{
ShouldReloadBrowser = true;
}
@@ -139,7 +150,12 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
Program.UserConfig.Reload();
if (importManager.IsRestarting){
Program.Restart(Arguments.ArgImportCookies);
if (Flags.HasFlag(ExportFileFlags.Session)){
Program.Restart(Arguments.ArgImportCookies);
}
else if (Flags.HasFlag(ExportFileFlags.SystemConfig)){
Program.Restart();
}
}
else{
ShouldReloadBrowser = true;
@@ -171,6 +187,8 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
}
Program.UserConfig.Save();
Program.SystemConfig.Save();
ExportManager manager = new ExportManager(file, plugins);
if (!manager.Export(Flags)){

View File

@@ -4,9 +4,10 @@ namespace TweetDuck.Core.Other.Settings.Export{
[Flags]
enum ExportFileFlags{
None = 0,
Config = 1,
Session = 2,
PluginData = 4,
All = Config|Session|PluginData
UserConfig = 1,
SystemConfig = 2, // TODO implement later
Session = 4,
PluginData = 8,
All = UserConfig|SystemConfig|Session|PluginData
}
}

View File

@@ -25,10 +25,14 @@ namespace TweetDuck.Core.Other.Settings.Export{
public bool Export(ExportFileFlags flags){
try{
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
if (flags.HasFlag(ExportFileFlags.Config)){
if (flags.HasFlag(ExportFileFlags.UserConfig)){
stream.WriteFile("config", Program.UserConfigFilePath);
}
if (flags.HasFlag(ExportFileFlags.SystemConfig)){
stream.WriteFile("system", Program.SystemConfigFilePath);
}
if (flags.HasFlag(ExportFileFlags.PluginData)){
stream.WriteFile("plugin.config", Program.PluginConfigFilePath);
@@ -67,7 +71,11 @@ namespace TweetDuck.Core.Other.Settings.Export{
while((key = stream.SkipFile()) != null){
switch(key){
case "config":
flags |= ExportFileFlags.Config;
flags |= ExportFileFlags.UserConfig;
break;
case "system":
flags |= ExportFileFlags.SystemConfig;
break;
case "plugin.config":
@@ -99,12 +107,20 @@ namespace TweetDuck.Core.Other.Settings.Export{
while((entry = stream.ReadFile()) != null){
switch(entry.KeyName){
case "config":
if (flags.HasFlag(ExportFileFlags.Config)){
if (flags.HasFlag(ExportFileFlags.UserConfig)){
entry.WriteToFile(Program.UserConfigFilePath);
}
break;
case "system":
if (flags.HasFlag(ExportFileFlags.SystemConfig)){
entry.WriteToFile(Program.SystemConfigFilePath);
IsRestarting = true;
}
break;
case "plugin.config":
if (flags.HasFlag(ExportFileFlags.PluginData)){
entry.WriteToFile(Program.PluginConfigFilePath);

View File

@@ -29,14 +29,10 @@ namespace TweetDuck.Core.Other.Settings{
numMemoryThreshold.Enabled = checkBrowserGCReload.Checked;
numMemoryThreshold.SetValueSafe(SysConfig.BrowserMemoryThreshold);
BrowserCache.CalculateCacheSize(bytes => this.InvokeSafe(() => {
if (bytes == -1L){
btnClearCache.Text = "Clear Cache (unknown size)";
}
else{
btnClearCache.Text = "Clear Cache ("+(int)Math.Ceiling(bytes/(1024.0*1024.0))+" MB)";
}
}));
BrowserCache.CalculateCacheSize(task => {
string text = task.IsCompleted ? (int)Math.Ceiling(task.Result/(1024.0*1024.0))+" MB" : "(unknown size)";
this.InvokeSafe(() => btnClearCache.Text = $"Clear Cache ({text})");
});
}
public override void OnReady(){

View File

@@ -25,12 +25,14 @@
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.panelFeedback = new System.Windows.Forms.Panel();
this.labelDataCollectionMessage = new System.Windows.Forms.Label();
this.btnViewReport = new System.Windows.Forms.Button();
this.btnSendFeedback = new System.Windows.Forms.Button();
this.labelDataCollectionLink = new System.Windows.Forms.LinkLabel();
this.checkDataCollection = new System.Windows.Forms.CheckBox();
this.labelDataCollection = new System.Windows.Forms.Label();
this.labelFeedback = new System.Windows.Forms.Label();
this.toolTip = new System.Windows.Forms.ToolTip(this.components);
this.btnSendFeedback = new System.Windows.Forms.Button();
this.panelFeedback.SuspendLayout();
this.SuspendLayout();
//
@@ -38,15 +40,48 @@
//
this.panelFeedback.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.panelFeedback.Controls.Add(this.labelDataCollectionMessage);
this.panelFeedback.Controls.Add(this.btnViewReport);
this.panelFeedback.Controls.Add(this.btnSendFeedback);
this.panelFeedback.Controls.Add(this.labelDataCollectionLink);
this.panelFeedback.Controls.Add(this.checkDataCollection);
this.panelFeedback.Controls.Add(this.labelDataCollection);
this.panelFeedback.Location = new System.Drawing.Point(9, 31);
this.panelFeedback.Name = "panelFeedback";
this.panelFeedback.Size = new System.Drawing.Size(322, 80);
this.panelFeedback.Size = new System.Drawing.Size(322, 188);
this.panelFeedback.TabIndex = 1;
//
// labelDataCollectionMessage
//
this.labelDataCollectionMessage.Location = new System.Drawing.Point(6, 114);
this.labelDataCollectionMessage.Margin = new System.Windows.Forms.Padding(6);
this.labelDataCollectionMessage.Name = "labelDataCollectionMessage";
this.labelDataCollectionMessage.Size = new System.Drawing.Size(310, 67);
this.labelDataCollectionMessage.TabIndex = 5;
//
// btnViewReport
//
this.btnViewReport.AutoSize = true;
this.btnViewReport.Location = new System.Drawing.Point(6, 82);
this.btnViewReport.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3);
this.btnViewReport.Name = "btnViewReport";
this.btnViewReport.Size = new System.Drawing.Size(144, 23);
this.btnViewReport.TabIndex = 4;
this.btnViewReport.Text = "View My Analytics Report";
this.btnViewReport.UseVisualStyleBackColor = true;
//
// btnSendFeedback
//
this.btnSendFeedback.AutoSize = true;
this.btnSendFeedback.Location = new System.Drawing.Point(5, 3);
this.btnSendFeedback.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3);
this.btnSendFeedback.Name = "btnSendFeedback";
this.btnSendFeedback.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0);
this.btnSendFeedback.Size = new System.Drawing.Size(164, 23);
this.btnSendFeedback.TabIndex = 0;
this.btnSendFeedback.Text = "Send Feedback / Bug Report";
this.btnSendFeedback.UseVisualStyleBackColor = true;
//
// labelDataCollectionLink
//
this.labelDataCollectionLink.AutoSize = true;
@@ -60,7 +95,6 @@
this.labelDataCollectionLink.TabStop = true;
this.labelDataCollectionLink.Text = "(learn more)";
this.labelDataCollectionLink.UseCompatibleTextRendering = true;
this.labelDataCollectionLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.labelDataCollectionLink_LinkClicked);
//
// checkDataCollection
//
@@ -94,18 +128,6 @@
this.labelFeedback.TabIndex = 0;
this.labelFeedback.Text = "Feedback";
//
// btnSendFeedback
//
this.btnSendFeedback.AutoSize = true;
this.btnSendFeedback.Location = new System.Drawing.Point(5, 3);
this.btnSendFeedback.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3);
this.btnSendFeedback.Name = "btnSendFeedback";
this.btnSendFeedback.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0);
this.btnSendFeedback.Size = new System.Drawing.Size(164, 23);
this.btnSendFeedback.TabIndex = 0;
this.btnSendFeedback.Text = "Send Feedback / Bug Report";
this.btnSendFeedback.UseVisualStyleBackColor = true;
//
// TabSettingsFeedback
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -113,7 +135,7 @@
this.Controls.Add(this.panelFeedback);
this.Controls.Add(this.labelFeedback);
this.Name = "TabSettingsFeedback";
this.Size = new System.Drawing.Size(340, 122);
this.Size = new System.Drawing.Size(340, 230);
this.panelFeedback.ResumeLayout(false);
this.panelFeedback.PerformLayout();
this.ResumeLayout(false);
@@ -130,5 +152,7 @@
private System.Windows.Forms.ToolTip toolTip;
private System.Windows.Forms.LinkLabel labelDataCollectionLink;
private System.Windows.Forms.Button btnSendFeedback;
private System.Windows.Forms.Button btnViewReport;
private System.Windows.Forms.Label labelDataCollectionMessage;
}
}

View File

@@ -1,22 +1,40 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsFeedback : BaseTabSettings{
public TabSettingsFeedback(){
private readonly AnalyticsFile analyticsFile;
private readonly AnalyticsReportGenerator.ExternalInfo analyticsInfo;
private readonly PluginManager plugins;
public TabSettingsFeedback(AnalyticsManager analytics, AnalyticsReportGenerator.ExternalInfo analyticsInfo, PluginManager plugins){
InitializeComponent();
this.analyticsFile = analytics?.File ?? AnalyticsFile.Load(Program.AnalyticsFilePath);
this.analyticsInfo = analyticsInfo;
this.plugins = plugins;
checkDataCollection.Checked = Config.AllowDataCollection;
if (analytics != null){
string collectionTime = analyticsFile.LastCollectionMessage;
labelDataCollectionMessage.Text = string.IsNullOrEmpty(collectionTime) ? "No collection yet" : "Last collection: "+collectionTime;
}
}
public override void OnReady(){
btnSendFeedback.Click += btnSendFeedback_Click;
checkDataCollection.CheckedChanged += checkDataCollection_CheckedChanged;
labelDataCollectionLink.LinkClicked += labelDataCollectionLink_LinkClicked;
btnViewReport.Click += btnViewReport_Click;
}
private void btnSendFeedback_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/issues/new");
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/issues/new");
}
private void checkDataCollection_CheckedChanged(object sender, EventArgs e){
@@ -24,7 +42,13 @@ namespace TweetDuck.Core.Other.Settings{
}
private void labelDataCollectionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/wiki/Send-anonymous-data");
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki/Send-anonymous-data");
}
private void btnViewReport_Click(object sender, EventArgs e){
using(DialogSettingsAnalytics dialog = new DialogSettingsAnalytics(AnalyticsReportGenerator.Create(analyticsFile, analyticsInfo, plugins))){
dialog.ShowDialog();
}
}
}
}

View File

@@ -40,6 +40,7 @@
this.panelUI = new System.Windows.Forms.Panel();
this.panelUpdates = new System.Windows.Forms.Panel();
this.labelUpdates = new System.Windows.Forms.Label();
this.checkAnimatedAvatars = new System.Windows.Forms.CheckBox();
((System.ComponentModel.ISupportInitialize)(this.trackBarZoom)).BeginInit();
this.panelUI.SuspendLayout();
this.panelUpdates.SuspendLayout();
@@ -60,11 +61,11 @@
// checkSpellCheck
//
this.checkSpellCheck.AutoSize = true;
this.checkSpellCheck.Location = new System.Drawing.Point(6, 97);
this.checkSpellCheck.Location = new System.Drawing.Point(6, 120);
this.checkSpellCheck.Margin = new System.Windows.Forms.Padding(6, 3, 3, 3);
this.checkSpellCheck.Name = "checkSpellCheck";
this.checkSpellCheck.Size = new System.Drawing.Size(119, 17);
this.checkSpellCheck.TabIndex = 4;
this.checkSpellCheck.TabIndex = 5;
this.checkSpellCheck.Text = "Enable Spell Check";
this.toolTip.SetToolTip(this.checkSpellCheck, "Underlines words that are spelled incorrectly.");
this.checkSpellCheck.UseVisualStyleBackColor = true;
@@ -95,11 +96,11 @@
// labelZoomValue
//
this.labelZoomValue.BackColor = System.Drawing.Color.Transparent;
this.labelZoomValue.Location = new System.Drawing.Point(147, 146);
this.labelZoomValue.Location = new System.Drawing.Point(147, 169);
this.labelZoomValue.Margin = new System.Windows.Forms.Padding(0, 0, 3, 0);
this.labelZoomValue.Name = "labelZoomValue";
this.labelZoomValue.Size = new System.Drawing.Size(38, 13);
this.labelZoomValue.TabIndex = 7;
this.labelZoomValue.TabIndex = 8;
this.labelZoomValue.Text = "100%";
this.labelZoomValue.TextAlign = System.Drawing.ContentAlignment.TopRight;
this.toolTip.SetToolTip(this.labelZoomValue, "Changes the zoom level.\r\nAlso affects notifications and screenshots.");
@@ -145,24 +146,24 @@
this.trackBarZoom.AutoSize = false;
this.trackBarZoom.BackColor = System.Drawing.SystemColors.Control;
this.trackBarZoom.LargeChange = 25;
this.trackBarZoom.Location = new System.Drawing.Point(3, 145);
this.trackBarZoom.Location = new System.Drawing.Point(3, 168);
this.trackBarZoom.Maximum = 200;
this.trackBarZoom.Minimum = 50;
this.trackBarZoom.Name = "trackBarZoom";
this.trackBarZoom.Size = new System.Drawing.Size(148, 30);
this.trackBarZoom.SmallChange = 5;
this.trackBarZoom.TabIndex = 6;
this.trackBarZoom.TabIndex = 7;
this.trackBarZoom.TickFrequency = 25;
this.trackBarZoom.Value = 100;
//
// labelZoom
//
this.labelZoom.AutoSize = true;
this.labelZoom.Location = new System.Drawing.Point(3, 129);
this.labelZoom.Location = new System.Drawing.Point(3, 152);
this.labelZoom.Margin = new System.Windows.Forms.Padding(3, 12, 3, 0);
this.labelZoom.Name = "labelZoom";
this.labelZoom.Size = new System.Drawing.Size(34, 13);
this.labelZoom.TabIndex = 5;
this.labelZoom.TabIndex = 6;
this.labelZoom.Text = "Zoom";
//
// zoomUpdateTimer
@@ -185,6 +186,7 @@
//
this.panelUI.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.panelUI.Controls.Add(this.checkAnimatedAvatars);
this.panelUI.Controls.Add(this.checkOpenSearchInFirstColumn);
this.panelUI.Controls.Add(this.checkBestImageQuality);
this.panelUI.Controls.Add(this.checkExpandLinks);
@@ -195,7 +197,7 @@
this.panelUI.Controls.Add(this.labelZoomValue);
this.panelUI.Location = new System.Drawing.Point(9, 31);
this.panelUI.Name = "panelUI";
this.panelUI.Size = new System.Drawing.Size(322, 179);
this.panelUI.Size = new System.Drawing.Size(322, 205);
this.panelUI.TabIndex = 1;
//
// panelUpdates
@@ -204,7 +206,7 @@
| System.Windows.Forms.AnchorStyles.Right)));
this.panelUpdates.Controls.Add(this.checkUpdateNotifications);
this.panelUpdates.Controls.Add(this.btnCheckUpdates);
this.panelUpdates.Location = new System.Drawing.Point(9, 257);
this.panelUpdates.Location = new System.Drawing.Point(8, 283);
this.panelUpdates.Name = "panelUpdates";
this.panelUpdates.Size = new System.Drawing.Size(322, 55);
this.panelUpdates.TabIndex = 3;
@@ -213,13 +215,25 @@
//
this.labelUpdates.AutoSize = true;
this.labelUpdates.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.labelUpdates.Location = new System.Drawing.Point(6, 234);
this.labelUpdates.Location = new System.Drawing.Point(5, 260);
this.labelUpdates.Margin = new System.Windows.Forms.Padding(0, 21, 0, 0);
this.labelUpdates.Name = "labelUpdates";
this.labelUpdates.Size = new System.Drawing.Size(70, 20);
this.labelUpdates.TabIndex = 2;
this.labelUpdates.Text = "Updates";
//
// checkAnimatedAvatars
//
this.checkAnimatedAvatars.AutoSize = true;
this.checkAnimatedAvatars.Location = new System.Drawing.Point(6, 97);
this.checkAnimatedAvatars.Margin = new System.Windows.Forms.Padding(6, 3, 3, 3);
this.checkAnimatedAvatars.Name = "checkAnimatedAvatars";
this.checkAnimatedAvatars.Size = new System.Drawing.Size(145, 17);
this.checkAnimatedAvatars.TabIndex = 4;
this.checkAnimatedAvatars.Text = "Enable Animated Avatars";
this.toolTip.SetToolTip(this.checkAnimatedAvatars, "Some old Twitter avatars could be uploaded as animated GIFs.");
this.checkAnimatedAvatars.UseVisualStyleBackColor = true;
//
// TabSettingsGeneral
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -229,7 +243,7 @@
this.Controls.Add(this.panelUI);
this.Controls.Add(this.labelUI);
this.Name = "TabSettingsGeneral";
this.Size = new System.Drawing.Size(340, 322);
this.Size = new System.Drawing.Size(340, 348);
((System.ComponentModel.ISupportInitialize)(this.trackBarZoom)).EndInit();
this.panelUI.ResumeLayout(false);
this.panelUI.PerformLayout();
@@ -258,5 +272,6 @@
private System.Windows.Forms.Label labelUpdates;
private System.Windows.Forms.CheckBox checkBestImageQuality;
private System.Windows.Forms.CheckBox checkOpenSearchInFirstColumn;
private System.Windows.Forms.CheckBox checkAnimatedAvatars;
}
}

View File

@@ -1,15 +1,19 @@
using System;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling.General;
using TweetDuck.Updates;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsGeneral : BaseTabSettings{
private readonly FormBrowser browser;
private readonly UpdateHandler updates;
private int updateCheckEventId = -1;
public TabSettingsGeneral(UpdateHandler updates){
public TabSettingsGeneral(FormBrowser browser, UpdateHandler updates){
InitializeComponent();
this.browser = browser;
this.updates = updates;
this.updates.CheckFinished += updates_CheckFinished;
Disposed += (sender, args) => this.updates.CheckFinished -= updates_CheckFinished;
@@ -22,6 +26,7 @@ namespace TweetDuck.Core.Other.Settings{
checkSwitchAccountSelectors.Checked = Config.SwitchAccountSelectors;
checkOpenSearchInFirstColumn.Checked = Config.OpenSearchInFirstColumn;
checkBestImageQuality.Checked = Config.BestImageQuality;
checkAnimatedAvatars.Checked = Config.EnableAnimatedImages;
checkSpellCheck.Checked = Config.EnableSpellCheck;
checkUpdateNotifications.Checked = Config.EnableUpdateCheck;
@@ -32,6 +37,7 @@ namespace TweetDuck.Core.Other.Settings{
checkSwitchAccountSelectors.CheckedChanged += checkSwitchAccountSelectors_CheckedChanged;
checkOpenSearchInFirstColumn.CheckedChanged += checkOpenSearchInFirstColumn_CheckedChanged;
checkBestImageQuality.CheckedChanged += checkBestImageQuality_CheckedChanged;
checkAnimatedAvatars.CheckedChanged += checkAnimatedAvatars_CheckedChanged;
checkSpellCheck.CheckedChanged += checkSpellCheck_CheckedChanged;
trackBarZoom.ValueChanged += trackBarZoom_ValueChanged;
@@ -59,9 +65,14 @@ namespace TweetDuck.Core.Other.Settings{
Config.BestImageQuality = checkBestImageQuality.Checked;
}
private void checkAnimatedAvatars_CheckedChanged(object sender, EventArgs e){
Config.EnableAnimatedImages = checkAnimatedAvatars.Checked;
BrowserProcessHandler.UpdatePrefs().ContinueWith(task => browser.ReloadColumns());
}
private void checkSpellCheck_CheckedChanged(object sender, EventArgs e){
Config.EnableSpellCheck = checkSpellCheck.Checked;
PromptRestart();
BrowserProcessHandler.UpdatePrefs();
}
private void trackBarZoom_ValueChanged(object sender, EventArgs e){

View File

@@ -2,21 +2,22 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Example;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsNotifications : BaseTabSettings{
private static readonly int[] IdlePauseSeconds = { 0, 30, 60, 120, 300 };
private readonly FormNotificationMain notification;
private readonly FormNotificationExample notification;
public TabSettingsNotifications(FormNotificationMain notification){
public TabSettingsNotifications(FormNotificationExample notification){
InitializeComponent();
this.notification = notification;
this.notification.Initialized += (sender, args) => {
this.InvokeAsyncSafe(() => {
this.notification.ShowNotificationForSettings(true);
this.notification.ShowExampleNotification(true);
this.notification.Move += notification_Move;
this.notification.ResizeEnd += notification_ResizeEnd;
});
@@ -73,9 +74,6 @@ namespace TweetDuck.Core.Other.Settings{
trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance);
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value+" px";
this.notification.CanMoveWindow = () => radioLocCustom.Checked;
this.notification.CanResizeWindow = radioSizeCustom.Checked;
Disposed += (sender, args) => this.notification.Dispose();
}
@@ -110,10 +108,10 @@ namespace TweetDuck.Core.Other.Settings{
private void TabSettingsNotifications_ParentChanged(object sender, EventArgs e){
if (Parent == null){
notification.HideNotification(false);
notification.HideNotification();
}
else{
notification.ShowNotificationForSettings(true);
notification.ShowExampleNotification(true);
}
}
@@ -131,7 +129,7 @@ namespace TweetDuck.Core.Other.Settings{
private void notification_ResizeEnd(object sender, EventArgs e){
if (radioSizeCustom.Checked){
Config.CustomNotificationSize = notification.BrowserSize;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
}
}
@@ -142,7 +140,7 @@ namespace TweetDuck.Core.Other.Settings{
else if (radioLocBR.Checked)Config.NotificationPosition = TweetNotification.Position.BottomRight;
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = true;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
}
private void radioLocCustom_Click(object sender, EventArgs e){
@@ -153,7 +151,7 @@ namespace TweetDuck.Core.Other.Settings{
Config.NotificationPosition = TweetNotification.Position.Custom;
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = false;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
if (notification.IsFullyOutsideView() && FormMessage.Question("Notification is outside view", "The notification seems to be outside of view, would you like to reset its position?", FormMessage.Yes, FormMessage.No)){
Config.NotificationPosition = TweetNotification.Position.TopRight;
@@ -167,10 +165,11 @@ namespace TweetDuck.Core.Other.Settings{
}
private void radioSize_CheckedChanged(object sender, EventArgs e){
if (radioSizeAuto.Checked)Config.NotificationSize = TweetNotification.Size.Auto;
if (radioSizeAuto.Checked){
Config.NotificationSize = TweetNotification.Size.Auto;
}
notification.ShowNotificationForSettings(false);
notification.CanResizeWindow = false; // must be after ShowNotificationForSettings
notification.ShowExampleNotification(false);
}
private void radioSizeCustom_Click(object sender, EventArgs e){
@@ -179,9 +178,7 @@ namespace TweetDuck.Core.Other.Settings{
}
Config.NotificationSize = TweetNotification.Size.Custom;
notification.CanResizeWindow = true;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
}
private void trackBarDuration_ValueChanged(object sender, EventArgs e){
@@ -206,18 +203,18 @@ namespace TweetDuck.Core.Other.Settings{
private void checkColumnName_CheckedChanged(object sender, EventArgs e){
Config.DisplayNotificationColumn = checkColumnName.Checked;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
}
private void checkNotificationTimer_CheckedChanged(object sender, EventArgs e){
Config.DisplayNotificationTimer = checkNotificationTimer.Checked;
checkTimerCountDown.Enabled = checkNotificationTimer.Checked;
notification.ShowNotificationForSettings(true);
notification.ShowExampleNotification(true);
}
private void checkTimerCountDown_CheckedChanged(object sender, EventArgs e){
Config.NotificationTimerCountDown = checkTimerCountDown.Checked;
notification.ShowNotificationForSettings(true);
notification.ShowExampleNotification(true);
}
private void checkMediaPreviews_CheckedChanged(object sender, EventArgs e){
@@ -245,17 +242,17 @@ namespace TweetDuck.Core.Other.Settings{
private void comboBoxDisplay_SelectedValueChanged(object sender, EventArgs e){
Config.NotificationDisplay = comboBoxDisplay.SelectedIndex;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
}
private void trackBarEdgeDistance_ValueChanged(object sender, EventArgs e){
labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value+" px";
Config.NotificationEdgeDistance = trackBarEdgeDistance.Value;
notification.ShowNotificationForSettings(false);
notification.ShowExampleNotification(false);
}
private void durationUpdateTimer_Tick(object sender, EventArgs e){
notification.ShowNotificationForSettings(true);
notification.ShowExampleNotification(true);
durationUpdateTimer.Stop();
}
}

View File

@@ -1,4 +1,4 @@
namespace TweetDuck.Core {
namespace TweetDuck.Core.Other {
partial class TrayIcon {
/// <summary>
/// Required designer variable.

View File

@@ -2,7 +2,7 @@
using System.ComponentModel;
using System.Windows.Forms;
namespace TweetDuck.Core{
namespace TweetDuck.Core.Other{
sealed partial class TrayIcon : Component{
public enum Behavior{ // keep order
Disabled, DisplayOnly, MinimizeToTray, CloseToTray, Combined
@@ -12,7 +12,9 @@ namespace TweetDuck.Core{
public event EventHandler ClickClose;
public bool Visible{
get => notifyIcon.Visible;
get{
return notifyIcon.Visible;
}
set{
if (value){

242
Core/TweetDeckBrowser.cs Normal file
View File

@@ -0,0 +1,242 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Management;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
using TweetDuck.Updates;
namespace TweetDuck.Core{
sealed class TweetDeckBrowser : IDisposable{
public bool Ready { get; private set; }
public bool Enabled{
get => browser.Enabled;
set => browser.Enabled = value;
}
public bool IsTweetDeckWebsite{
get{
using(IFrame frame = browser.GetBrowser().MainFrame){
return TwitterUtils.IsTweetDeckWebsite(frame);
}
}
}
public event EventHandler PageLoaded;
private readonly ChromiumWebBrowser browser;
private readonly PluginManager plugins;
private readonly MemoryUsageTracker memoryUsageTracker;
public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge bridge){
this.browser = new ChromiumWebBrowser(TwitterUtils.TweetDeckURL){
DialogHandler = new FileDialogHandler(),
DragHandler = new DragHandlerBrowser(),
MenuHandler = new ContextMenuBrowser(owner),
JsDialogHandler = new JavaScriptDialogHandler(),
KeyboardHandler = new KeyboardHandlerBrowser(owner),
LifeSpanHandler = new LifeSpanHandler(),
RequestHandler = new RequestHandlerBrowser()
};
#if DEBUG
this.browser.ConsoleMessage += BrowserUtils.HandleConsoleMessage;
#endif
this.browser.LoadingStateChanged += browser_LoadingStateChanged;
this.browser.FrameLoadStart += browser_FrameLoadStart;
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.browser.LoadError += browser_LoadError;
this.browser.RegisterAsyncJsObject("$TD", bridge);
this.browser.RegisterAsyncJsObject("$TDP", plugins.Bridge);
this.browser.BrowserSettings.BackgroundColor = (uint)TwitterUtils.BackgroundColor.ToArgb();
this.browser.Dock = DockStyle.None;
this.browser.Location = ControlExtensions.InvisibleLocation;
owner.Controls.Add(browser);
this.plugins = plugins;
this.plugins.PluginChangedState += plugins_PluginChangedState;
this.memoryUsageTracker = new MemoryUsageTracker("TDGF_tryRunCleanup");
Program.UserConfig.MuteToggled += UserConfig_MuteToggled;
Program.UserConfig.ZoomLevelChanged += UserConfig_ZoomLevelChanged;
}
// setup and management
private void OnBrowserReady(){
if (!Ready){
browser.Location = Point.Empty;
browser.Dock = DockStyle.Fill;
Ready = true;
}
}
public void Focus(){
browser.Focus();
}
public void Dispose(){
plugins.PluginChangedState -= plugins_PluginChangedState;
Program.UserConfig.MuteToggled -= UserConfig_MuteToggled;
Program.UserConfig.ZoomLevelChanged -= UserConfig_ZoomLevelChanged;
memoryUsageTracker.Dispose();
browser.Dispose();
}
// event handlers
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
if (!e.IsLoading){
foreach(string word in TwitterUtils.DictionaryWords){
browser.AddWordToDictionary(word);
}
browser.BeginInvoke(new Action(OnBrowserReady));
browser.LoadingStateChanged -= browser_LoadingStateChanged;
}
}
private void browser_FrameLoadStart(object sender, FrameLoadStartEventArgs e){
if (e.Frame.IsMain){
memoryUsageTracker.Stop();
if (Program.UserConfig.ZoomLevel != 100){
BrowserUtils.SetZoomLevel(browser.GetBrowser(), Program.UserConfig.ZoomLevel);
}
if (TwitterUtils.IsTwitterWebsite(e.Frame)){
ScriptLoader.ExecuteFile(e.Frame, "twitter.js");
}
}
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
if (e.Frame.IsMain && TwitterUtils.IsTweetDeckWebsite(e.Frame)){
e.Frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorFix);
UpdateProperties();
TweetDeckBridge.RestoreSessionData(e.Frame);
ScriptLoader.ExecuteFile(e.Frame, "code.js");
InjectBrowserCSS();
ReinjectCustomCSS(Program.UserConfig.CustomBrowserCSS);
plugins.ExecutePlugins(e.Frame, PluginEnvironment.Browser);
TweetDeckBridge.ResetStaticProperties();
if (Program.SystemConfig.EnableBrowserGCReload){
memoryUsageTracker.Start(browser, Program.SystemConfig.BrowserMemoryThreshold);
}
if (Program.UserConfig.FirstRun){
ScriptLoader.ExecuteFile(e.Frame, "introduction.js");
}
PageLoaded?.Invoke(this, EventArgs.Empty);
}
}
private void browser_LoadError(object sender, LoadErrorEventArgs e){
if (e.ErrorCode == CefErrorCode.Aborted){
return;
}
if (!e.FailedUrl.StartsWith("http://td/", StringComparison.Ordinal)){
string errorPage = ScriptLoader.LoadResource("pages/error.html", true);
if (errorPage != null){
browser.LoadHtml(errorPage.Replace("{err}", BrowserUtils.GetErrorName(e.ErrorCode)), "http://td/error");
}
}
}
private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
browser.ExecuteScriptAsync("TDPF_setPluginState", e.Plugin, e.IsEnabled);
}
private void UserConfig_MuteToggled(object sender, EventArgs e){
UpdateProperties();
}
private void UserConfig_ZoomLevelChanged(object sender, EventArgs e){
BrowserUtils.SetZoomLevel(browser.GetBrowser(), Program.UserConfig.ZoomLevel);
}
// external handling
public UpdateHandler CreateUpdateHandler(UpdaterSettings settings){
return new UpdateHandler(browser, settings);
}
public void RefreshMemoryTracker(){
if (Program.SystemConfig.EnableBrowserGCReload){
memoryUsageTracker.Start(browser, Program.SystemConfig.BrowserMemoryThreshold);
}
else{
memoryUsageTracker.Stop();
}
}
public void HideVideoOverlay(bool focus){
if (focus){
browser.GetBrowser().GetHost().SendFocusEvent(true);
}
browser.ExecuteScriptAsync("$('#td-video-player-overlay').remove()");
}
// javascript calls
public void ReloadToTweetDeck(){
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUtils.TweetDeckURL}'");
}
public void UpdateProperties(){
browser.ExecuteScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Browser));
}
public void InjectBrowserCSS(){
browser.ExecuteScriptAsync("TDGF_injectBrowserCSS", ScriptLoader.LoadResource("styles/browser.css").TrimEnd());
}
public void ReinjectCustomCSS(string css){
browser.ExecuteScriptAsync("TDGF_reinjectCustomCSS", css?.Replace(Environment.NewLine, " ") ?? string.Empty);
}
public void OnMouseClickExtra(IntPtr param){
browser.ExecuteScriptAsync("TDGF_onMouseClickExtra", (param.ToInt32() >> 16) & 0xFFFF);
}
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl){
browser.ExecuteScriptAsync("TDGF_showTweetDetail", columnId, chirpId, fallbackUrl);
}
public void TriggerTweetScreenshot(){
browser.ExecuteScriptAsync("TDGF_triggerScreenshot()");
}
public void ReloadColumns(){
browser.ExecuteScriptAsync("TDGF_reloadColumns()");
}
public void ApplyROT13(){
browser.ExecuteScriptAsync("TDGF_applyROT13()");
}
}
}

View File

@@ -8,15 +8,10 @@ namespace TweetDuck.Core.Utils{
static class BrowserCache{
private static bool ClearOnExit { get; set; }
private static readonly string CacheFolder = Path.Combine(Program.StoragePath, "Cache");
public static readonly string CacheFolder = Path.Combine(Program.StoragePath, "Cache");
private static IEnumerable<string> CacheFiles => Directory.EnumerateFiles(CacheFolder);
private static IEnumerable<string> CacheFiles{
get{
return Directory.EnumerateFiles(CacheFolder);
}
}
public static void CalculateCacheSize(Action<long> callbackBytes){
public static void CalculateCacheSize(Action<Task<long>> callbackBytes){
Task<long> task = new Task<long>(() => {
return CacheFiles.Select(file => {
try{
@@ -27,7 +22,7 @@ namespace TweetDuck.Core.Utils{
}).Sum();
});
task.ContinueWith(originalTask => callbackBytes(originalTask.Exception == null ? originalTask.Result : -1L), TaskContinuationOptions.ExecuteSynchronously);
task.ContinueWith(callbackBytes);
task.Start();
}

View File

@@ -31,7 +31,7 @@ namespace TweetDuck.Core.Utils{
args["disable-gpu-vsync"] = "1";
}
args["disable-extensions"] = "1";
args["disable-pdf-extension"] = "1";
args["disable-plugins-discovery"] = "1";
args["enable-system-flash"] = "0";
@@ -70,12 +70,12 @@ namespace TweetDuck.Core.Utils{
switch(CheckUrl(url)){
case UrlCheckResult.Fine:
OpenExternalBrowserUnsafe(url);
WindowsUtils.OpenAssociatedProgram(url);
break;
case UrlCheckResult.Tracking:
if (FormMessage.Warning("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n"+url, FormMessage.Yes, FormMessage.No)){
OpenExternalBrowserUnsafe(url);
WindowsUtils.OpenAssociatedProgram(url);
}
break;
@@ -86,10 +86,6 @@ namespace TweetDuck.Core.Utils{
}
}
public static void OpenExternalBrowserUnsafe(string url){
using(Process.Start(url)){}
}
public static string GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
@@ -99,9 +95,14 @@ namespace TweetDuck.Core.Utils{
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
}
public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){
public static WebClient CreateWebClient(){
WebClient client = new WebClient{ Proxy = null };
client.Headers[HttpRequestHeader.UserAgent] = HeaderUserAgent;
return client;
}
public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){
WebClient client = CreateWebClient();
client.DownloadFileCompleted += (sender, args) => {
if (args.Cancelled){

View File

@@ -8,6 +8,7 @@ namespace TweetDuck.Core.Utils{
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
static class NativeMethods{
public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
public static readonly IntPtr HOOK_HANDLED = new IntPtr(-1);
public const int HWND_TOPMOST = -1;
@@ -51,6 +52,12 @@ namespace TweetDuck.Core.Utils{
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
private static extern bool SetWindowPos(int hWnd, int hWndOrder, int x, int y, int width, int height, uint flags);
[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")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO info);
@@ -96,13 +103,18 @@ namespace TweetDuck.Core.Utils{
}
}
public static void BroadcastMessage(uint msg, uint wParam, int lParam){
PostMessage(HWND_BROADCAST, msg, new UIntPtr(wParam), new IntPtr(lParam));
}
public static int GetMouseHookData(IntPtr ptr){
return Marshal.PtrToStructure<MSLLHOOKSTRUCT>(ptr).mouseData >> 16;
}
public static int GetIdleSeconds(){
LASTINPUTINFO info = new LASTINPUTINFO();
info.cbSize = LASTINPUTINFO.Size;
LASTINPUTINFO info = new LASTINPUTINFO{
cbSize = LASTINPUTINFO.Size
};
if (!GetLastInputInfo(ref info)){
return 0;

View File

@@ -18,5 +18,16 @@ namespace TweetDuck.Core.Utils{
public static string ConvertPascalCaseToScreamingSnakeCase(string str){
return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpper();
}
public static int CountOccurrences(string source, string substring){
int count = 0, index = 0;
while((index = source.IndexOf(substring, index)) != -1){
index += substring.Length;
++count;
}
return count;
}
}
}

View File

@@ -21,6 +21,10 @@ namespace TweetDuck.Core.Utils{
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
};
public static readonly string[] ValidImageExtensions = {
".jpg", ".jpeg", ".png", ".gif"
};
public enum ImageQuality{
Default, Orig
}
@@ -53,6 +57,10 @@ namespace TweetDuck.Core.Utils{
}
}
public static string GetImageFileName(string url){
return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
}
public static void DownloadImage(string url, string username, ImageQuality quality){
DownloadImages(new string[]{ url }, username, quality);
}
@@ -65,7 +73,7 @@ namespace TweetDuck.Core.Utils{
string firstImageLink = GetMediaLink(urls[0], quality);
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
string file = BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(firstImageLink));
string file = GetImageFileName(firstImageLink);
string ext = Path.GetExtension(file); // includes dot
string[] fileNameParts = qualityIndex == -1 ? new string[]{
@@ -103,7 +111,7 @@ namespace TweetDuck.Core.Utils{
}
}
public static void DownloadVideo(string url){
public static void DownloadVideo(string url, string username){
string filename = BrowserUtils.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename);
@@ -111,7 +119,7 @@ namespace TweetDuck.Core.Utils{
AutoUpgradeEnabled = true,
OverwritePrompt = true,
Title = "Save video",
FileName = filename,
FileName = string.IsNullOrEmpty(username) ? filename : $"{username} {filename}",
Filter = "Video"+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){
if (dialog.ShowDialog() == DialogResult.OK){

View File

@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Management;
@@ -49,17 +50,22 @@ namespace TweetDuck.Core.Utils{
}
}
public static Process StartProcess(string file, string arguments, bool runElevated){
ProcessStartInfo processInfo = new ProcessStartInfo{
FileName = file,
Arguments = arguments
};
if (runElevated){
processInfo.Verb = "runas";
public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
try{
using(Process.Start(new ProcessStartInfo{
FileName = file,
Arguments = arguments,
Verb = runElevated ? "runas" : string.Empty,
ErrorDialog = true
})){
return true;
}
}catch(Win32Exception e) when (e.NativeErrorCode == 0x000004C7){ // operation canceled by the user
return false;
}catch(Exception e){
Program.Reporter.HandleException("Error opening file", e.Message, true, e);
return false;
}
return Process.Start(processInfo);
}
public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){
@@ -104,7 +110,7 @@ namespace TweetDuck.Core.Utils{
}
public static void ClipboardStripHtmlStyles(){
if (!Clipboard.ContainsText(TextDataFormat.Html)){
if (!Clipboard.ContainsText(TextDataFormat.Html) || !Clipboard.ContainsText(TextDataFormat.UnicodeText)){
return;
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using TweetDuck.Core.Utils;
namespace TweetDuck.Data.Serialization{
sealed class FileSerializer<T>{
@@ -28,6 +29,8 @@ namespace TweetDuck.Data.Serialization{
}
public void Write(string file, T obj){
WindowsUtils.CreateDirectoryForFile(file);
using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
foreach(KeyValuePair<string, PropertyInfo> prop in props){
Type type = prop.Value.PropertyType;
@@ -54,8 +57,12 @@ namespace TweetDuck.Data.Serialization{
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.");
switch(reader.Peek()){
case -1:
throw new FormatException("File is empty.");
case 0:
case 1:
throw new FormatException("Input appears to be a binary file.");
}
foreach(string line in reader.ReadToEnd().Split(new string[]{ NewLineReal }, StringSplitOptions.RemoveEmptyEntries)){
@@ -95,6 +102,13 @@ namespace TweetDuck.Data.Serialization{
}
}
public void ReadIfExists(string file, T obj){
try{
Read(file, obj);
}catch(FileNotFoundException){
}catch(DirectoryNotFoundException){}
}
private sealed class BasicTypeConverter : ITypeConverter{
bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
switch(Type.GetTypeCode(type)){

View File

@@ -62,9 +62,7 @@ namespace TweetDuck.Plugins.Controls{
}
private void btnToggleState_Click(object sender, EventArgs e){
bool newState = !pluginManager.Config.IsEnabled(plugin);
pluginManager.Config.SetEnabled(plugin, newState);
pluginManager.Config.ToggleEnabled(plugin);
UpdatePluginState();
}

View File

@@ -7,12 +7,11 @@ using TweetDuck.Plugins.Enums;
namespace TweetDuck.Plugins{
sealed class Plugin{
private static readonly Version AppVersion = new Version(Program.VersionTag);
private const string VersionWildcard = "*";
public string Identifier { get; }
public PluginGroup Group { get; }
public PluginEnvironment Environments { get; private set; }
public PluginEnvironment Environments { get; }
public string Name => metadata["NAME"];
public string Description => metadata["DESCRIPTION"];
@@ -23,9 +22,7 @@ namespace TweetDuck.Plugins{
public string ConfigDefault => metadata["CONFIGDEFAULT"];
public string RequiredVersion => metadata["REQUIRES"];
public bool CanRun{
get => canRun ?? (canRun = CheckRequiredVersion(RequiredVersion)).Value;
}
public bool CanRun { get; private set; }
public bool HasConfig{
get => ConfigFile.Length > 0 && GetFullPathIfSafe(PluginFolder.Data, ConfigFile).Length > 0;
@@ -56,21 +53,18 @@ namespace TweetDuck.Plugins{
{ "REQUIRES", VersionWildcard }
};
private bool? canRun;
private Plugin(string path, PluginGroup group){
string name = Path.GetFileName(path);
System.Diagnostics.Debug.Assert(name != null);
private Plugin(string path, string name, PluginGroup group, PluginEnvironment environments){
this.pathRoot = path;
this.pathData = Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name);
this.Identifier = group.GetIdentifierPrefix()+name;
this.Group = group;
this.Environments = PluginEnvironment.None;
this.Environments = environments;
}
private void OnMetadataLoaded(){
CanRun = CheckRequiredVersion(RequiredVersion);
string configPath = ConfigPath, defaultConfigPath = DefaultConfigPath;
if (configPath.Length > 0 && defaultConfigPath.Length > 0 && !File.Exists(configPath) && File.Exists(defaultConfigPath)){
@@ -80,7 +74,7 @@ namespace TweetDuck.Plugins{
Directory.CreateDirectory(dataFolder);
File.Copy(defaultConfigPath, configPath, false);
}catch(Exception e){
Program.Reporter.HandleException("Plugin Loading Error", "Could not generate a configuration file for '"+Identifier+"' plugin.", true, e);
throw new IOException("Could not generate a configuration file for '"+Identifier+"' plugin: "+e.Message, e);
}
}
}
@@ -138,89 +132,74 @@ namespace TweetDuck.Plugins{
return obj is Plugin plugin && plugin.Identifier.Equals(Identifier);
}
public static Plugin CreateFromFolder(string path, PluginGroup group, out string error){
Plugin plugin = new Plugin(path, group);
// Static
if (!LoadMetadata(path, plugin, out error)){
return null;
}
private static readonly Version AppVersion = new Version(Program.VersionTag);
private static readonly string[] EndTag = { "[END]" };
if (!LoadEnvironments(path, plugin, out error)){
return null;
}
error = string.Empty;
public static Plugin CreateFromFolder(string path, PluginGroup group){
Plugin plugin = new Plugin(path, Path.GetFileName(path), group, LoadEnvironments(path));
LoadMetadata(path, plugin);
return plugin;
}
private static bool LoadEnvironments(string path, Plugin plugin, out string error){
private static PluginEnvironment LoadEnvironments(string path){
PluginEnvironment environments = PluginEnvironment.None;
foreach(string file in Directory.EnumerateFiles(path, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName)){
plugin.Environments |= PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
environments |= PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
}
if (plugin.Environments == PluginEnvironment.None){
error = "Plugin has no script files.";
return false;
if (environments == PluginEnvironment.None){
throw new ArgumentException("Plugin has no script files");
}
error = string.Empty;
return true;
return environments;
}
private static readonly string[] endTag = { "[END]" };
private static bool LoadMetadata(string path, Plugin plugin, out string error){
private static void LoadMetadata(string path, Plugin plugin){
string metaFile = Path.Combine(path, ".meta");
if (!File.Exists(metaFile)){
error = "Missing .meta file.";
return false;
throw new ArgumentException("Missing .meta file");
}
string[] lines = File.ReadAllLines(metaFile, Encoding.UTF8);
string currentTag = null, currentContents = "";
string currentTag = null, currentContents = string.Empty;
foreach(string line in lines.Concat(endTag).Select(line => line.TrimEnd()).Where(line => line.Length > 0)){
foreach(string line in File.ReadAllLines(metaFile, Encoding.UTF8).Concat(EndTag).Select(line => line.TrimEnd()).Where(line => line.Length > 0)){
if (line[0] == '[' && line[line.Length-1] == ']'){
if (currentTag != null){
plugin.metadata[currentTag] = currentContents;
}
currentTag = line.Substring(1, line.Length-2).ToUpper();
currentContents = "";
currentContents = string.Empty;
if (line.Equals(endTag[0])){
if (line.Equals(EndTag[0])){
break;
}
if (!plugin.metadata.ContainsKey(currentTag)){
error = "Invalid metadata tag: "+currentTag;
return false;
throw new FormatException("Invalid metadata tag: "+currentTag);
}
}
else if (currentTag != null){
currentContents = currentContents.Length == 0 ? line : currentContents+"\r\n"+line;
currentContents = currentContents.Length == 0 ? line : currentContents+Environment.NewLine+line;
}
else{
error = "Missing metadata tag before value: "+line;
return false;
throw new FormatException("Missing metadata tag before value: "+line);
}
}
if (plugin.Name.Length == 0){
error = "Plugin is missing a name in the .meta file.";
return false;
throw new FormatException("Plugin is missing a name in the .meta file");
}
if (plugin.RequiredVersion.Length == 0 || !(plugin.RequiredVersion == VersionWildcard || System.Version.TryParse(plugin.RequiredVersion, out Version _))){
error = "Plugin contains invalid version: "+plugin.RequiredVersion;
return false;
throw new FormatException("Plugin contains invalid version: "+plugin.RequiredVersion);
}
plugin.OnMetadataLoaded();
error = string.Empty;
return true;
}
private static bool CheckRequiredVersion(string requires){

View File

@@ -24,10 +24,28 @@ namespace TweetDuck.Plugins{
}
}
public void ToggleEnabled(Plugin plugin){
SetEnabled(plugin, !IsEnabled(plugin));
}
public bool IsEnabled(Plugin plugin){
return !disabled.Contains(plugin.Identifier);
}
public void Save(string file){
try{
using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8)){
writer.WriteLine("#Disabled");
foreach(string identifier in disabled){
writer.WriteLine(identifier);
}
}
}catch(Exception e){
Program.Reporter.HandleException("Plugin Configuration Error", "Could not save the plugin configuration file.", true, e);
}
}
public void Load(string file){
try{
using(StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8)){
@@ -43,30 +61,12 @@ namespace TweetDuck.Plugins{
}
}catch(FileNotFoundException){
disabled.Clear();
foreach(string identifier in DefaultDisabled){
disabled.Add(identifier);
}
disabled.UnionWith(DefaultDisabled);
Save(file);
}catch(DirectoryNotFoundException){
}catch(Exception e){
Program.Reporter.HandleException("Plugin Configuration Error", "Could not read the plugin configuration file. If you continue, the list of disabled plugins will be reset to default.", true, e);
}
}
public void Save(string file){
try{
using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8)){
writer.WriteLine("#Disabled");
foreach(string identifier in disabled){
writer.WriteLine(identifier);
}
}
}catch(Exception e){
Program.Reporter.HandleException("Plugin Configuration Error", "Could not save the plugin configuration file.", true, e);
}
}
}
}

View File

@@ -9,8 +9,6 @@ using TweetDuck.Resources;
namespace TweetDuck.Plugins{
sealed class PluginManager{
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") },
@@ -35,8 +33,6 @@ namespace TweetDuck.Plugins{
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
private readonly Random rand = new Random();
private List<string> loadErrors;
public PluginManager(string rootPath, string configPath){
this.rootPath = rootPath;
this.configPath = configPath;
@@ -68,7 +64,18 @@ namespace TweetDuck.Plugins{
}
}
return InvalidToken;
int token, attempts = 1000;
do{
token = rand.Next();
}while(tokens.ContainsKey(token) && --attempts >= 0);
if (attempts < 0){
token = -tokens.Count-1;
}
tokens[token] = plugin;
return token;
}
public Plugin GetPluginFromToken(int token){
@@ -81,28 +88,41 @@ namespace TweetDuck.Plugins{
plugins.Clear();
tokens.Clear();
loadErrors = new List<string>(2);
List<string> loadErrors = new List<string>(2);
foreach(Plugin plugin in LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official)){
plugins.Add(plugin);
IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
if (!Directory.Exists(path)){
yield break;
}
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
Plugin plugin;
try{
plugin = Plugin.CreateFromFolder(fullDir, group);
}catch(Exception e){
loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+e.Message);
continue;
}
yield return plugin;
}
}
foreach(Plugin plugin in LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom)){
plugins.Add(plugin);
}
plugins.UnionWith(LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official));
plugins.UnionWith(LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom));
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
}
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);
if (!HasAnyPlugin(environment)){
return;
}
}
private void ExecutePluginScripts(IFrame frame, PluginEnvironment environment){
ScriptLoader.ExecuteScript(frame, PluginSetupScripts[environment], environment.GetScriptIdentifier());
ScriptLoader.ExecuteScript(frame, PluginSetupScripts[PluginEnvironment.None], PluginEnvironment.None.GetScriptIdentifier());
bool includeDisabled = environment.IncludesDisabledPlugins();
if (includeDisabled){
@@ -113,7 +133,10 @@ namespace TweetDuck.Plugins{
foreach(Plugin plugin in Plugins){
string path = plugin.GetScriptPath(environment);
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun)continue;
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun){
continue;
}
string script;
@@ -124,49 +147,10 @@ namespace TweetDuck.Plugins{
continue;
}
int token;
if (tokens.ContainsValue(plugin)){
token = GetTokenFromPlugin(plugin);
}
else{
token = GenerateToken();
tokens[token] = plugin;
}
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, token, environment), "plugin:"+plugin);
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, GetTokenFromPlugin(plugin), environment), "plugin:"+plugin);
}
Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
}
private IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
if (!Directory.Exists(path)){
yield break;
}
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
Plugin plugin = Plugin.CreateFromFolder(fullDir, group, out string error);
if (plugin == null){
loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+error);
}
else{
yield return plugin;
}
}
}
private int GenerateToken(){
for(int attempt = 0; attempt < 1000; attempt++){
int token = rand.Next();
if (!tokens.ContainsKey(token) && token != InvalidToken){
return token;
}
}
return -tokens.Count;
}
}
}

View File

@@ -14,14 +14,13 @@ using TweetDuck.Core.Other.Settings.Export;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Updates;
using TweetLib.Communication;
namespace TweetDuck{
static class Program{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.10";
public const string VersionTag = "1.11.1";
public static readonly bool IsPortable = File.Exists("makeportable");
@@ -37,6 +36,7 @@ namespace TweetDuck{
public static string UserConfigFilePath => Path.Combine(StoragePath, "TD_UserConfig.cfg");
public static string SystemConfigFilePath => Path.Combine(StoragePath, "TD_SystemConfig.cfg");
public static string PluginConfigFilePath => Path.Combine(StoragePath, "TD_PluginConfig.cfg");
public static string AnalyticsFilePath => Path.Combine(StoragePath, "TD_Analytics.cfg");
private static string ErrorLogFilePath => Path.Combine(StoragePath, "TD_Log.txt");
private static string ConsoleLogFilePath => Path.Combine(StoragePath, "TD_Console.txt");
@@ -69,9 +69,10 @@ namespace TweetDuck{
private static void Main(){
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Cef.EnableHighDPISupport();
WindowRestoreMessage = Comms.RegisterMessage("TweetDuckRestore");
SubProcessMessage = Comms.RegisterMessage("TweetDuckSubProcess");
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
SubProcessMessage = NativeMethods.RegisterWindowMessage("TweetDuckSubProcess");
if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
@@ -145,7 +146,6 @@ namespace TweetDuck{
CommandLineArgs.ReadCefArguments(UserConfig.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
Cef.EnableHighDPISupport();
Cef.Initialize(settings, false, new BrowserProcessHandler());
Application.ApplicationExit += (sender, args) => ExitCleanup();
@@ -166,8 +166,12 @@ namespace TweetDuck{
string updaterArgs = "/SP- /SILENT /CLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath);
WindowsUtils.StartProcess(mainForm.UpdateInstallerPath, updaterArgs, runElevated);
Application.Exit();
if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
Application.Exit();
}
else{
RestartWithArgsInternal(Arguments.GetCurrentClean());
}
}
}
@@ -211,11 +215,14 @@ namespace TweetDuck{
FormBrowser browserForm = Application.OpenForms.OfType<FormBrowser>().FirstOrDefault();
if (browserForm == null)return;
args.AddFlag(Arguments.ArgRestart);
browserForm.ForceClose();
ExitCleanup();
ExitCleanup();
RestartWithArgsInternal(args);
}
private static void RestartWithArgsInternal(CommandLineArgs args){
args.AddFlag(Arguments.ArgRestart);
Process.Start(Application.ExecutablePath, args.ToString());
Application.Exit();
}

View File

@@ -1,3 +1,7 @@
# Support
[Follow TweetDuck on Twitter](https://twitter.com/TryTweetDuck) &nbsp;|&nbsp; [Support via PayPal](https://paypal.me/chylex) &nbsp;|&nbsp; [Support via Patreon](https://www.patreon.com/chylex)
# Build Instructions
### Setup
@@ -25,21 +29,34 @@ To do that, open **TweetDuck Properties**, click the **Debug** tab, make sure yo
### Build
To make a release build of TweetDuck, open **Batch Build**, tick all `Release` configurations except for the `UnitTest` project (otherwise the build will fail), and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, open the **Output** view and see which part of the build failed.
To make a release build of TweetDuck, open **Batch Build**, tick all `Release` configurations except for the `UnitTest` project (otherwise the build will fail), and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the [Troubleshooting](#troubleshooting) section.
After the build succeeds, the **bin/x86/Release** folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#Installers) section for automated installer generation.
After the build succeeds, the `bin/x86/Release` folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#installers) section for automated installer generation.
If you decide to release a custom version publicly, please make it clear that it is not an official release of TweetDuck.
### Troubleshooting
There are a few quirks in the build process that may catch you off guard:
- **Plugin files are not updated automatically**
- Since official plugins (`Resources/Plugins`) are not included in the project, Visual Studio will not automatically detect changes in the files
- To ensure plugins are updated when testing the app, click **Rebuild Solution** before clicking **Start**
- **Error: The command (...) exited with code 1**
- If the post-build event fails, open the **Output** tab and look for the cause
- Determine if there was an IO error while copying files or modifying folders, or whether the final .ps1 script failed (`Encountered an error while running PostBuild.ps1 on line xyz`)
- Some files are checked for invalid characters:
- `Resources/Plugins/emoji-keyboard/emoji-ordering.txt` line endings must be LF (line feed); any CR (carriage return) in the file will cause a failed build, and you will need to ensure correct line endings in your text editor
### Installers
TweetDuck uses **Inno Setup** to automate the creation of installers. First, download and install [InnoSetup QuickStart Pack](http://www.jrsoftware.org/isdl.php) (non-unicode; editor and encryption support not required) and the [Inno Download Plugin](https://code.google.com/archive/p/inno-download-plugin).
Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer for the change to take place.
Now you can generate installers after a build by running **bld/RUN BUILD.bat**. Note that despite the name, this will only package the files, you still need to run the [build](#Build) in Visual Studio!
Now you can generate installers after a build by running `bld/RUN BUILD.bat`. Note that despite the name, this will only package the files, you still need to run the [build](#build) in Visual Studio!
After the window closes, three installers will be generated inside the **bld/Output** folder:
After the window closes, three installers will be generated inside the `bld/Output` folder:
* **TweetDuck.exe**
* This is the main installer that creates entries in the Start Menu & Programs and Features, and an optional desktop icon
* **TweetDuck.Update.exe**

View File

@@ -28,7 +28,7 @@
<img src="img/app-menu.png" alt="">
<p>There are two ways to open the main menu:</p>
<ul>
<li>Click <em>Settings</em> in the left panel, then select <em>TweetDuck</em></li>
<li>Click <em>Settings</em> in the left panel and select <em>TweetDuck</em>, or...</li>
<li><em>Right-click anywhere</em> and you will either see the listed options, or a <em>TweetDuck</em> entry that contains these options</li>
</ul>
<img src="img/settings-dropdown.png" alt="">
@@ -57,9 +57,10 @@
<details>
<summary id="emoji">How to add emoji to tweets</summary>
<div>
<p>When writing a new tweet, click the heart icon to open an emoji picker. If you're writing a reply, click the <em>Popout</em> icon first to bring the reply into the large panel.</p>
<p>When writing a new tweet, click the heart icon to open an emoji picker. If you're writing a reply, click the <em>Popout</em> icon to bring the reply into the New Tweet panel.</p>
<p>Then you can immediately type into the <em>Search</em> field, which accepts keywords separated by space. Pressing <em>Enter</em> in the search field (when not empty) will insert the first result into your tweet. Pressing <em>Escape</em> closes the emoji picker.</p>
<p>You can also use your mouse to scroll through the emoji, click on the emoji to insert them, and change the skin tone.</p>
<p>You can also use your mouse to change the skin tone, scroll through the emoji, and click on the emoji to insert them.</p>
<p>Typing <em>:emoji_name:</em> in the New Tweet panel will automatically search for an emoji using those keywords. If a single emoji is found, it will be inserted into the tweet (press <em>Backspace</em> or <em>Escape</em> to revert it). If multiple emojis are found, it will open the emoji picker where you can refine the search, or press <em>Escape</em> to go back.</p>
<p>Emoji are provided by an official plugin called <em>Emoji keyboard</em>, which is enabled by default. The heart icon will not show if the plugin is disabled.</p>
<img src="img/new-tweet-emoji.png" alt="">
</div>
@@ -124,7 +125,7 @@
<li><em>Save image/video as...</em></li>
</ul>
<p>TweetDuck will attempt to fetch the highest quality image when you click any of these options. You can disable that by going to the <em>main menu</em>, selecting <em>Options</em>, and then unchecking <em>Best Image Quality</em>.</p>
<p>Whenever possible, the username and quality are included in the filename by default, for convenience. After you select a folder and click Save, the image/video will be downloaded in the background. There is no notification for when the download finishes, but you will be notified if it fails.</p>
<p>Whenever possible, the username and quality are included in the filename by default for convenience. After you select a folder and click Save, the image/video will be downloaded in the background. There is no notification for when the download finishes, but you will be notified if it fails.</p>
</div>
</details>
@@ -330,10 +331,10 @@
</details>
<details>
<summary id="popout-replies">How to instantly popout replies</summary>
<summary id="popout-replies">How to instantly popout replies or quote tweets</summary>
<div>
<p><em>Middle-click the reply icon</em> to instantly open your reply in the New Tweet panel.</p>
<p>Middle-clicks are usually done by pressing your mouse wheel as if it was a button. When using a laptop touchpad or certain mice, the ways of triggering a middle click vary.</p>
<p><em>Middle-click the reply icon</em> to instantly open your reply in the New Tweet panel, or <em>middle-click the retweet icon</em> to quote a tweet instead of retweeting it.</p>
<p>Middle-clicking is usually done by pressing your mouse wheel as if it was a button. Some mice or other devices, such as laptop touchpads, may have a dedicated button or button combination instead.</p>
</div>
</details>

View File

@@ -8,10 +8,10 @@ Edit layout & design
chylex
[version]
1.1.3
1.1.7
[website]
https://tweetduck.chylex.com
[requires]
1.7
1.10.2

View File

@@ -6,12 +6,15 @@ enabled(){
this.config = null;
this.defaultConfig = {
_theme: "light",
columnWidth: "310px",
fontSize: "12px",
hideTweetActions: true,
moveTweetActionsToRight: true,
themeColorTweaks: true,
revertIcons: true,
showCharacterCount: true,
increaseQuoteTextSize: false,
smallComposeTextSize: false,
optimizeAnimations: true,
avatarRadius: 2
@@ -37,11 +40,13 @@ enabled(){
this.currentStage = 1;
}
else if (this.tmpConfig !== null){
let needsResave = !("_theme" in this.tmpConfig);
this.config = $.extend(this.defaultConfig, this.tmpConfig);
this.tmpConfig = null;
this.reinjectAll();
if (this.firstTimeLoad){
if (this.firstTimeLoad || needsResave){
$TDP.writeFile(this.$token, configFile, JSON.stringify(this.config));
}
}
@@ -59,6 +64,8 @@ enabled(){
}
else{
$(document).one("dataSettingsValues", () => {
this.defaultConfig._theme = TD.settings.getTheme();
switch(TD.settings.getColumnWidth()){
case "wide": this.defaultConfig.columnWidth = "350px"; break;
case "narrow": this.defaultConfig.columnWidth = "270px"; break;
@@ -172,28 +179,44 @@ enabled(){
}
// SELECTS
else if (tag === "SELECT"){
if (!item.val(me.config[key]).val()){
let custom = item.find("option[value='custom']");
let optionCustom = item.find("option[value^='custom']");
if (custom.length === 1){
item.val("custom");
custom.text(getTextForCustom(key));
let resetMyValue = () => {
if (!item.val(me.config[key]).val() && optionCustom.length === 1){
item.val(optionCustom.attr("value"));
optionCustom.text(getTextForCustom(key));
}
}
};
resetMyValue();
item.change(function(){ // TODO change doesn't fire when Custom is already selected
let val = item.val();
if (val === "custom"){
val = prompt("Enter custom value:");
if (val === "custom-px"){
val = (prompt("Enter custom value (px):") || "").trim();
if (val){
updateKey(key, val);
item.find("option[value='custom']").text(getTextForCustom(key));
if (val.endsWith("px")){
val = val.slice(0, -2).trim();
}
if (/^[0-9]+$/.test(val)){
updateKey(key, val+"px");
optionCustom.text(getTextForCustom(key));
}
else{
alert("Invalid value, only px values are supported.");
resetMyValue();
}
}
else{
resetMyValue();
}
}
else{
updateKey(key, item.val());
optionCustom.text("Custom");
}
});
}
@@ -217,9 +240,11 @@ enabled(){
modal.find("[data-td-theme='"+TD.settings.getTheme()+"']").prop("checked", true);
modal.find("[data-td-theme]").change(function(){
me.config._theme = $(this).attr("data-td-theme");
setTimeout(function(){
TD.settings.setTheme($(this).attr("data-td-theme"));
$(document).trigger("uiToggleTheme");
me.saveConfig();
me.reinjectAll();
}, 1);
});
@@ -331,7 +356,7 @@ enabled(){
this.css.insert("#general_settings .cf { display: none !important }");
this.css.insert("#settings-modal .js-setting-list li:nth-child(3) { border-bottom: 1px solid #ccd6dd }");
this.css.insert(".txt-base-smallest:not(.icon), .txt-base-largest:not(.icon) { font-size: "+this.config.fontSize+" !important }");
this.css.insert("html[data-td-font] { font-size: "+this.config.fontSize+" !important }");
this.css.insert(".avatar { border-radius: "+this.config.avatarRadius+"% !important }");
let notificationScrollbarColor = null;
@@ -339,31 +364,39 @@ enabled(){
if (this.config.themeColorTweaks){
switch(TD.settings.getTheme()){
case "dark":
this.css.insert(".app-content, .app-columns-container { background-color: #444448 }");
this.css.insert(".column-drag-handle { opacity: 0.5 }");
this.css.insert(".column-drag-handle:hover { opacity: 1 }");
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-thumb, .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-thumb { background-color: #666 }");
this.css.insert(".app-content, .app-columns-container { background-color: #444448 !important }");
this.css.insert(".column-drag-handle { opacity: 0.5 !important }");
this.css.insert(".column-drag-handle:hover { opacity: 1 !important }");
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-thumb, .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-thumb { background-color: #666 !important }");
notificationScrollbarColor = "666";
break;
case "light":
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-thumb, .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-thumb { background-color: #d2d6da }");
this.css.insert(".app-columns-container.scroll-styled-h::-webkit-scrollbar-thumb:not(:hover) { background-color: #a5aeb5 }");
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-thumb, .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-thumb { background-color: #d2d6da !important }");
this.css.insert(".app-columns-container.scroll-styled-h::-webkit-scrollbar-thumb:not(:hover) { background-color: #a5aeb5 !important }");
notificationScrollbarColor = "a5aeb5";
break;
}
}
if (this.config.showCharacterCount){
this.css.insert("#tduck .js-character-count.is-hidden { display: inline !important }");
}
if (this.config.hideTweetActions){
this.css.insert(".tweet-action { opacity: 0; }");
this.css.insert(".tweet-actions.is-visible .tweet-action { opacity: 0.5; }");
this.css.insert(".tweet-actions.is-visible .tweet-action { opacity: 0.5 }");
this.css.insert(".is-favorite .tweet-action, .is-retweet .tweet-action { opacity: 0.5; visibility: visible !important }");
this.css.insert(".tweet:hover .tweet-action, .tweet-action.is-selected, .is-favorite .tweet-action[rel='favorite'], .is-retweet .tweet-action[rel='retweet'] { opacity: 1 !important; visibility: visible !important }");
}
if (this.config.moveTweetActionsToRight){
this.css.insert(".tweet-actions { float: right !important; width: auto !important }");
this.css.insert(".tweet-actions > li:nth-child(4) { margin-right: 2px !important }");
this.css.insert("#tduck .tweet-actions { float: right !important; width: auto !important }");
this.css.insert("#tduck .tweet-actions > li:nth-child(4) { margin-right: 2px !important }");
}
if (this.config.increaseQuoteTextSize){
this.css.insert(".quoted-tweet { font-size: 1em !important }");
}
if (this.config.smallComposeTextSize){
@@ -371,96 +404,100 @@ enabled(){
}
if (this.config.revertIcons){
let iconData = [
[ ".icon-twitter-bird", "00" ],
[ ".icon-mention", "01" ],
[ ".icon-following", "02" ],
[ ".icon-message", "03" ],
[ ".icon-home", "04" ],
[ ".icon-hashtag", "05" ],
[ ".icon-reply", "06" ],
[ ".icon-favorite", "55" ],
[ ".icon-retweet", "08" ],
[ ".icon-drafts", "09" ],
[ ".icon-search", "0a" ],
[ ".icon-trash", "0c" ],
[ ".icon-close", "0d" ],
[ ".icon-arrow-r:before,.Icon--caretRight", "0e" ],
[ ".icon-arrow-l:before,.Icon--caretLeft", "0f" ],
[ ".icon-protected", "13" ],
[ ".icon-list", "14" ],
[ ".icon-camera", "15" ],
[ ".icon-more", "16" ],
[ ".icon-settings", "18" ],
[ ".icon-notifications", "19" ],
[ ".icon-user-dd", "1a" ],
[ ".icon-activity", "1c" ],
[ ".icon-trending", "1d" ],
[ ".icon-minus", "1e" ],
[ ".icon-plus", "1f" ],
[ ".icon-geo", "20" ],
[ ".icon-check", "21" ],
[ ".icon-schedule", "22" ],
[ ".icon-dot", "23" ],
[ ".icon-user", "24" ],
[ ".icon-content", "25" ],
[ ".icon-arrow-d:before,.Icon--caretDown", "26" ],
[ ".icon-arrow-u", "27" ],
[ ".icon-share", "28" ],
[ ".icon-info", "29" ],
[ ".icon-verified", "2a" ],
[ ".icon-translator", "2b" ],
[ ".icon-blocked", "2c" ],
[ ".icon-constrain", "2d" ],
[ ".icon-play-video", "2e" ],
[ ".icon-empty", "2f" ],
[ ".icon-clear-input", "30" ],
[ ".icon-compose", "31" ],
[ ".icon-mark-read", "32" ],
[ ".icon-arrow-r-double", "33" ],
[ ".icon-arrow-l-double", "34" ],
[ ".icon-follow", "35" ],
[ ".icon-image", "36" ],
[ ".icon-popout", "37" ],
[ ".icon-move", "39" ],
[ ".icon-compose-grid", "3a" ],
[ ".icon-compose-minigrid", "3b" ],
[ ".icon-compose-list", "3c" ],
[ ".icon-edit", "40" ],
[ ".icon-clear-timeline", "41" ],
[ ".icon-sliders", "42" ],
[ ".icon-custom-timeline", "43" ],
[ ".icon-compose-dm", "44" ],
[ ".icon-bg-dot", "45" ],
[ ".icon-user-team-mgr", "46" ],
[ ".icon-user-switch", "47" ],
[ ".icon-conversation", "48" ],
[ ".icon-dataminr", "49" ],
[ ".icon-link", "4a", ],
[ ".icon-flash", "50" ],
[ ".icon-pointer-u", "51" ],
[ ".icon-analytics", "54" ],
[ ".icon-heart", "55" ],
[ ".icon-calendar", "56" ],
[ ".icon-attachment", "57" ],
[ ".icon-play", "58" ],
[ ".icon-bookmark", "59" ],
[ ".icon-play-badge", "60" ],
[ ".icon-gif-badge", "61" ],
[ ".icon-poll", "62" ],
[ ".icon-heart-filled", "55" ],
[ ".icon-retweet-filled", "08" ],
[ ".icon-list-filled", "14" ],
[ ".icon-user-filled", "35" ],
];
this.icons = document.createElement("style");
this.icons.innerHTML = `
@font-face {
font-family: 'tweetdeckold';
font-family: '_of';
src: url("https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.icon-twitter-bird:before{content:"\\f000";font-family:tweetdeckold}
.icon-mention:before{content:"\\f001";font-family:tweetdeckold}
.icon-following:before{content:"\\f002";font-family:tweetdeckold}
.icon-message:before{content:"\\f003";font-family:tweetdeckold}
.icon-home:before{content:"\\f004";font-family:tweetdeckold}
.icon-hashtag:before{content:"\\f005";font-family:tweetdeckold}
.icon-reply:before{content:"\\f006";font-family:tweetdeckold}
.icon-favorite:before{content:"\\f055";font-family:tweetdeckold}
.icon-retweet:before{content:"\\f008";font-family:tweetdeckold}
.icon-drafts:before{content:"\\f009";font-family:tweetdeckold}
.icon-search:before{content:"\\f00a";font-family:tweetdeckold}
.icon-trash:before{content:"\\f00c";font-family:tweetdeckold}
.icon-close:before{content:"\\f00d";font-family:tweetdeckold}
.icon-arrow-r:before,.Icon--caretRight:before{content:"\\f00e";font-family:tweetdeckold}
.icon-arrow-l:before,.Icon--caretLeft:before{content:"\\f00f";font-family:tweetdeckold}
.icon-protected:before{content:"\\f013";font-family:tweetdeckold}
.icon-list:before{content:"\\f014";font-family:tweetdeckold}
.icon-camera:before{content:"\\f015";font-family:tweetdeckold}
.icon-more:before{content:"\\f016";font-family:tweetdeckold}
.icon-settings:before{content:"\\f018";font-family:tweetdeckold}
.icon-notifications:before{content:"\\f019";font-family:tweetdeckold}
.icon-user-dd:before{content:"\\f01a";font-family:tweetdeckold}
.icon-activity:before{content:"\\f01c";font-family:tweetdeckold}
.icon-trending:before{content:"\\f01d";font-family:tweetdeckold}
.icon-minus:before{content:"\\f01e";font-family:tweetdeckold}
.icon-plus:before{content:"\\f01f";font-family:tweetdeckold}
.icon-geo:before{content:"\\f020";font-family:tweetdeckold}
.icon-check:before{content:"\\f021";font-family:tweetdeckold}
.icon-schedule:before{content:"\\f022";font-family:tweetdeckold}
.icon-dot:before{content:"\\f023";font-family:tweetdeckold}
.icon-user:before{content:"\\f024";font-family:tweetdeckold}
.icon-content:before{content:"\\f025";font-family:tweetdeckold}
.icon-arrow-d:before,.Icon--caretDown:before{content:"\\f026";font-family:tweetdeckold}
.icon-arrow-u:before{content:"\\f027";font-family:tweetdeckold}
.icon-share:before{content:"\\f028";font-family:tweetdeckold}
.icon-info:before{content:"\\f029";font-family:tweetdeckold}
.icon-verified:before{content:"\\f02a";font-family:tweetdeckold}
.icon-translator:before{content:"\\f02b";font-family:tweetdeckold}
.icon-blocked:before{content:"\\f02c";font-family:tweetdeckold}
.icon-constrain:before{content:"\\f02d";font-family:tweetdeckold}
.icon-play-video:before{content:"\\f02e";font-family:tweetdeckold}
.icon-empty:before{content:"\\f02f";font-family:tweetdeckold}
.icon-clear-input:before{content:"\\f030";font-family:tweetdeckold}
.icon-compose:before{content:"\\f031";font-family:tweetdeckold}
.icon-mark-read:before{content:"\\f032";font-family:tweetdeckold}
.icon-arrow-r-double:before{content:"\\f033";font-family:tweetdeckold}
.icon-arrow-l-double:before{content:"\\f034";font-family:tweetdeckold}
.icon-follow:before{content:"\\f035";font-family:tweetdeckold}
.icon-image:before{content:"\\f036";font-family:tweetdeckold}
.icon-popout:before{content:"\\f037";font-family:tweetdeckold}
.icon-move:before{content:"\\f039";font-family:tweetdeckold}
.icon-compose-grid:before{content:"\\f03a";font-family:tweetdeckold}
.icon-compose-minigrid:before{content:"\\f03b";font-family:tweetdeckold}
.icon-compose-list:before{content:"\\f03c";font-family:tweetdeckold}
.icon-edit:before{content:"\\f040";font-family:tweetdeckold}
.icon-clear-timeline:before{content:"\\f041";font-family:tweetdeckold}
.icon-sliders:before{content:"\\f042";font-family:tweetdeckold}
.icon-custom-timeline:before{content:"\\f043";font-family:tweetdeckold}
.icon-compose-dm:before{content:"\\f044";font-family:tweetdeckold}
.icon-bg-dot:before{content:"\\f045";font-family:tweetdeckold}
.icon-user-team-mgr:before{content:"\\f046";font-family:tweetdeckold}
.icon-user-switch:before{content:"\\f047";font-family:tweetdeckold}
.icon-conversation:before{content:"\\f048";font-family:tweetdeckold}
.icon-dataminr:before{content:"\\f049";font-family:tweetdeckold}
.icon-link:before{content:"\\f04a";font-family:tweetdeckold}
.icon-flash:before{content:"\\f050";font-family:tweetdeckold}
.icon-pointer-u:before{content:"\\f051";font-family:tweetdeckold}
.icon-analytics:before{content:"\\f054";font-family:tweetdeckold}
.icon-heart:before{content:"\\f055";font-family:tweetdeckold}
.icon-calendar:before{content:"\\f056";font-family:tweetdeckold}
.icon-attachment:before{content:"\\f057";font-family:tweetdeckold}
.icon-play:before{content:"\\f058";font-family:tweetdeckold}
.icon-bookmark:before{content:"\\f059";font-family:tweetdeckold}
.icon-play-badge:before{content:"\\f060";font-family:tweetdeckold}
.icon-gif-badge:before{content:"\\f061";font-family:tweetdeckold}
.icon-poll:before{content:"\\f062";font-family:tweetdeckold}
.icon-heart-filled:before{content:"\\f055";font-family:tweetdeckold}
.icon-retweet-filled:before{content:"\\f008";font-family:tweetdeckold}
.icon-list-filled:before{content:"\\f014";font-family:tweetdeckold}
.icon-user-filled:before{content:"\\f035";font-family:tweetdeckold}
${iconData.map(entry => `#tduck ${entry[0]}:before{content:\"\\f0${entry[1]}\";font-family:_of!important}`).join("")}
.drawer .btn .icon, .app-header .btn .icon { line-height: 1em !important }
.column-header .column-type-icon { bottom: 26px !important }
@@ -507,21 +544,25 @@ enabled(){
$TDP.injectIntoNotificationsBefore(this.$token, "css", "</head>", `
<style type='text/css'>
.txt-base-smallest:not(.icon), .txt-base-largest:not(.icon) { font-size: ${this.config.fontSize} !important }
html[data-td-font] { font-size: ${this.config.fontSize} !important }
.avatar { border-radius: ${this.config.avatarRadius}% !important }
${this.config.increaseQuoteTextSize ? `
.quoted-tweet { font-size: 1em !important }
` : ``}
${this.config.revertIcons ? `
@font-face { font-family: 'tweetdeckold'; src: url(\"https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff\") format(\"woff\"); font-weight: normal; font-style: normal }
.icon-reply:before{content:"\\f006";font-family:tweetdeckold}
.icon-heart-filled:before{content:"\\f055";font-family:tweetdeckold}
.icon-retweet-filled:before{content:"\\f008";font-family:tweetdeckold}
.icon-list-filled:before{content:"\\f014";font-family:tweetdeckold}
.icon-user-filled:before{content:"\\f035";font-family:tweetdeckold}
.icon-user-dd:before{content:"\\f01a";font-family:tweetdeckold}
@font-face { font-family: '_of'; src: url(\"https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff\") format(\"woff\"); font-weight: normal; font-style: normal }
#tduck .icon-reply:before{content:"\\f006";font-family:_of!important}
#tduck .icon-heart-filled:before{content:"\\f055";font-family:_of!important}
#tduck .icon-retweet-filled:before{content:"\\f008";font-family:_of!important}
#tduck .icon-list-filled:before{content:"\\f014";font-family:_of!important}
#tduck .icon-user-filled:before{content:"\\f035";font-family:_of!important}
#tduck .icon-user-dd:before{content:"\\f01a";font-family:_of!important}
` : ``}
${notificationScrollbarColor ? `
.scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb { background-color: #${notificationScrollbarColor} }
.scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb { background-color: #${notificationScrollbarColor} !important }
` : ``}
</style>`);
};

View File

@@ -1,4 +1,4 @@
<div class="td-modal-panel js-modal-panel mdl s-tall-fixed is-inverted-dark">
<div id="edit-design-panel" class="js-modal-panel mdl s-tall-fixed is-inverted-dark">
<header class="js-mdl-header mdl-header js-drag-handle">
<h3 class="mdl-header-title js-header-title">TweetDuck - Layout &amp; Design</h3>
<a href="#" class="mdl-dismiss js-dismiss link-normal-dark">
@@ -6,8 +6,8 @@
</a>
</header>
<div class="mdl-inner">
<div class="td-modal-content mdl-content js-mdl-content horizontal-flow-container">
<div class="td-modal-inner-cols">
<div id="edit-design-panel-content" class="mdl-content js-mdl-content horizontal-flow-container">
<div id="edit-design-panel-inner-cols">
<div class="l-column mdl-column">
<!-- THEME -->
@@ -36,7 +36,7 @@
<option value="310px">Medium (310px)</option>
<option value="350px">Wide (350px)</option>
<option value="400px">Extreme (400px)</option>
<option value="custom">Custom</option>
<option value="custom-px">Custom</option>
</optgroup>
<option disabled></option>
<optgroup label="Dynamic width">
@@ -59,17 +59,15 @@
<option value="14px">Medium (14px)</option>
<option value="15px">Large (15px)</option>
<option value="16px">Largest (16px)</option>
<option value="custom">Custom</option>
<option value="custom-px">Custom</option>
</select>
<!-- ADVANCED -->
<label class="txt-uppercase touch-larger-label">
<b>Advanced</b>
<label class="checkbox">
<input data-td-key="increaseQuoteTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox">
Increase quoted tweet font size
</label>
<label class="checkbox">
<input data-td-key="optimizeAnimations" class="js-theme-checkbox touch-larger-label" type="checkbox">
Use more memory for smoother animations
<input data-td-key="smallComposeTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox">
Small tweet input font size
</label>
</div>
@@ -94,17 +92,27 @@
<label class="txt-uppercase touch-larger-label">
<b>Design</b>
</label>
<label class="checkbox">
<input data-td-key="themeColorTweaks" class="js-theme-checkbox touch-larger-label" type="checkbox">
Theme color tweaks
</label>
<label class="checkbox">
<input data-td-key="revertIcons" class="js-theme-checkbox touch-larger-label" type="checkbox">
Revert icon design
</label>
<label class="checkbox">
<input data-td-key="smallComposeTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox">
Small compose tweet font size
<input data-td-key="showCharacterCount" class="js-theme-checkbox touch-larger-label" type="checkbox">
Always show character count
</label>
<label class="checkbox">
<input data-td-key="themeColorTweaks" class="js-theme-checkbox touch-larger-label" type="checkbox">
Theme color tweaks
</label>
<!-- ADVANCED -->
<label class="txt-uppercase touch-larger-label">
<b>Advanced</b>
</label>
<label class="checkbox">
<input data-td-key="optimizeAnimations" class="js-theme-checkbox touch-larger-label" type="checkbox">
Use more memory for smoother animations
</label>
</div>
@@ -151,54 +159,55 @@
/* Containers */
.td-modal-panel {
#edit-design-panel {
width: 693px;
height: 380px;
}
.td-modal-inner-cols {
#edit-design-panel-inner-cols {
padding: 0 6px;
}
.td-modal-inner-cols .l-column {
#edit-design-panel-inner-cols .l-column {
padding: 15px 9px;
box-sizing: border-box;
width: 225px;
font-size: 0; /* fix custom font size breaking the modal layout */
}
.td-modal-inner-cols .l-column:nth-child(2) {
width: 250px;
}
.td-modal-inner-full {
padding: 15px;
}
.td-modal-inner-full .txt-center {
margin-bottom: 10px;
#edit-design-panel-inner-cols .l-column:nth-child(3) {
width: 200px;
}
/* Elements */
.td-modal-content label {
#edit-design-panel-content label.txt-uppercase {
margin-top: 18px;
}
.td-modal-content label:first-child {
#edit-design-panel-content label.txt-uppercase:first-child {
margin-top: 0;
}
.td-modal-content label.radio {
#edit-design-panel-content label.radio {
display: inline-block;
margin: 0 16px 5px 4px;
cursor: pointer;
}
.td-modal-content label.checkbox {
#edit-design-panel-content label.checkbox {
margin: 0 0 5px 4px;
cursor: pointer;
}
#edit-design-panel-content select + label.checkbox {
margin-top: 9px;
}
#edit-design-panel-content input.js-theme-checkbox, #edit-design-panel-content input.js-theme-radio {
margin-top: 1px;
}
/* Avatar shape */
.td-avatar-shape-container {

View File

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

View File

@@ -303,23 +303,26 @@ enabled(){
let firstColon = val.lastIndexOf(':', ele[0].selectionStart);
return if firstColon === -1;
let search = val.substring(firstColon+1, ele[0].selectionStart);
return if !/^[a-z_]+$/i.test(search);
let search = val.substring(firstColon+1, ele[0].selectionStart).toLowerCase();
return if !/^[a-z_]+$/.test(search);
let keywords = search.split("_").filter(kw => kw.length > 0).map(kw => kw.toLowerCase());
return if keywords.length === 0;
let foundName = me.emojiNames.filter(name => keywords.every(kw => name.includes(kw)));
let foundNames = me.emojiNames.filter(name => keywords.every(kw => name.includes(kw)));
if (foundName.length === 0){
if (foundNames.length === 0){
return;
}
else if (foundNames.length > 1 && foundNames.includes(search)){
foundNames = [ search ];
}
lastEmojiKeyword = `:${search}:`;
lastEmojiPosition = lastEmojiLength = 0;
if (foundName.length === 1){
let foundIndex = me.emojiNames.indexOf(foundName[0]);
if (foundNames.length === 1){
let foundIndex = me.emojiNames.indexOf(foundNames[0]);
let foundEmoji;
for(let array of [ me.emojiData1, me.emojiData2[me.selectedSkinTone], me.emojiData3 ]){
@@ -346,7 +349,7 @@ enabled(){
lastEmojiLength = foundEmoji.length;
}
}
else if (foundName.length > 1){
else if (foundNames.length > 1){
e.preventDefault();
ele.val(val.substring(0, firstColon)+val.substring(ele[0].selectionStart));
ele[0].selectionEnd = ele[0].selectionStart = firstColon;

View File

@@ -8,7 +8,7 @@ Custom reply account
chylex
[version]
1.2.2
1.2.4
[website]
https://tweetduck.chylex.com
@@ -20,4 +20,4 @@ configuration.js
configuration.default.js
[requires]
1.3.3
1.10.3

View File

@@ -6,7 +6,7 @@ enabled(){
this.lastSelectedAccount = null;
this.uiComposeTweetEvent = (e, data) => {
return if data.type !== "reply" || data.popFromInline || !("element" in data);
return if !(data.type === "reply" || (data.type === "tweet" && "quotedTweet" in data)) || data.popFromInline || !("element" in data);
var query;
@@ -17,7 +17,7 @@ enabled(){
return;
}
var section = data.element.closest("section.column");
var section = data.element.closest("section.js-column");
var column = TD.controller.columnManager.get(section.attr("data-column"));
var header = $(".column-title", section);
@@ -35,7 +35,7 @@ enabled(){
}
try{
query = configuration.customSelector(column.getColumnType(), columnTitle, columnAccount, column);
query = configuration.customSelector(column.getColumnType(), columnTitle, columnAccount, column, section.hasClass("column-temp"));
}catch(e){
$TD.alert("warning", "Plugin reply-account has invalid configuration: customSelector threw an error: "+e.message);
return;
@@ -117,19 +117,9 @@ enabled(){
}
ready(){
var events = $._data(document, "events");
for(var event of [ "uiInlineComposeTweet", "uiDockedComposeTweet" ]){
for(let event of [ "uiInlineComposeTweet", "uiDockedComposeTweet" ]){
$(document).on(event, this.uiComposeTweetEvent);
var handlers = events[event];
var newHandler = handlers[handlers.length-1];
for(var index = handlers.length-1; index > 0; index--){
handlers[index] = handlers[index-1];
}
handlers[0] = newHandler;
window.TDPF_prioritizeNewestEvent(document, event);
}
$(document).on("click", ".js-account-list .js-account-item", this.onSelectedAccountChanged);

View File

@@ -21,11 +21,14 @@
* This assumes a basic knowledge of JavaScript and jQuery.
*
* 1. Set value of 'useAdvancedSelector' to true
* 2. Uncomment the 'customSelector' function, and replace the example code with your desired behavior
* 2. Replace the example code in 'customSelector' function with your desired behavior
*
* The 'customSelector' function should return a string in one of the formats supported by 'defaultAccount'.
* If it returns anything else (for example, false or undefined), it falls back to 'defaultAccount' behavior.
*
* When writing a custom function, you can install Dev Tools to access the browser console:
* https://tweetduck.chylex.com/guide/#dev-tools
*
*
* The 'type' parameter is TweetDeck column type. Here is the full list of column types, note that some are
* unused and have misleading names (for example, Home columns are 'col_timeline' instead of 'col_home'):
@@ -46,28 +49,33 @@
* contain other text (for example, the Scheduled column contains the string 'All accounts').
*
*
* The 'column' parameter is a TweetDeck column object. If you want to see all properties of the object,
* run the following code in your browser console, which will return an array containing all of your
* current column objects in order:
* The 'column' parameter is a TweetDeck column object. If you want to see the object structure,
* run the following code in the console for an array containing all of your column objects:
* TD.controller.columnManager.getAllOrdered()
*
*
* The 'isTemporary' parameter is true if the column is not attached to the main column list,
* for example when clicking on a profile and viewing their tweets in a modal dialog.
*
*/
useAdvancedSelector: false,
/*customSelector: function(type, title, account, column){
customSelector: function(type, title, account, column, isTemporary){
console.info(arguments); // Prints all arguments into the console
if (type === "col_search" && title === "TweetDuck"){
// This is a search column that looks for 'TweetDuck' in the tweets,
// search columns are normally linked to the preferred account
// so this forces the @TryTweetDuck account to be used instead.
// so this forces the @TryTweetDuck account to be used instead
return "@TryTweetDuck";
}
else if (type === "col_timeline" && account === "@chylexcz"){
// This is a Home column of my test account @chylexcz,
// but I want to reply to tweets from my official account.
// but I want to reply to tweets from my official account
return "@chylexmc";
}
// otherwise returns 'undefined' which falls back to 'defaultAccount' behavior
}*/
}
}

View File

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

View File

@@ -50,25 +50,25 @@ enabled(){
this.css = window.TDPF_createCustomStyle(this);
this.css.insert(".manage-templates-btn.active { color: #fff; box-shadow: 0 0 2px 3px #50a5e6; outline: 0; }");
this.css.insert(".templates-modal-wrap { width: 100%; height: 100%; padding: 49px; position: absolute; z-index: 999; box-sizing: border-box; background-color: rgba(0, 0, 0, 0.5); }");
this.css.insert(".templates-modal { width: 100%; height: 100%; background-color: #fff; display: flex; }");
this.css.insert(".templates-modal > div { display: flex; flex-direction: column; }");
this.css.insert("#templates-modal-wrap { width: 100%; height: 100%; padding: 49px; position: absolute; z-index: 999; box-sizing: border-box; background-color: rgba(0, 0, 0, 0.5); }");
this.css.insert("#templates-modal { width: 100%; height: 100%; background-color: #fff; display: flex; }");
this.css.insert("#templates-modal > div { display: flex; flex-direction: column; }");
this.css.insert(".templates-modal-bottom { flex: 0 0 auto; padding: 16px; }");
this.css.insert(".template-list .templates-modal-bottom { display: flex; justify-content: space-between; }");
this.css.insert(".template-editor .templates-modal-bottom { text-align: right; }");
this.css.insert("#template-list .templates-modal-bottom { display: flex; justify-content: space-between; }");
this.css.insert("#template-editor .templates-modal-bottom { text-align: right; }");
this.css.insert(".template-list { height: 100%; flex: 1 1 auto; }");
this.css.insert(".template-list ul { list-style-type: none; font-size: 24px; color: #222; flex: 1 1 auto; padding: 12px; overflow-y: auto; }");
this.css.insert(".template-list li { display: block; width: 100%; padding: 4px 8px; box-sizing: border-box; }");
this.css.insert(".template-list li[data-template] { cursor: pointer; }");
this.css.insert(".template-list li[data-template]:hover { background-color: #d8d8d8; }");
this.css.insert(".template-list li span { white-space: nowrap; }");
this.css.insert(".template-list li .icon { opacity: 0.6; margin-left: 4px; padding: 3px; }");
this.css.insert(".template-list li .icon:hover { opacity: 1; }");
this.css.insert(".template-list li .template-actions { float: right; }");
this.css.insert("#template-list { height: 100%; flex: 1 1 auto; }");
this.css.insert("#template-list ul { list-style-type: none; font-size: 24px; color: #222; flex: 1 1 auto; padding: 12px; overflow-y: auto; }");
this.css.insert("#template-list li { display: block; width: 100%; padding: 4px 8px; box-sizing: border-box; }");
this.css.insert("#template-list li[data-template] { cursor: pointer; }");
this.css.insert("#template-list li[data-template]:hover { background-color: #d8d8d8; }");
this.css.insert("#template-list li span { white-space: nowrap; }");
this.css.insert("#template-list li .icon { opacity: 0.6; margin-left: 4px; padding: 3px; }");
this.css.insert("#template-list li .icon:hover { opacity: 1; }");
this.css.insert("#template-list li .template-actions { float: right; }");
this.css.insert(".template-editor { height: 100%; flex: 0 0 auto; width: 25vw; min-width: 150px; max-width: 400px; background-color: #485865; }");
this.css.insert("#template-editor { height: 100%; flex: 0 0 auto; width: 25vw; min-width: 150px; max-width: 400px; background-color: #485865; }");
this.css.insert(".template-editor-form { flex: 1 1 auto; padding: 12px 16px; font-size: 14px; overflow-y: auto; }");
this.css.insert(".template-editor-form .compose-text-title { margin: 24px 0 9px; }");
this.css.insert(".template-editor-form .compose-text-title:first-child { margin-top: 0; }");
@@ -261,9 +261,9 @@ enabled(){
$(".manage-templates-btn").addClass("active");
let html = `
<div class="templates-modal-wrap">
<div class="templates-modal">
<div class="template-list">
<div id="templates-modal-wrap">
<div id="templates-modal">
<div id="template-list">
<ul></ul>
<div class="templates-modal-bottom">
@@ -272,7 +272,7 @@ enabled(){
</div>
</div>
<div class="template-editor invisible">
<div id="template-editor" class="invisible">
<div class="template-editor-form">
<div class="compose-text-title">Template Name</div>
<input name="template-name" type="text">
@@ -320,7 +320,7 @@ enabled(){
$(".js-app-content").prepend(html);
let ele = $(".templates-modal-wrap").first();
let ele = $("#templates-modal-wrap").first();
ele.on("click", "li[data-template]", function(e){
let template = me.config.templates[$(this).attr("data-template")];
@@ -332,7 +332,7 @@ enabled(){
switch($(this).attr("data-action")){
case "edit-template":
let editor = $(".template-editor");
let editor = $("#template-editor");
if (editor.hasClass("invisible")){
toggleEditor();
@@ -372,7 +372,7 @@ enabled(){
break;
case "editor-confirm":
let editor = $(".template-editor");
let editor = $("#template-editor");
if (me.editingTemplate !== null){
delete me.config.templates[me.editingTemplate];
@@ -408,15 +408,15 @@ enabled(){
};
var hideTemplateModal = function(){
$(".templates-modal-wrap").remove();
$("#templates-modal-wrap").remove();
$(".manage-templates-btn").removeClass("active");
};
var toggleEditor = function(){
let editor = $(".template-editor");
let editor = $("#template-editor");
$("[name]", editor).val("");
if ($("button[data-action='new-template']", ".template-list").add(editor).toggleClass("invisible").hasClass("invisible")){
if ($("button[data-action='new-template']", "#template-list").add(editor).toggleClass("invisible").hasClass("invisible")){
me.editingTemplate = null;
}
};
@@ -435,7 +435,7 @@ enabled(){
eles.push("<li>No templates available</li>");
}
$(".template-list").children("ul").html(eles.join(""));
$("#template-list").children("ul").html(eles.join(""));
if (save){
this.saveConfig();
@@ -445,7 +445,7 @@ enabled(){
// event handlers
this.manageTemplatesButtonClickEvent = function(e){
if ($(".templates-modal-wrap").length){
if ($("#templates-modal-wrap").length){
hideTemplateModal();
}
else{
@@ -471,7 +471,7 @@ disabled(){
this.css.remove();
$(".manage-templates-btn").remove();
$(".templates-modal-wrap").remove();
$("#templates-modal-wrap").remove();
$(document).off("uiDrawerActive", this.drawerToggleEvent);

View File

@@ -1,8 +1,4 @@
enabled(){
this.reloadColumns = () => {
Object.values(TD.controller.columnManager.getAll()).forEach(column => column.reloadTweets());
};
// styles
this.css = window.TDPF_createCustomStyle(this);
@@ -10,17 +6,17 @@ enabled(){
// utility functions
var hasPoll = function(tweet){
const hasPoll = function(tweet){
return tweet.hasPoll && tweet.hasPoll();
};
var renderTweetPoll = function(tweet){
const renderTweetPoll = function(tweet){
return `<div class='td-timeline-poll'>${TD.ui.template.render("status/poll", $.extend({}, tweet, {
chirp: tweet
}))}</div>`;
};
var renderPollHook = function(tweet, html){
const renderPollHook = function(tweet, html){
let ele = null;
if (hasPoll(tweet)){
@@ -67,7 +63,7 @@ enabled(){
};
this.prevRenderFuncs = funcs;
this.reloadColumns();
window.TDPF_reloadColumns();
}
disabled(){
@@ -76,5 +72,5 @@ disabled(){
TD.components.TweetDetailView.prototype._renderChirp = this.prevRenderFuncs.TweetDetailView;
this.css.remove();
this.reloadColumns();
window.TDPF_reloadColumns();
}

View File

@@ -3,6 +3,18 @@ $ErrorActionPreference = "Stop"
Set-Location $dir
function Check-Carriage-Return{
Param([Parameter(Mandatory = $True, Position = 1)] $fname)
$file = @(Get-ChildItem -Include $fname -Recurse)[0]
if ((Get-Content -Path $file.FullName -Raw).Contains("`r")){
Throw "$fname cannot contain carriage return"
}
Write-Host "Verified" $file.FullName.Substring($dir.Length)
}
function Rewrite-File{
[CmdletBinding()]
Param([Parameter(Mandatory = $True, ValueFromPipeline = $True)][array] $lines, [Parameter(Mandatory = $True, Position = 1)] $file)
@@ -11,23 +23,31 @@ function Rewrite-File{
Write-Host "Processed" $file.FullName.Substring($dir.Length)
}
ForEach($file in Get-ChildItem -Include *.js -Exclude configuration.default.js -Recurse){
$lines = Get-Content -Path $file.FullName
$lines = $lines | % { $_.TrimStart() }
$lines = $lines -Replace '^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$', '$1'
$lines = $lines -Replace '(?<!\w)return(\s.*?)? if (.*?);', 'if ($2)return$1;'
,$lines | Rewrite-File $file
}
try{
Check-Carriage-Return("emoji-ordering.txt")
ForEach($file in Get-ChildItem -Include *.css -Recurse){
$lines = Get-Content -Path $file.FullName
$lines = $lines -Replace '\s*/\*.*?\*/', ''
$lines = $lines -Replace '^\s+(.+):\s?(.+?)(?:\s?(!important))?;$', '$1:$2$3;'
$lines = $lines -Replace '^(\S.*?) {$', '$1{'
@(($lines | Where { $_ -ne '' }) -Join ' ') | Rewrite-File $file
}
ForEach($file in Get-ChildItem -Filter *.js -Exclude configuration.default.js -Recurse){
$lines = Get-Content -Path $file.FullName
$lines = $lines | % { $_.TrimStart() }
$lines = $lines -Replace '^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$', '$1'
$lines = $lines -Replace '(?<!\w)return(\s.*?)? if (.*?);', 'if ($2)return$1;'
,$lines | Rewrite-File $file
}
ForEach($file in Get-ChildItem -Include *.html -Recurse){
$lines = Get-Content -Path $file.FullName
,($lines | % { $_.TrimStart() }) | Rewrite-File $file
ForEach($file in Get-ChildItem -Filter *.css -Recurse){
$lines = Get-Content -Path $file.FullName
$lines = $lines -Replace '\s*/\*.*?\*/', ''
$lines = $lines -Replace '^\s+(.+):\s?(.+?)(?:\s?(!important))?;$', '$1:$2$3;'
$lines = $lines -Replace '^(\S.*?) {$', '$1{'
@(($lines | Where { $_ -ne '' }) -Join ' ') | Rewrite-File $file
}
ForEach($file in Get-ChildItem -Filter *.html -Recurse){
$lines = Get-Content -Path $file.FullName
,($lines | % { $_.TrimStart() }) | Rewrite-File $file
}
}catch{
Write-Host "Encountered an error while running PostBuild.ps1 on line" $_.InvocationInfo.ScriptLineNumber
Write-Host $_
Exit 1
}

View File

@@ -14,10 +14,15 @@
//
var onAppReady = [];
//
// Variable: DOM HTML element.
//
const doc = document.documentElement;
//
// Variable: DOM object containing the main app element.
//
var app = $(document.body).children(".js-app");
const app = $(document.body).children(".js-app");
//
// Constant: Column types mapped to their titles.
@@ -45,7 +50,7 @@
//
// Function: Prepends code at the beginning of a function. If the prepended function returns true, execution of the original function is cancelled.
//
var prependToFunction = function(func, extension){
const prependToFunction = function(func, extension){
return function(){
return extension.apply(this, arguments) === true ? undefined : func.apply(this, arguments);
};
@@ -54,7 +59,7 @@
//
// Function: Appends code at the end of a function.
//
var appendToFunction = function(func, extension){
const appendToFunction = function(func, extension){
return function(){
let res = func.apply(this, arguments);
extension.apply(this, arguments);
@@ -65,7 +70,7 @@
//
// Function: Returns true if an object has a specified property, otherwise returns false without throwing an error.
//
var ensurePropertyExists = function(obj, ...chain){
const ensurePropertyExists = function(obj, ...chain){
for(let index = 0; index < chain.length; index++){
if (!obj.hasOwnProperty(chain[index])){
$TD.crashDebug("Missing property "+chain[index]+" in chain [obj]."+chain.join("."));
@@ -81,7 +86,7 @@
//
// Function: Retrieves a property of an element with a specified class.
//
var getClassStyleProperty = function(cls, property){
const getClassStyleProperty = function(cls, property){
let column = document.createElement("div");
column.classList.add(cls);
column.style.display = "none";
@@ -96,7 +101,7 @@
//
// Function: Event callback for a new tweet.
//
var onNewTweet = (function(){
const onNewTweet = (function(){
let recentMessages = new Set();
let recentTweets = new Set();
let recentTweetTimer = null;
@@ -123,7 +128,7 @@
};
let fixMedia = (html, media) => {
return html.find(".js-media a[data-media-entity-id='"+media.mediaId+"']").css("background-image", 'url("'+media.thumb()+'")').removeClass("is-zoomable");
return html.find(".js-media a[data-media-entity-id='"+media.mediaId+"']").css("background-image", 'url("'+media.small()+'")').removeClass("is-zoomable");
};
return function(column, tweet){
@@ -214,7 +219,7 @@
// Function: Shows tweet detail, used in notification context menu.
//
(function(){
var showTweetDetailInternal = function(column, chirp){
const showTweetDetailInternal = function(column, chirp){
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp) || chirp);
TD.controller.columnManager.showColumn(column.model.privateState.key);
@@ -258,49 +263,64 @@
})();
//
// Function: Retrieves the tags to be put into <head> for notification HTML code.
// Block: Hook into settings object to detect when the settings change, and update html attributes and notification layout.
//
var getNotificationHeadContents = function(){
let tags = [];
(function(){
const refreshSettings = function(){
let fontSizeName = TD.settings.getFontSize();
let themeName = TD.settings.getTheme();
$(document.head).children("link[rel='stylesheet']:not([title]),link[title='"+TD.settings.getTheme()+"'],meta[charset],meta[http-equiv]").each(function(){
tags.push($(this)[0].outerHTML);
let tags = [
"<html "+Array.prototype.map.call(document.documentElement.attributes, ele => `${ele.name}="${ele.value}"`).join(" ")+"><head>"
];
$(document.head).children("link[href*='css/font.']:first,link[href*='css/app-"+themeName+".']:first,meta[charset],meta[http-equiv]").each(function(){
tags.push($(this)[0].outerHTML);
});
tags.push("<style type='text/css'>");
tags.push("body { background: "+getClassStyleProperty("column", "background-color")+" !important }"); // set background color
tags.push("a[data-full-url] { word-break: break-all !important }"); // break long urls
tags.push(".media-item, .media-preview { border-radius: 1px !important }"); // square-ify media
tags.push(".quoted-tweet { border-radius: 0 !important }"); // square-ify quoted tweets
tags.push(".activity-header { align-items: center !important; margin-bottom: 4px !important }"); // tweak alignment of avatar and text in notifications
tags.push(".activity-header .tweet-timestamp { line-height: unset !important }"); // fix timestamp position in notifications
if (fontSizeName === "smallest"){
tags.push(".badge-verified:before { width: 13px !important; height: 13px !important; background-position: -223px -98px !important }"); // fix cut off badge icon
}
tags.push("</style>");
doc.setAttribute("data-td-font", fontSizeName);
doc.setAttribute("data-td-theme", themeName);
$TD.loadNotificationLayout(fontSizeName, tags.join(""));
};
TD.settings.setFontSize = appendToFunction(TD.settings.setFontSize, function(name){
setTimeout(refreshSettings, 0);
});
tags.push("<style type='text/css'>");
tags.push("body { background: "+getClassStyleProperty("column", "background-color")+" }"); // set background color
tags.push("a[data-full-url] { word-break: break-all }"); // break long urls
tags.push(".txt-base-smallest .badge-verified:before { width: 13px !important; height: 13px !important; background-position: -223px -98px !important }"); // fix cut off badge icon
tags.push(".media-item, .media-preview { border-radius: 1px !important }"); // square-ify media
tags.push(".quoted-tweet { border-radius: 0 !important }"); // square-ify quoted tweets
tags.push(".activity-header { align-items: center !important; margin-bottom: 4px }"); // tweak alignment of avatar and text in notifications
tags.push(".activity-header .tweet-timestamp { line-height: unset }"); // fix timestamp position in notifications
tags.push("</style>");
TD.settings.setTheme = appendToFunction(TD.settings.setTheme, function(name){
setTimeout(refreshSettings, 0);
});
return tags.join("");
};
onAppReady.push(refreshSettings);
})();
//
// Block: Hook into settings object to detect when the settings change.
// Block: Fix OS name and add ID to the document for priority CSS selectors.
//
TD.settings.setFontSize = appendToFunction(TD.settings.setFontSize, function(name){
$TD.loadFontSizeClass(name);
});
if (ensurePropertyExists(TD, "util", "getOSName")){
TD.util.getOSName = function(){
return "windows";
};
TD.settings.setTheme = appendToFunction(TD.settings.setTheme, function(name){
document.documentElement.setAttribute("data-td-theme", name);
doc.classList.remove("os-");
doc.classList.add("os-windows");
}
setTimeout(function(){
$TD.loadNotificationHeadContents(getNotificationHeadContents());
}, 0);
});
onAppReady.push(function(){
document.documentElement.setAttribute("data-td-theme", TD.settings.getTheme());
$TD.loadFontSizeClass(TD.settings.getFontSize());
$TD.loadNotificationHeadContents(getNotificationHeadContents());
});
doc.id = "tduck";
//
// Block: Enable popup notifications.
@@ -357,7 +377,7 @@
mouseenter: function(){
let me = $(this);
let text = me.text();
return if text.charCodeAt(text.length-1) !== 8230; // horizontal ellipsis
return if text.charCodeAt(text.length-1) !== 8230 && text.charCodeAt(0) !== 8230; // horizontal ellipsis
if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){
@@ -409,7 +429,7 @@
});
if (ensurePropertyExists(TD, "services", "TwitterUser", "prototype", "fromJSONObject")){
let prevFunc = TD.services.TwitterUser.prototype.fromJSONObject;
const prevFunc = TD.services.TwitterUser.prototype.fromJSONObject;
TD.services.TwitterUser.prototype.fromJSONObject = function(){
let obj = prevFunc.apply(this, arguments);
@@ -424,7 +444,7 @@
}
if (ensurePropertyExists(TD, "services", "TwitterMedia", "prototype", "fromMediaEntity")){
let prevFunc = TD.services.TwitterMedia.prototype.fromMediaEntity;
const prevFunc = TD.services.TwitterMedia.prototype.fromMediaEntity;
TD.services.TwitterMedia.prototype.fromMediaEntity = function(){
let obj = prevFunc.apply(this, arguments);
@@ -479,13 +499,13 @@
(function(){
var lastTweet = "";
var updateHighlightedColumn = function(ele){
const updateHighlightedColumn = function(ele){
highlightedColumnEle = ele;
highlightedColumnObj = ele ? TD.controller.columnManager.get(ele.attr("data-column")) : null;
return !!highlightedColumnObj;
};
var updateHighlightedTweet = function(ele, obj, tweetUrl, quoteUrl, authors, imageList){
const updateHighlightedTweet = function(ele, obj, tweetUrl, quoteUrl, authors, imageList){
highlightedTweetEle = ele;
highlightedTweetObj = obj;
@@ -495,7 +515,7 @@
}
};
var processMedia = function(chirp){
const processMedia = function(chirp){
return chirp.getMedia().filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https+":small").join(";");
};
@@ -544,7 +564,7 @@
(function(){
var selectedTweet;
var setImportantProperty = function(obj, property, value){
const setImportantProperty = function(obj, property, value){
if (obj.length === 1){
obj[0].style.setProperty(property, value, "important");
}
@@ -580,7 +600,7 @@
setImportantProperty(selectedTweet.find(".js-media-preview-container"), "margin-bottom", "4px");
selectedTweet.find(".js-translate-call-to-action").first().remove();
selectedTweet.find(".js-tweet").first().nextAll().remove();
selectedTweet.find("footer").last().prevUntil(":not(.txt-mute.txt-small)").addBack().remove(); // footer, date, location
selectedTweet.find("footer").last().prevUntil(":not(.txt-mute)").addBack().remove(); // footer, date, location
}
else{
setImportantProperty(selectedTweet.find(".js-media-preview-container"), "margin-bottom", "10px");
@@ -613,7 +633,7 @@
// Block: Paste images when tweeting.
//
onAppReady.push(function(){
var uploader = $._data(document, "events")["uiComposeAddImageClick"][0].handler.context;
const uploader = $._data(document, "events")["uiComposeAddImageClick"][0].handler.context;
app.delegate(".js-compose-text,.js-reply-tweetbox,.td-detect-image-paste", "paste", function(e){
for(let item of e.originalEvent.clipboardData.items){
@@ -631,25 +651,41 @@
});
});
//
// Block: Setup a global function to reorder event priority.
//
window.TDGF_prioritizeNewestEvent = function(element, event){
let events = $._data(element, "events");
let handlers = events[event];
let newHandler = handlers[handlers.length-1];
for(let index = handlers.length-1; index > 0; index--){
handlers[index] = handlers[index-1];
}
handlers[0] = newHandler;
};
//
// Block: Support for extra mouse buttons.
//
(function(){
var tryClickSelector = function(selector, parent){
const tryClickSelector = function(selector, parent){
return $(selector, parent).click().length;
};
var tryCloseModal1 = function(){
const tryCloseModal1 = function(){
let modal = $("#open-modal");
return modal.is(":visible") && tryClickSelector("a.mdl-dismiss", modal);
};
var tryCloseModal2 = function(){
const tryCloseModal2 = function(){
let modal = $(".js-modals-container");
return modal.length && tryClickSelector("a.mdl-dismiss", modal);
};
var tryCloseHighlightedColumn = function(){
const tryCloseHighlightedColumn = function(){
if (highlightedColumnEle){
let column = highlightedColumnEle.closest(".js-column");
return (column.is(".is-shifted-2") && tryClickSelector(".js-tweet-social-proof-back", column)) || (column.is(".is-shifted-1") && tryClickSelector(".js-column-back", column));
@@ -684,7 +720,7 @@
let isDraggingValid = false;
window.TDGF_onGlobalDragStart = function(type, data){
isDraggingValid = type === "link" && tweetRegex.test(data);
isDraggingValid = (type === "link" || type === "text") && tweetRegex.test(data);
};
app.delegate("section.js-column", {
@@ -737,14 +773,14 @@
(function(){
var holdingShift = false;
var updateShiftState = (pressed) => {
const updateShiftState = (pressed) => {
if (pressed != holdingShift){
holdingShift = pressed;
$("button[data-action='clear']").children("span").text(holdingShift ? "Restore" : "Clear");
}
};
var resetActiveFocus = () => {
const resetActiveFocus = () => {
document.activeElement.blur();
};
@@ -775,7 +811,7 @@
// Block: Swap shift key functionality for selecting accounts, and refocus the textbox afterwards.
//
onAppReady.push(function(){
var onAccountClick = function(e){
const onAccountClick = function(e){
if ($TDX.switchAccountSelectors){
e.shiftKey = !e.shiftKey;
}
@@ -796,23 +832,63 @@
});
//
// Block: Make middle click on tweet reply icon open the compose drawer.
// Block: Make middle click on tweet reply icon open the compose drawer, retweet icon trigger a quote, and favorite icon open a 'Like from accounts...' modal.
//
app.delegate(".js-reply-action", "mousedown", function(e){
if (e.which === 2){
if ($("[data-drawer='compose']").hasClass("is-hidden")){
$(document).trigger("uiDrawerShowDrawer", {
drawer: "compose",
withAnimation: true
app.delegate(".tweet-action,.tweet-detail-action", "auxclick", function(e){
return if e.which !== 2;
let column = TD.controller.columnManager.get($(this).closest("section.js-column").attr("data-column"));
return if !column;
let ele = $(this).closest("article");
let tweet = column.findChirp(ele.attr("data-tweet-id")) || column.findChirp(ele.attr("data-key"));
return if !tweet;
switch($(this).attr("rel")){
case "reply":
let main = tweet.getMainTweet();
$(document).trigger("uiDockedComposeTweet", {
type: "reply",
from: [ tweet.account.getKey() ],
inReplyTo: {
id: tweet.id,
htmlText: main.htmlText,
user: {
screenName: main.user.screenName,
name: main.user.name,
profileImageURL: main.user.profileImageURL
}
},
mentions: tweet.getReplyUsers(),
element: ele
});
window.setTimeout(() => $(this).trigger("click"), 1);
}
break;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
case "favorite":
$(document).trigger("uiShowFavoriteFromOptions", { tweet });
break;
case "retweet":
TD.controller.stats.quoteTweet();
$(document).trigger("uiComposeTweet", {
type: "tweet",
from: [ tweet.account.getKey() ],
quotedTweet: tweet.getMainTweet(),
element: ele // triggers reply-account plugin
});
break;
default:
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
//
@@ -826,7 +902,7 @@
// Block: Inject custom CSS and layout into the page.
//
(function(){
var createStyle = function(id, styles){
const createStyle = function(id, styles){
let ele = document.createElement("style");
ele.id = id;
ele.innerText = styles;
@@ -870,26 +946,34 @@
// Block: Setup video player hooks.
//
(function(){
var playVideo = function(url){
window.TDGF_playVideo = function(url, username){
$('<div id="td-video-player-overlay" class="ovl" style="display:block"></div>').on("click contextmenu", function(){
$TD.playVideo(null);
$TD.playVideo(null, null);
}).appendTo(app);
$TD.playVideo(url);
$TD.playVideo(url, username || null);
};
var getVideoTweetLink = function(obj){
const getGifLink = function(ele){
return ele.attr("src") || ele.children("source[video-src]").first().attr("video-src");
};
const getVideoTweetLink = function(obj){
let parent = obj.closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
return link.attr("href");
}
const getUsername = function(tweet){
return tweet && (tweet.quotedTweet || tweet).getMainUser().screenName;
}
app.delegate(".js-gif-play", {
click: function(e){
let src = !e.ctrlKey && $(this).closest(".js-media-gif-container").find("video").attr("src");
let src = !e.ctrlKey && getGifLink($(this).closest(".js-media-gif-container").find("video"));
if (src){
playVideo(src);
window.TDGF_playVideo(src, getUsername(highlightedTweetObj));
}
else{
$TD.openBrowser(getVideoTweetLink($(this)));
@@ -925,7 +1009,7 @@
let media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service === "twitter"){
playVideo(media.chooseVideoVariant().url);
window.TDGF_playVideo(media.chooseVideoVariant().url, getUsername(this.chirp));
cancelModal = true;
}
});
@@ -1006,6 +1090,58 @@
}
});
//
// Block: Make submitting search queries while holding Ctrl or middle-clicking the search icon open the search externally.
//
onAppReady.push(function(){
const openSearchExternally = function(event, input){
$TD.openBrowser("https://twitter.com/search/?q="+encodeURIComponent(input.val() || ""));
event.preventDefault();
event.stopPropagation();
input.val("").blur();
app.click(); // unfocus everything
};
$(".js-app-search-input").keydown(function(e){
(e.ctrlKey && e.keyCode === 13) && openSearchExternally(e, $(this)); // enter
});
$(".js-perform-search").on("click auxclick", function(e){
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $(".js-app-search-input:visible"));
}).each(function(){
window.TDGF_prioritizeNewestEvent($(this)[0], "click");
});
$("[data-action='show-search']").on("click auxclick", function(e){
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $());
});
});
//
// Block: Setup global function to refresh all columns.
//
window.TDGF_reloadColumns = function(){
Object.values(TD.controller.columnManager.getAll()).forEach(column => column.reloadTweets());
};
//
// Block: Allow applying ROT13 to input selection.
//
window.TDGF_applyROT13 = function(){
let ele = document.activeElement;
return if !ele || !ele.value;
let selection = ele.value.substring(ele.selectionStart, ele.selectionEnd);
return if !selection;
document.execCommand("insertText", false, selection.replace(/[a-zA-Z]/g, function(chr){
let code = chr.charCodeAt(0);
let start = code <= 90 ? 65 : 97;
return String.fromCharCode(start+(code-start+13)%26);
}));
};
//
// Block: Fix DM reply input box not getting focused after opening a conversation.
//
@@ -1021,7 +1157,7 @@
// Block: Fix DM notifications not showing if the conversation is open.
//
if (ensurePropertyExists(TD, "services", "TwitterConversation", "prototype", "getUnreadChirps")){
var prevFunc = TD.services.TwitterConversation.prototype.getUnreadChirps;
const prevFunc = TD.services.TwitterConversation.prototype.getUnreadChirps;
TD.services.TwitterConversation.prototype.getUnreadChirps = function(e){
return (e && e.sortIndex && !e.id && !this.notificationsDisabled)
@@ -1163,15 +1299,24 @@
});
}
//
// Block: Disable default TweetDeck update notification, as the app usually reloads automatically every so often.
//
onAppReady.push(function(){
let events = $._data(document, "events");
delete events["uiSuggestRefreshToggle"];
});
//
// Block: Disable TweetDeck metrics.
//
if (ensurePropertyExists(TD, "metrics")){
TD.metrics.inflate = function(){};
TD.metrics.inflateMetricTriple = function(){};
TD.metrics.log = function(){};
TD.metrics.makeKey = function(){};
TD.metrics.send = function(){};
const noop = function(){};
TD.metrics.inflate = noop;
TD.metrics.inflateMetricTriple = noop;
TD.metrics.log = noop;
TD.metrics.makeKey = noop;
TD.metrics.send = noop;
}
onAppReady.push(function(){

View File

@@ -52,7 +52,7 @@
return if !url;
var text = me.textContent;
return if text.charCodeAt(text.length-1) !== 8230; // horizontal ellipsis
return if text.charCodeAt(text.length-1) !== 8230 && text.charCodeAt(0) !== 8230; // horizontal ellipsis
if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){

View File

@@ -3,7 +3,7 @@
<div class="js-tweet tweet">
<header class="tweet-header">
<time class="tweet-timestamp js-timestamp pull-right txt-mute">
<a target="_blank" rel="url" href="https://twitter.com/chylexmc" class="txt-small">0s</a>
<a target="_blank" rel="url" href="https://twitter.com/chylexmc" class="txt-size-variable--12">0s</a>
</time>
<a target="_blank" rel="user" href="https://twitter.com/chylexmc" class="account-link link-complex block">
<div class="obj-left item-img tweet-img">

View File

@@ -87,14 +87,14 @@
};
//
// Block: Setup global function to change plugin state.
// Block: Setup a function to change plugin state.
//
window.TDPF_setPluginState = function(identifier, enable){
window.TD_PLUGINS.setState(window.TD_PLUGINS.findObject(identifier), enable);
};
//
// Block: Setup global function to reload the page.
// Block: Setup a function to reload the page.
//
window.TDPF_requestReload = function(){
if (!isReloading){
@@ -102,4 +102,11 @@
isReloading = true;
}
};
//
// Block: Setup bridges to global functions.
//
window.TDPF_playVideo = window.TDGF_playVideo;
window.TDPF_reloadColumns = window.TDGF_reloadColumns;
window.TDPF_prioritizeNewestEvent = window.TDGF_prioritizeNewestEvent;
})();

View File

@@ -3,23 +3,23 @@
/***********************/
.scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb, .antiscroll-scrollbar {
border-radius: 0;
border-radius: 0 !important;
}
.antiscroll-scrollbar-vertical {
margin-top: 0;
margin-top: 0 !important;
}
.antiscroll-scrollbar-horizontal {
margin-left: 0;
margin-left: 0 !important;
}
.scroll-styled-v:not(.antiscroll-inner)::-webkit-scrollbar {
width: 8px;
width: 8px !important;
}
.scroll-styled-h:not(.antiscroll-inner)::-webkit-scrollbar {
height: 8px;
height: 8px !important;
}
.app-columns-container::-webkit-scrollbar {
@@ -27,7 +27,7 @@
}
.app-columns-container::-webkit-scrollbar-track {
border-left: 0;
border-left: 0 !important;
}
.app-columns-container {
@@ -38,11 +38,19 @@
/* Square-ify stuff */
/********************/
.btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .media-item, .media-preview, .tooltip-inner {
.btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .tooltip-inner {
border-radius: 1px !important;
}
.compose-text-container, .dropdown-menu, .list-item-last, .quoted-tweet, .input-group-button, input, textarea, select, .prf-header, .accs li, .accs img {
.media-item, .media-preview, .media-image, .js-media-added .br--4 {
border-radius: 1px !important;
}
.compose-text-container, .compose-reply-tweet, .compose-message-recipient-input-container, .compose-message-recipient, .compose-media-bar-holder, .media-grid-container, .js-quote-tweet-holder {
border-radius: 0 !important;
}
.dropdown-menu, .list-item-last, .quoted-tweet, .input-group-button, input, textarea, select, .prf-header, .accs li, .accs img {
border-radius: 0 !important;
}
@@ -79,7 +87,7 @@
}
.app-header:not(.is-condensed) .nav-user-info {
padding: 0 5px;
padding: 0 5px !important;
}
/****************************************/
@@ -88,11 +96,11 @@
.activity-header {
align-items: center !important;
margin-bottom: 4px;
margin-bottom: 4px !important;
}
.activity-header .tweet-timestamp {
line-height: unset;
line-height: unset !important;
}
.account-bio.padding-t--5 {
@@ -118,17 +126,12 @@ html[data-td-theme='dark'] .stream-item:not(:hover) .js-user-actions-menu {
a[data-full-url] {
/* break long urls */
word-break: break-all;
}
.character-count-compose {
/* fix strangely wide character count element */
width: 40px !important;
word-break: break-all !important;
}
.is-video a:not([href*='youtu']) .icon-bg-dot, .is-gif .icon-bg-dot {
/* change play icon on mp4s */
color: #9f51cf;
color: #9f51cf !important;
}
/***************************************/
@@ -144,7 +147,7 @@ a[data-full-url] {
}
.js-docked-compose footer {
display: none;
display: none !important;
}
.compose-content {
@@ -157,17 +160,17 @@ a[data-full-url] {
.account-inline .username {
/* move usernames a bit higher */
vertical-align: 10%;
vertical-align: 10% !important;
}
.txt-base-smallest .sprite-verified-mini {
html[data-td-font='smallest'] .sprite-verified-mini {
/* fix cut off badge when zoomed in */
width: 13px !important;
height: 13px !important;
background-position: -223px -99px !important;
}
.txt-base-smallest .badge-verified:before {
html[data-td-font='smallest'] .badge-verified:before {
/* fix cut off badge in notifications */
width: 13px !important;
height: 13px !important;
@@ -181,7 +184,7 @@ a[data-full-url] {
.keyboard-shortcut-list {
/* fix keyboard navigation alignment */
vertical-align: top;
vertical-align: top !important;
}
.tweet-detail-wrapper .js-media-gif-container {
@@ -189,20 +192,36 @@ a[data-full-url] {
cursor: pointer;
}
/********************************************************************/
/* Fix glaring visual issues because twitter is fucking incompetent */
/********************************************************************/
.is-inverted-dark .inline-reply .btn:hover {
/* Reply buttons in modals are bork */
background-color: transparent !important;
}
/***************************************************************/
/* Fix glaring visual issues that twitter hasn't fixed yet smh */
/***************************************************************/
.column-nav-link .attribution {
/* fix cut off account names */
position: absolute;
position: absolute !important;
}
.js-docked-compose .js-drawer-close {
#tduck .js-docked-compose .js-drawer-close {
/* fix close drawer button position */
margin: 20px 0 0 !important;
}
.compose-reply-tweet-remove {
/* fix close reply button position; this would make sense if the reply account name became full size, but it still cuts off where the button used to be */
top: 3px !important;
right: -2px !important;
}
#account-safeguard-checkbox, #inline-account-safeguard-checkbox {
/* fix "Ready to Tweet/send" alignment with the checkbox */
vertical-align: -15% !important;
}
/************************************************/
/* Fix tooltips in navigation and column header */
/************************************************/
@@ -212,7 +231,7 @@ a[data-full-url] {
}
.js-column-header .column-header-link {
padding: 0;
padding: 0 !important;
}
.js-column-header .column-header-link .icon {
@@ -231,9 +250,9 @@ a[data-full-url] {
}
.column:not(.is-options-open) .column-header {
border-bottom: none;
border-bottom: none !important;
}
.is-options-open .column-type-icon {
bottom: 27px;
bottom: 27px !important;
}

View File

@@ -1,23 +1,23 @@
/* General */
body:before {
content: none;
content: none !important;
}
body {
overflow-y: auto;
overflow-y: auto !important;
}
.scroll-styled-v::-webkit-scrollbar {
width: 7px;
width: 7px !important;
}
.scroll-styled-v::-webkit-scrollbar-thumb {
border-radius: 0;
border-radius: 0 !important;
}
.scroll-styled-v::-webkit-scrollbar-track {
border-left: 0;
border-left: 0 !important;
}
/* Media */

View File

@@ -46,25 +46,25 @@
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
.tdu-title {
#tweetduck-update .tdu-title {
font-size: 15px;
font-weight: bold;
margin: 8px 0 2px;
cursor: default;
}
.tdu-info {
#tweetduck-update .tdu-info {
display: inline-block;
font-size: 14px;
margin: 3px 0;
}
.tdu-showlog {
#tweetduck-update .tdu-showlog {
text-decoration: underline;
cursor: pointer;
}
.tdu-buttons button {
#tweetduck-update .tdu-buttons button {
display: block;
margin: 7px auto 0;
padding: 4px 10px;
@@ -81,12 +81,12 @@
transition: box-shadow 0.2s ease;
}
.tdu-buttons button:hover {
#tweetduck-update .tdu-buttons button:hover {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.75);
box-shadow: 1px 1px 1px rgba(17, 17, 17, 0.75), 0 -2px 0 rgba(17, 17, 17, 0.33) inset !important;
}
.tdu-buttons button.tdu-btn-ignore, .tdu-buttons button.tdu-btn-later {
#tweetduck-update .tdu-buttons button.tdu-btn-ignore, .tdu-buttons button.tdu-btn-later {
background-color: #607a8e;
color: #dfdfdf;
}

View File

@@ -105,6 +105,9 @@
<Compile Include="Core\Handling\RequestHandlerBrowser.cs" />
<Compile Include="Core\Handling\General\RequestHandlerBase.cs" />
<Compile Include="Core\Handling\ResourceHandlerNotification.cs" />
<Compile Include="Core\Notification\Example\FormNotificationExample.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Core\Notification\FormNotificationMain.cs">
<SubType>Form</SubType>
</Compile>
@@ -128,6 +131,10 @@
</Compile>
<Compile Include="Core\Notification\SoundNotification.cs" />
<Compile Include="Core\Notification\TweetNotification.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsFile.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsManager.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsReport.cs" />
<Compile Include="Core\Other\Analytics\AnalyticsReportGenerator.cs" />
<Compile Include="Core\Other\FormAbout.cs">
<SubType>Form</SubType>
</Compile>
@@ -153,6 +160,12 @@
<DependentUpon>FormPlugins.cs</DependentUpon>
</Compile>
<Compile Include="Core\Other\Management\VideoPlayer.cs" />
<Compile Include="Core\Other\Settings\Dialogs\DialogSettingsAnalytics.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Core\Other\Settings\Dialogs\DialogSettingsAnalytics.Designer.cs">
<DependentUpon>DialogSettingsAnalytics.cs</DependentUpon>
</Compile>
<Compile Include="Core\Other\Settings\Dialogs\DialogSettingsCSS.cs">
<SubType>Form</SubType>
</Compile>
@@ -190,6 +203,7 @@
<Compile Include="Core\Other\Settings\TabSettingsTray.Designer.cs">
<DependentUpon>TabSettingsTray.cs</DependentUpon>
</Compile>
<Compile Include="Core\TweetDeckBrowser.cs" />
<Compile Include="Core\Utils\StringUtils.cs" />
<Compile Include="Core\Utils\TwitterUtils.cs" />
<Compile Include="Data\CombinedFileStream.cs" />
@@ -269,10 +283,10 @@
<Compile Include="Updates\FormUpdateDownload.Designer.cs">
<DependentUpon>FormUpdateDownload.cs</DependentUpon>
</Compile>
<Compile Include="Core\TrayIcon.cs">
<Compile Include="Core\Other\TrayIcon.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Core\TrayIcon.Designer.cs">
<Compile Include="Core\Other\TrayIcon.Designer.cs">
<DependentUpon>TrayIcon.cs</DependentUpon>
</Compile>
<Compile Include="Core\Utils\BrowserCache.cs" />

View File

@@ -31,7 +31,7 @@ namespace TweetDuck.Updates{
timerDownloadCheck.Stop();
if (FormMessage.Error("Update Has Failed", "Could not download the update: "+(updateInfo.DownloadError?.Message ?? "unknown error")+"\n\nDo you want to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowserUnsafe(Program.Website);
BrowserUtils.OpenExternalBrowser(Program.Website);
DialogResult = DialogResult.OK;
}

View File

@@ -43,9 +43,9 @@ namespace TweetLib.Audio.Impl{
}
private void OnNotificationSoundError(string message){
if (!ignorePlaybackError && PlaybackError != null){
if (!ignorePlaybackError){
PlaybackErrorEventArgs args = new PlaybackErrorEventArgs(message);
PlaybackError(this, args);
PlaybackError?.Invoke(this, args);
ignorePlaybackError = args.Ignore;
}
}

View File

@@ -102,9 +102,9 @@ namespace TweetLib.Audio.Impl{
if (wasTryingToPlay){
wasTryingToPlay = false;
if (!ignorePlaybackError && PlaybackError != null){
if (!ignorePlaybackError){
PlaybackErrorEventArgs args = new PlaybackErrorEventArgs(message);
PlaybackError(this, args);
PlaybackError?.Invoke(this, args);
ignorePlaybackError = args.Ignore;
}
}

View File

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

View File

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

View File

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

View File

@@ -36,9 +36,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="DuplexPipe.cs" />
<Compile Include="Utils\NativeMethods.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Comms.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -1,14 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace TweetLib.Communication.Utils{
static class NativeMethods{
public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
[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);
}
}

View File

@@ -1,12 +1,12 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using CefSharp;
using CefSharp.BrowserSubprocess;
using TweetLib.Communication;
namespace TweetDuck.Browser{
static class Program{
internal const string Version = "1.2.0.0";
internal const string Version = "1.2.1.0";
private static int Main(string[] args){
SubProcess.EnableHighDPISupport();
@@ -30,9 +30,17 @@ namespace TweetDuck.Browser{
base.OnBrowserCreated(wrapper);
using(Process me = Process.GetCurrentProcess()){
Comms.BroadcastMessage(Comms.RegisterMessage("TweetDuckSubProcess"), (uint)me.Id, wrapper.BrowserId);
PostMessage(HWND_BROADCAST, RegisterWindowMessage("TweetDuckSubProcess"), new UIntPtr((uint)me.Id), new IntPtr(wrapper.BrowserId));
}
}
}
private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern uint RegisterWindowMessage(string messageName);
}
}

View File

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

View File

@@ -3,7 +3,7 @@ using System.Diagnostics.Contracts;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Configuration;
using TweetDuck.Core;
using TweetDuck.Core.Other;
namespace UnitTests.Configuration{
[TestClass]
@@ -76,7 +76,7 @@ namespace UnitTests.Configuration{
cfg.Save();
cfg.ZoomLevel = 200;
Assert.IsTrue(cfg.Reload());
cfg.Reload();
Assert.AreEqual(123, cfg.ZoomLevel);
}
@@ -87,7 +87,7 @@ namespace UnitTests.Configuration{
cfg.Save();
File.Delete("reset");
Assert.IsTrue(cfg.Reload());
cfg.Reload();
Assert.AreEqual(100, cfg.ZoomLevel);
Assert.IsTrue(File.Exists("reset"));
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Drawing;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using TweetDuck.Video.Controls;
@@ -107,6 +108,7 @@ namespace TweetDuck.Video{
Environment.Exit(Program.CODE_MEDIA_ERROR);
}
[HandleProcessCorruptedStateExceptions]
private void timerSync_Tick(object sender, EventArgs e){
if (NativeMethods.GetWindowRect(ownerHandle, out NativeMethods.RECT rect)){
int width = rect.Right-rect.Left+1;
@@ -139,9 +141,14 @@ namespace TweetDuck.Video{
}
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();
try{
controls.stop();
controls.currentPosition = 0;
controls.play();
}catch(AccessViolationException){
// something is super retarded here because shit gets disposed between the start of this method and
// the controls.play() call even though it runs on the UI thread
}
}
if (isCursorInside && !wasCursorInside){
@@ -255,6 +262,7 @@ namespace TweetDuck.Video{
Visible = false;
pipe.Write("rip");
Player.close();
Close();
}