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

Compare commits

..

1 Commits

Author SHA1 Message Date
9238410756 WIP? 2019-05-09 11:54:38 +02:00
187 changed files with 2481 additions and 2897 deletions

4
.github/FUNDING.yml vendored
View File

@@ -1,4 +0,0 @@
# These are supported funding model platforms
patreon: chylex
ko_fi: chylex

View File

@@ -1,5 +1,5 @@
using System;
using TweetLib.Core.Collections;
using TweetDuck.Data;
namespace TweetDuck.Configuration{
static class Arguments{
@@ -22,8 +22,8 @@ namespace TweetDuck.Configuration{
return Current.HasFlag(flag);
}
public static string GetValue(string key){
return Current.GetValue(key);
public static string GetValue(string key, string defaultValue){
return Current.GetValue(key, defaultValue);
}
public static CommandLineArgs GetCurrentClean(){

View File

@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using TweetDuck.Configuration.Instance;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core.Features.Configuration;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration{
sealed class ConfigManager : IConfigManager{
sealed class ConfigManager{
public UserConfig User { get; }
public SystemConfig System { get; }
public PluginConfig Plugins { get; }
@@ -16,7 +16,7 @@ namespace TweetDuck.Configuration{
private readonly FileConfigInstance<UserConfig> infoUser;
private readonly FileConfigInstance<SystemConfig> infoSystem;
private readonly PluginConfigInstance<PluginConfig> infoPlugins;
private readonly PluginConfigInstance infoPlugins;
private readonly IConfigInstance<BaseConfig>[] infoList;
@@ -28,7 +28,7 @@ namespace TweetDuck.Configuration{
infoList = new IConfigInstance<BaseConfig>[]{
infoUser = new FileConfigInstance<UserConfig>(Program.UserConfigFilePath, User, "program options"),
infoSystem = new FileConfigInstance<SystemConfig>(Program.SystemConfigFilePath, System, "system options"),
infoPlugins = new PluginConfigInstance<PluginConfig>(Program.PluginConfigFilePath, Plugins)
infoPlugins = new PluginConfigInstance(Program.PluginConfigFilePath, Plugins)
};
// TODO refactor further
@@ -70,13 +70,59 @@ namespace TweetDuck.Configuration{
infoPlugins.Reload();
}
void IConfigManager.TriggerProgramRestartRequested(){
private void TriggerProgramRestartRequested(){
ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
}
IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance){
private IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance){
Type instanceType = instance.GetType();
return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null
}
public abstract class BaseConfig{
private readonly ConfigManager configManager;
protected BaseConfig(ConfigManager configManager){
this.configManager = configManager;
}
// Management
public void Save(){
configManager.GetInstanceInfo(this).Save();
}
public void Reload(){
configManager.GetInstanceInfo(this).Reload();
}
public void Reset(){
configManager.GetInstanceInfo(this).Reset();
}
// Construction methods
public T ConstructWithDefaults<T>() where T : BaseConfig{
return ConstructWithDefaults(configManager) as T;
}
protected abstract BaseConfig ConstructWithDefaults(ConfigManager configManager);
// Utility methods
protected void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler eventHandler){
if (!EqualityComparer<T>.Default.Equals(field, value)){
field = value;
eventHandler?.Invoke(this, EventArgs.Empty);
}
}
protected void UpdatePropertyWithRestartRequest<T>(ref T field, T value){
if (!EqualityComparer<T>.Default.Equals(field, value)){
field = value;
configManager.TriggerProgramRestartRequested();
}
}
}
}
}

View File

@@ -1,20 +1,22 @@
using System;
using System.IO;
using TweetLib.Core.Serialization;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration.Instance{
sealed class FileConfigInstance<T> : IConfigInstance<T> where T : ConfigManager.BaseConfig{
private const string ErrorTitle = "Configuration Error";
namespace TweetLib.Core.Features.Configuration{
public sealed class FileConfigInstance<T> : IConfigInstance<T> where T : BaseConfig{
public T Instance { get; }
public FileSerializer<T> Serializer { get; }
private readonly string filenameMain;
private readonly string filenameBackup;
private readonly string identifier;
private readonly string errorIdentifier;
public FileConfigInstance(string filename, T instance, string identifier){
public FileConfigInstance(string filename, T instance, string errorIdentifier){
this.filenameMain = filename;
this.filenameBackup = filename+".bak";
this.identifier = identifier;
this.errorIdentifier = errorIdentifier;
this.Instance = instance;
this.Serializer = new FileSerializer<T>();
@@ -25,14 +27,14 @@ namespace TweetLib.Core.Features.Configuration{
}
public void Load(){
Exception? firstException = null;
Exception firstException = null;
for(int attempt = 0; attempt < 2; attempt++){
try{
LoadInternal(attempt > 0);
if (firstException != null){ // silently log exception that caused a backup restore
App.ErrorHandler.Log(firstException.ToString());
Program.Reporter.LogImportant(firstException.ToString());
}
return;
@@ -47,13 +49,13 @@ namespace TweetLib.Core.Features.Configuration{
}
if (firstException is FormatException){
OnException($"The configuration file for {identifier} is outdated or corrupted. If you continue, your {identifier} will be reset.", firstException);
Program.Reporter.HandleException(ErrorTitle, "The configuration file for "+errorIdentifier+" is outdated or corrupted. If you continue, your "+errorIdentifier+" will be reset.", true, firstException);
}
else if (firstException is SerializationSoftException sse){
OnException($"{sse.Errors.Count} error{(sse.Errors.Count == 1 ? " was" : "s were")} encountered while loading the configuration file for {identifier}. If you continue, some of your {identifier} will be reset.", firstException);
Program.Reporter.HandleException(ErrorTitle, $"{sse.Errors.Count} error{(sse.Errors.Count == 1 ? " was" : "s were")} encountered while loading the configuration file for "+errorIdentifier+". If you continue, some of your "+errorIdentifier+" will be reset.", true, firstException);
}
else if (firstException != null){
OnException($"Could not open the configuration file for {identifier}. If you continue, your {identifier} will be reset.", firstException);
Program.Reporter.HandleException(ErrorTitle, "Could not open the configuration file for "+errorIdentifier+". If you continue, your "+errorIdentifier+" will be reset.", true, firstException);
}
}
@@ -66,9 +68,9 @@ namespace TweetLib.Core.Features.Configuration{
Serializer.Write(filenameMain, Instance);
}catch(SerializationSoftException e){
OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e);
Program.Reporter.HandleException(ErrorTitle, $"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for "+errorIdentifier+".", true, e);
}catch(Exception e){
OnException($"Could not save the configuration file for {identifier}.", e);
Program.Reporter.HandleException(ErrorTitle, "Could not save the configuration file for "+errorIdentifier+".", true, e);
}
}
@@ -80,10 +82,10 @@ namespace TweetLib.Core.Features.Configuration{
Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>());
LoadInternal(false);
}catch(Exception e){
OnException($"Could not regenerate the configuration file for {identifier}.", e);
Program.Reporter.HandleException(ErrorTitle, "Could not regenerate the configuration file for "+errorIdentifier+".", true, e);
}
}catch(Exception e){
OnException($"Could not reload the configuration file for {identifier}.", e);
Program.Reporter.HandleException(ErrorTitle, "Could not reload the configuration file for "+errorIdentifier+".", true, e);
}
}
@@ -92,15 +94,11 @@ namespace TweetLib.Core.Features.Configuration{
File.Delete(filenameMain);
File.Delete(filenameBackup);
}catch(Exception e){
OnException($"Could not delete configuration files to reset {identifier}.", e);
Program.Reporter.HandleException(ErrorTitle, "Could not delete configuration files to reset "+errorIdentifier+".", true, e);
return;
}
Reload();
}
private static void OnException(string message, Exception e){
App.ErrorHandler.HandleException("Configuration Error", message, true, e);
}
}
}

View File

@@ -1,5 +1,5 @@
namespace TweetLib.Core.Features.Configuration{
public interface IConfigInstance<out T>{
namespace TweetDuck.Configuration.Instance{
interface IConfigInstance<out T>{
T Instance { get; }
void Save();

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace TweetDuck.Configuration.Instance{
class PluginConfigInstance : IConfigInstance<PluginConfig>{
public PluginConfig Instance { get; }
private readonly string filename;
public PluginConfigInstance(string filename, PluginConfig instance){
this.filename = filename;
this.Instance = instance;
}
public void Load(){
try{
using(StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8)){
string line = reader.ReadLine();
if (line == "#Disabled"){
HashSet<string> newDisabled = new HashSet<string>();
while((line = reader.ReadLine()) != null){
newDisabled.Add(line);
}
Instance.ReloadSilently(newDisabled);
}
}
}catch(FileNotFoundException){
}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(){
try{
using(StreamWriter writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8)){
writer.WriteLine("#Disabled");
foreach(string identifier in Instance.DisabledPlugins){
writer.WriteLine(identifier);
}
}
}catch(Exception e){
Program.Reporter.HandleException("Plugin Configuration Error", "Could not save the plugin configuration file.", true, e);
}
}
public void Reload(){
Load();
}
public void Reset(){
try{
File.Delete(filename);
Instance.ReloadSilently(Instance.ConstructWithDefaults<PluginConfig>().DisabledPlugins);
}catch(Exception e){
Program.Reporter.HandleException("Plugin Configuration Error", "Could not delete the plugin configuration file.", true, e);
return;
}
Reload();
}
}
}

View File

@@ -1,11 +1,12 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using TweetDuck.Core.Utils;
namespace TweetLib.Core.Application.Helpers{
public sealed class LockManager{
namespace TweetDuck.Configuration{
sealed class LockManager{
private const int RetryDelay = 250;
public enum Result{
@@ -13,8 +14,8 @@ namespace TweetLib.Core.Application.Helpers{
}
private readonly string file;
private FileStream? lockStream;
private Process? lockingProcess;
private FileStream lockStream;
private Process lockingProcess;
public LockManager(string file){
this.file = file;
@@ -36,7 +37,7 @@ namespace TweetLib.Core.Application.Helpers{
private Result TryCreateLockFile(){
void CreateLockFileStream(){
lockStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read);
lockStream.Write(BitConverter.GetBytes(CurrentProcessID), 0, sizeof(int));
lockStream.Write(BitConverter.GetBytes(WindowsUtils.CurrentProcessID), 0, sizeof(int));
lockStream.Flush(true);
}
@@ -81,12 +82,14 @@ namespace TweetLib.Core.Application.Helpers{
try{
Process foundProcess = Process.GetProcessById(pid);
if (MatchesCurrentProcess(foundProcess)){
using(Process currentProcess = Process.GetCurrentProcess()){
if (foundProcess.MainModule.FileVersionInfo.InternalName == currentProcess.MainModule.FileVersionInfo.InternalName){
lockingProcess = foundProcess;
}
else{
foundProcess.Close();
}
}
}catch{
// GetProcessById throws ArgumentException if the process is missing
// Process.MainModule can throw exceptions in some cases
@@ -121,7 +124,7 @@ namespace TweetLib.Core.Application.Helpers{
try{
File.Delete(file);
}catch(Exception e){
App.ErrorHandler.Log(e.ToString());
Program.Reporter.LogImportant(e.ToString());
return false;
}
}
@@ -131,32 +134,50 @@ namespace TweetLib.Core.Application.Helpers{
// Locking process
public bool RestoreLockingProcess(){
return lockingProcess != null && App.LockHandler.RestoreProcess(lockingProcess);
}
public bool RestoreLockingProcess(int failTimeout){
if (lockingProcess != null && lockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)lockingProcess.Id, 0);
public bool CloseLockingProcess(){
if (lockingProcess != null && App.LockHandler.CloseProcess(lockingProcess)){
lockingProcess = null;
if (WindowsUtils.TrySleepUntil(() => CheckLockingProcessExited() || (lockingProcess.MainWindowHandle != IntPtr.Zero && lockingProcess.Responding), failTimeout, RetryDelay)){
return true;
}
}
return false;
}
// Utilities
public bool CloseLockingProcess(int closeTimeout, int killTimeout){
if (lockingProcess != null){
try{
if (lockingProcess.CloseMainWindow()){
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, closeTimeout, RetryDelay);
}
private static int CurrentProcessID{
get{
using Process me = Process.GetCurrentProcess();
return me.Id;
if (!lockingProcess.HasExited){
lockingProcess.Kill();
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, killTimeout, RetryDelay);
}
if (lockingProcess.HasExited){
lockingProcess.Dispose();
lockingProcess = null;
return true;
}
}catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){
if (lockingProcess != null){
bool hasExited = CheckLockingProcessExited();
lockingProcess.Dispose();
return hasExited;
}
}
}
[SuppressMessage("ReSharper", "PossibleNullReferenceException")]
private static bool MatchesCurrentProcess(Process process){
using Process current = Process.GetCurrentProcess();
return current.MainModule.FileVersionInfo.InternalName == process.MainModule.FileVersionInfo.InternalName;
return false;
}
private bool CheckLockingProcessExited(){
lockingProcess.Refresh();
return lockingProcess.HasExited;
}
}
}

View File

@@ -1,42 +1,21 @@
using System;
using System.Collections.Generic;
using TweetLib.Core.Features.Configuration;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Events;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Events;
namespace TweetDuck.Configuration{
sealed class PluginConfig : BaseConfig, IPluginConfig{
sealed class PluginConfig : ConfigManager.BaseConfig, IPluginConfig{
private static readonly string[] DefaultDisabled = {
"official/clear-columns",
"official/reply-account"
};
// CONFIGURATION DATA
// CONFIGURATION
private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
// EVENTS
public IEnumerable<string> DisabledPlugins => disabled;
public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
// END OF CONFIG
public PluginConfig(IConfigManager configManager) : base(configManager){}
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
return new PluginConfig(configManager);
}
// INTERFACE IMPLEMENTATION
IEnumerable<string> IPluginConfig.DisabledPlugins => disabled;
void IPluginConfig.Reset(IEnumerable<string> newDisabledPlugins){
disabled.Clear();
disabled.UnionWith(newDisabledPlugins);
}
public void SetEnabled(Plugin plugin, bool enabled){
if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))){
PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
@@ -47,5 +26,20 @@ namespace TweetDuck.Configuration{
public bool IsEnabled(Plugin plugin){
return !disabled.Contains(plugin.Identifier);
}
public void ReloadSilently(IEnumerable<string> newDisabled){
disabled.Clear();
disabled.UnionWith(newDisabled);
}
private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
// END OF CONFIG
public PluginConfig(ConfigManager configManager) : base(configManager){}
protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){
return new PluginConfig(configManager);
}
}
}

View File

@@ -1,7 +1,5 @@
using TweetLib.Core.Features.Configuration;
namespace TweetDuck.Configuration{
sealed class SystemConfig : BaseConfig{
namespace TweetDuck.Configuration{
sealed class SystemConfig : ConfigManager.BaseConfig{
// CONFIGURATION DATA
@@ -19,9 +17,9 @@ namespace TweetDuck.Configuration{
// END OF CONFIG
public SystemConfig(IConfigManager configManager) : base(configManager){}
public SystemConfig(ConfigManager configManager) : base(configManager){}
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){
return new SystemConfig(configManager);
}
}

View File

@@ -1,14 +1,13 @@
using System;
using System.Drawing;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core.Features.Configuration;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Configuration{
sealed class UserConfig : BaseConfig{
sealed class UserConfig : ConfigManager.BaseConfig{
// CONFIGURATION DATA
@@ -57,12 +56,12 @@ namespace TweetDuck.Configuration{
public bool NotificationTimerCountDown { get; set; } = false;
public int NotificationDurationValue { get; set; } = 25;
public DesktopNotification.Position NotificationPosition { get; set; } = DesktopNotification.Position.TopRight;
public TweetNotification.Position NotificationPosition { get; set; } = TweetNotification.Position.TopRight;
public Point CustomNotificationPosition { get; set; } = ControlExtensions.InvisibleLocation;
public int NotificationDisplay { get; set; } = 0;
public int NotificationEdgeDistance { get; set; } = 8;
public DesktopNotification.Size NotificationSize { get; set; } = DesktopNotification.Size.Auto;
public TweetNotification.Size NotificationSize { get; set; } = TweetNotification.Size.Auto;
public Size CustomNotificationSize { get; set; } = Size.Empty;
public int NotificationScrollSpeed { get; set; } = 100;
@@ -80,7 +79,7 @@ namespace TweetDuck.Configuration{
public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty;
public ImageQuality TwitterImageQuality => BestImageQuality ? ImageQuality.Best : ImageQuality.Default;
public TwitterUtils.ImageQuality TwitterImageQuality => BestImageQuality ? TwitterUtils.ImageQuality.Orig : TwitterUtils.ImageQuality.Default;
public string NotificationSoundPath{
get => _notificationSoundPath ?? string.Empty;
@@ -136,9 +135,9 @@ namespace TweetDuck.Configuration{
// END OF CONFIG
public UserConfig(IConfigManager configManager) : base(configManager){}
public UserConfig(ConfigManager configManager) : base(configManager){}
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){
return new UserConfig(configManager);
}
}

View File

@@ -1,41 +0,0 @@
using System.IO;
using CefSharp;
using TweetLib.Core.Browser;
namespace TweetDuck.Core.Adapters{
sealed class CefScriptExecutor : IScriptExecutor{
private readonly IWebBrowser browser;
public CefScriptExecutor(IWebBrowser browser){
this.browser = browser;
}
public void RunFunction(string name, params object[] args){
browser.ExecuteScriptAsync(name, args);
}
public void RunScript(string identifier, string script){
using IFrame frame = browser.GetMainFrame();
RunScript(frame, script, identifier);
}
public bool RunFile(string file){
using IFrame frame = browser.GetMainFrame();
return RunFile(frame, file);
}
// Helpers
public static void RunScript(IFrame frame, string script, string identifier){
if (script != null){
frame.ExecuteJavaScriptAsync(script, identifier, 1);
}
}
public static bool RunFile(IFrame frame, string file){
string script = Program.Resources.Load(file);
RunScript(frame, script, "root:" + Path.GetFileNameWithoutExtension(file));
return script != null;
}
}
}

View File

@@ -8,8 +8,8 @@ namespace TweetDuck.Core.Bridge{
}
public static string GenerateScript(Environment environment){
static string Bool(bool value) => value ? "true;" : "false;";
static string Str(string value) => $"\"{value}\";";
string Bool(bool value) => value ? "true;" : "false;";
string Str(string value) => '"'+value+"\";";
UserConfig config = Program.Config.User;
StringBuilder build = new StringBuilder(128).Append("(function(x){");

View File

@@ -1,18 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Management;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Notifications;
namespace TweetDuck.Core.Bridge{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class TweetDeckBridge{
public static string FontSize { get; private set; }
public static string NotificationHeadLayout { get; private set; }
public static readonly ContextInfo ContextInfo = new ContextInfo();
public static void ResetStaticProperties(){
FormNotificationBase.FontSize = null;
FormNotificationBase.HeadLayout = null;
FontSize = NotificationHeadLayout = null;
}
private readonly FormBrowser form;
@@ -44,17 +44,17 @@ namespace TweetDuck.Core.Bridge{
public void LoadNotificationLayout(string fontSize, string headLayout){
form.InvokeAsyncSafe(() => {
FormNotificationBase.FontSize = fontSize;
FormNotificationBase.HeadLayout = headLayout;
FontSize = fontSize;
NotificationHeadLayout = headLayout;
});
}
public void SetRightClickedLink(string type, string url){
ContextMenuBase.CurrentInfo.SetLink(type, url);
ContextInfo.SetLink(type, url);
}
public void SetRightClickedChirp(string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages){
ContextMenuBase.CurrentInfo.SetChirp(tweetUrl, quoteUrl, chirpAuthors, chirpImages);
ContextInfo.SetChirp(tweetUrl, quoteUrl, chirpAuthors, chirpImages);
}
public void DisplayTooltip(string text){
@@ -85,7 +85,7 @@ namespace TweetDuck.Core.Bridge{
public void OnTweetPopup(string columnId, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl){
notification.InvokeAsyncSafe(() => {
form.OnTweetNotification();
notification.ShowNotification(new DesktopNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
notification.ShowNotification(new TweetNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
});
}
@@ -117,12 +117,14 @@ namespace TweetDuck.Core.Bridge{
}
public void Alert(string type, string contents){
MessageBoxIcon icon = type switch{
"error" => MessageBoxIcon.Error,
"warning" => MessageBoxIcon.Warning,
"info" => MessageBoxIcon.Information,
_ => MessageBoxIcon.None
};
MessageBoxIcon icon;
switch(type){
case "error": icon = MessageBoxIcon.Error; break;
case "warning": icon = MessageBoxIcon.Warning; break;
case "info": icon = MessageBoxIcon.Information; break;
default: icon = MessageBoxIcon.None; break;
}
FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
}

View File

@@ -1,11 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetLib.Core.Features.Updates;
using TweetDuck.Updates;
namespace TweetDuck.Core.Bridge{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class UpdateBridge{
private readonly UpdateHandler updates;
private readonly Control sync;
@@ -13,6 +11,7 @@ namespace TweetDuck.Core.Bridge{
private UpdateInfo nextUpdate = null;
public event EventHandler<UpdateInfo> UpdateAccepted;
public event EventHandler<UpdateInfo> UpdateDelayed;
public event EventHandler<UpdateInfo> UpdateDismissed;
public UpdateBridge(UpdateHandler updates, Control sync){
@@ -55,6 +54,10 @@ namespace TweetDuck.Core.Bridge{
HandleInteractionEvent(UpdateAccepted);
}
public void OnUpdateDelayed(){
HandleInteractionEvent(UpdateDelayed);
}
public void OnUpdateDismissed(){
HandleInteractionEvent(UpdateDismissed);

View File

@@ -21,16 +21,17 @@ namespace TweetDuck.Core.Controls{
}
public static float GetDPIScale(this Control control){
using Graphics graphics = control.CreateGraphics();
using(Graphics graphics = control.CreateGraphics()){
return graphics.DpiY/96F;
}
}
public static bool IsFullyOutsideView(this Form form){
return !Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(form.Bounds));
}
public static void MoveToCenter(this Form targetForm, Form parentForm){
targetForm.Location = new Point(parentForm.Location.X + (parentForm.Width / 2) - (targetForm.Width / 2), parentForm.Location.Y + (parentForm.Height / 2) - (targetForm.Height / 2));
targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2);
}
public static void SetValueInstant(this ProgressBar bar, int value){
@@ -62,8 +63,7 @@ namespace TweetDuck.Core.Controls{
trackBar.Value = trackBar.SmallChange*(int)Math.Floor(((double)trackBar.Value/trackBar.SmallChange)+0.5);
return false;
}
return true;
else return true;
}
public static void EnableMultilineShortcuts(this TextBox textBox){

View File

@@ -8,8 +8,8 @@ namespace TweetDuck.Core.Controls{
protected override void OnPaint(PaintEventArgs e){
int y = (int)Math.Floor((ClientRectangle.Height-Text.Length*LineHeight)/2F)-1;
using Brush brush = new SolidBrush(ForeColor);
using(Brush brush = new SolidBrush(ForeColor)){
foreach(char chr in Text){
string str = chr.ToString();
float x = (ClientRectangle.Width-e.Graphics.MeasureString(str, Font).Width)/2F;
@@ -20,3 +20,4 @@ namespace TweetDuck.Core.Controls{
}
}
}
}

View File

@@ -1,8 +1,7 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.WindowsAPICodePack.Taskbar;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
@@ -15,10 +14,10 @@ using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
using TweetDuck.Updates;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Updates;
namespace TweetDuck.Core{
sealed partial class FormBrowser : Form, AnalyticsFile.IProvider{
@@ -52,6 +51,7 @@ namespace TweetDuck.Core{
private readonly FormNotificationTweet notification;
private readonly ContextMenu contextMenu;
private readonly UpdateBridge updateBridge;
private readonly TaskbarIcon taskbarIcon;
private bool isLoaded;
private FormWindowState prevState;
@@ -65,7 +65,7 @@ namespace TweetDuck.Core{
Text = Program.BrandName;
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath);
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
this.plugins.Reload();
@@ -73,11 +73,12 @@ namespace TweetDuck.Core{
this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show();
this.updates = new UpdateHandler(new UpdateCheckClient(Program.InstallerPath), TaskScheduler.FromCurrentSynchronizationContext());
this.updates = new UpdateHandler(Program.InstallerPath);
this.updates.CheckFinished += updates_CheckFinished;
this.updateBridge = new UpdateBridge(updates, this);
this.updateBridge.UpdateAccepted += updateBridge_UpdateAccepted;
this.updateBridge.UpdateDelayed += updateBridge_UpdateDelayed;
this.updateBridge.UpdateDismissed += updateBridge_UpdateDismissed;
this.browser = new TweetDeckBrowser(this, plugins, new TweetDeckBridge.Browser(this, notification), updateBridge);
@@ -85,6 +86,9 @@ namespace TweetDuck.Core{
Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
this.taskbarIcon = new TaskbarIcon();
Shown += (sender, args) => taskbarIcon.UpdateIcon();
Disposed += (sender, args) => {
Config.MuteToggled -= Config_MuteToggled;
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
@@ -92,6 +96,7 @@ namespace TweetDuck.Core{
browser.Dispose();
updates.Dispose();
contextMenu.Dispose();
taskbarIcon.Dispose();
notificationScreenshotManager?.Dispose();
videoPlayer?.Dispose();
@@ -106,10 +111,6 @@ namespace TweetDuck.Core{
UpdateTray();
if (Config.MuteNotifications){
UpdateFormIcon();
}
if (Config.AllowDataCollection){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
@@ -135,10 +136,6 @@ namespace TweetDuck.Core{
isLoaded = true;
}
private void UpdateFormIcon(){ // TODO fix to show icon in taskbar too
Icon = Config.MuteNotifications ? Properties.Resources.icon_muted : Properties.Resources.icon;
}
private void UpdateTray(){
trayIcon.Visible = Config.TrayBehavior.ShouldDisplayIcon();
}
@@ -153,6 +150,7 @@ namespace TweetDuck.Core{
if (!isLoaded)return;
trayIcon.HasNotifications = false;
taskbarIcon.HasNotifications = false;
if (!browser.Enabled){ // when taking a screenshot, the window is unfocused and
browser.Enabled = true; // the browser is disabled; if the user clicks back into
@@ -214,7 +212,6 @@ namespace TweetDuck.Core{
}
private void Config_MuteToggled(object sender, EventArgs e){
UpdateFormIcon();
AnalyticsFile.NotificationMutes.Trigger();
}
@@ -235,9 +232,7 @@ namespace TweetDuck.Core{
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
this.InvokeAsyncSafe(() => { // TODO not needed but makes code consistent...
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
});
}
if (isLoaded){
@@ -245,11 +240,9 @@ namespace TweetDuck.Core{
}
}
private void plugins_Executed(object sender, PluginErrorEventArgs e){
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
this.InvokeAsyncSafe(() => {
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
});
}
}
@@ -322,6 +315,10 @@ namespace TweetDuck.Core{
}
}
private void updateBridge_UpdateDelayed(object sender, UpdateInfo update){
// stops the timer
}
private void updateBridge_UpdateDismissed(object sender, UpdateInfo update){
Config.DismissedUpdate = update.VersionTag;
Config.Save();
@@ -329,9 +326,7 @@ namespace TweetDuck.Core{
protected override void WndProc(ref Message m){
if (isLoaded && m.Msg == Program.WindowRestoreMessage){
using Process me = Process.GetCurrentProcess();
if (me.Id == m.WParam.ToInt32()){
if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
trayIcon_ClickRestore(trayIcon, EventArgs.Empty);
}
@@ -368,7 +363,14 @@ namespace TweetDuck.Core{
}
public void ReloadToTweetDeck(){
Program.Resources.OnReloadTriggered();
#if DEBUG
ScriptLoader.HotSwap();
#else
if (ModifierKeys.HasFlag(Keys.Shift)){
ScriptLoader.ClearCache();
}
#endif
ignoreUpdateCheckError = false;
browser.ReloadToTweetDeck();
AnalyticsFile.BrowserReloads.Trigger();
@@ -499,9 +501,13 @@ namespace TweetDuck.Core{
}
public void OnTweetNotification(){ // may be called multiple times, once for each type of notification
if (Config.EnableTrayHighlight && !ContainsFocus){
if (!ContainsFocus){
if (Config.EnableTrayHighlight){
trayIcon.HasNotifications = true;
}
taskbarIcon.HasNotifications = false;
}
}
public void OnTweetSound(){

View File

@@ -17,6 +17,8 @@ namespace TweetDuck.Core{
else return false;
}
public static bool HasAnyDialogs => Application.OpenForms.OfType<IAppDialog>().Any();
public static void CloseAllDialogs(){
foreach(IAppDialog dialog in Application.OpenForms.OfType<IAppDialog>().Reverse()){
((Form)dialog).Close();

View File

@@ -6,20 +6,18 @@ using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using System.Linq;
using TweetDuck.Configuration;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Management;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
using TweetDuck.Resources;
namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{
public static ContextInfo CurrentInfo { get; } = new ContextInfo();
protected static UserConfig Config => Program.Config.User;
private static ImageQuality ImageQuality => Config.TwitterImageQuality;
private static TwitterUtils.ImageQuality ImageQuality => Config.TwitterImageQuality;
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501;
@@ -42,11 +40,11 @@ namespace TweetDuck.Core.Handling{
}
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
if (!TwitterUrls.IsTweetDeck(frame.Url) || browser.IsLoading){
Context = CurrentInfo.Reset();
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
Context = TweetDeckBridge.ContextInfo.Reset();
}
else{
Context = CurrentInfo.Create(parameters);
Context = TweetDeckBridge.ContextInfo.Create(parameters);
}
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection) && !parameters.TypeFlags.HasFlag(ContextMenuType.Editable)){
@@ -56,12 +54,12 @@ namespace TweetDuck.Core.Handling{
model.AddSeparator();
}
static string TextOpen(string name) => "Open " + name + " in browser";
static string TextCopy(string name) => "Copy " + name + " address";
static string TextSave(string name) => "Save " + name + " as...";
string TextOpen(string name) => "Open "+name+" in browser";
string TextCopy(string name) => "Copy "+name+" address";
string TextSave(string name) => "Save "+name+" as...";
if (Context.Types.HasFlag(ContextInfo.ContextType.Link) && !Context.UnsafeLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal)){
if (TwitterUrls.RegexAccount.IsMatch(Context.UnsafeLinkUrl)){
if (TwitterUtils.RegexAccount.IsMatch(Context.UnsafeLinkUrl)){
model.AddItem(MenuOpenLinkUrl, TextOpen("account"));
model.AddItem(MenuCopyLinkUrl, TextCopy("account"));
model.AddItem(MenuCopyUsername, "Copy account username");
@@ -80,7 +78,7 @@ namespace TweetDuck.Core.Handling{
model.AddItem(MenuSaveMedia, TextSave("video"));
model.AddSeparator();
}
else if (Context.Types.HasFlag(ContextInfo.ContextType.Image) && Context.MediaUrl != FormNotificationBase.AppLogo.Url){
else if (Context.Types.HasFlag(ContextInfo.ContextType.Image) && Context.MediaUrl != TweetNotification.AppLogo.Url){
model.AddItem(MenuViewImage, "View image in photo viewer");
model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
@@ -108,7 +106,7 @@ namespace TweetDuck.Core.Handling{
case MenuCopyUsername: {
string url = Context.UnsafeLinkUrl;
Match match = TwitterUrls.RegexAccount.Match(url);
Match match = TwitterUtils.RegexAccount.Match(url);
SetClipboardText(control, match.Success ? match.Groups[1].Value : url);
control.InvokeAsyncSafe(analytics.AnalyticsFile.CopiedUsernames.Trigger);
@@ -116,11 +114,11 @@ namespace TweetDuck.Core.Handling{
}
case MenuOpenMediaUrl:
OpenBrowser(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
OpenBrowser(control, TwitterUtils.GetMediaLink(Context.MediaUrl, ImageQuality));
break;
case MenuCopyMediaUrl:
SetClipboardText(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
SetClipboardText(control, TwitterUtils.GetMediaLink(Context.MediaUrl, ImageQuality));
break;
case MenuViewImage: {
@@ -186,7 +184,7 @@ namespace TweetDuck.Core.Handling{
}
public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
Context = CurrentInfo.Reset();
Context = TweetDeckBridge.ContextInfo.Reset();
}
public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback){
@@ -194,7 +192,7 @@ namespace TweetDuck.Core.Handling{
}
protected static void DeselectAll(IFrame frame){
CefScriptExecutor.RunScript(frame, "window.getSelection().removeAllRanges()", "gen:deselect");
ScriptLoader.ExecuteScript(frame, "window.getSelection().removeAllRanges()", "gen:deselect");
}
protected static void OpenBrowser(Control control, string url){

View File

@@ -2,7 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Management;
using TweetLib.Core.Features.Twitter;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuBrowser : ContextMenuBase{
@@ -53,7 +53,7 @@ namespace TweetDuck.Core.Handling{
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (isSelecting && !isEditing && TwitterUrls.IsTweetDeck(frame.Url)){
if (isSelecting && !isEditing && TwitterUtils.IsTweetDeckWebsite(frame)){
InsertSelectionSearchItem(model, MenuSearchInColumn, "Search in a column");
}

View File

@@ -11,12 +11,13 @@ namespace TweetDuck.Core.Handling.General{
private static void UpdatePrefsInternal(){
UserConfig config = Program.Config.User;
using IRequestContext ctx = Cef.GetGlobalRequestContext();
using(IRequestContext ctx = Cef.GetGlobalRequestContext()){
ctx.SetPreference("browser.enable_spellchecking", config.EnableSpellCheck, out string _);
ctx.SetPreference("spellcheck.dictionary", config.SpellCheckLanguage, out string _);
ctx.SetPreference("settings.a11y.animation_policy", config.EnableAnimatedImages ? "allowed" : "none", out string _);
}
}
void IBrowserProcessHandler.OnContextInitialized(){
UpdatePrefsInternal();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
@@ -8,7 +9,7 @@ namespace TweetDuck.Core.Handling.General{
sealed class FileDialogHandler : IDialogHandler{
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback){
if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple){
string allFilters = string.Join(";", acceptFilters.SelectMany(ParseFileType).Where(filter => !string.IsNullOrEmpty(filter)).Select(filter => "*" + filter));
string allFilters = string.Join(";", acceptFilters.Select(filter => "*"+filter));
using(OpenFileDialog dialog = new OpenFileDialog{
AutoUpgradeEnabled = true,
@@ -18,8 +19,8 @@ namespace TweetDuck.Core.Handling.General{
Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*"
}){
if (dialog.ShowDialog() == DialogResult.OK){
string ext = Path.GetExtension(dialog.FileName)?.ToLower();
callback.Continue(acceptFilters.FindIndex(filter => ParseFileType(filter).Contains(ext)), dialog.FileNames.ToList());
string ext = Path.GetExtension(dialog.FileName);
callback.Continue(acceptFilters.FindIndex(filter => filter.Equals(ext, StringComparison.OrdinalIgnoreCase)), dialog.FileNames.ToList());
}
else{
callback.Cancel();
@@ -35,27 +36,5 @@ namespace TweetDuck.Core.Handling.General{
return false;
}
}
private static IEnumerable<string> ParseFileType(string type){
if (string.IsNullOrEmpty(type)){
return new string[0];
}
if (type[0] == '.'){
return new string[]{ type };
}
switch(type){
case "image/jpeg": return new string[]{ ".jpg", ".jpeg" };
case "image/png": return new string[]{ ".png" };
case "image/gif": return new string[]{ ".gif" };
case "image/webp": return new string[]{ ".webp" };
case "video/mp4": return new string[]{ ".mp4" };
case "video/quicktime": return new string[]{ ".mov", ".qt" };
}
System.Diagnostics.Debugger.Break();
return new string[0];
}
}
}

View File

@@ -12,17 +12,15 @@ namespace TweetDuck.Core.Handling.General{
int pipe = text.IndexOf('|');
if (pipe != -1){
icon = text.Substring(0, pipe) switch{
"error" => MessageBoxIcon.Error,
"warning" => MessageBoxIcon.Warning,
"info" => MessageBoxIcon.Information,
"question" => MessageBoxIcon.Question,
_ => MessageBoxIcon.None
};
if (icon != MessageBoxIcon.None){
text = text.Substring(pipe + 1);
switch(text.Substring(0, pipe)){
case "error": icon = MessageBoxIcon.Error; break;
case "warning": icon = MessageBoxIcon.Warning; break;
case "info": icon = MessageBoxIcon.Information; break;
case "question": icon = MessageBoxIcon.Question; break;
default: return new FormMessage(caption, text, icon);
}
text = text.Substring(pipe+1);
}
return new FormMessage(caption, text, icon);

View File

@@ -4,15 +4,11 @@ using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling.General{
sealed class LifeSpanHandler : ILifeSpanHandler{
private static bool IsPopupAllowed(string url){
return url.StartsWith("https://twitter.com/teams/authorize?");
}
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl){
switch(targetDisposition){
case WindowOpenDisposition.NewBackgroundTab:
case WindowOpenDisposition.NewForegroundTab:
case WindowOpenDisposition.NewPopup when !IsPopupAllowed(targetUrl):
case WindowOpenDisposition.NewPopup:
case WindowOpenDisposition.NewWindow:
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
return true;

View File

@@ -7,11 +7,10 @@ using CefSharp;
using CefSharp.Handler;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{
class RequestHandlerBase : DefaultRequestHandler{
private static readonly Regex TweetDeckResourceUrl = new Regex(@"/dist/(.*?)\.(.*?)\.(css|js)$");
private static readonly Regex TweetDeckResourceUrl = new Regex(@"/dist/(.*?)\.(.*?)\.(css|js)$", RegexOptions.Compiled);
private static readonly SortedList<string, string> TweetDeckHashes = new SortedList<string, string>(4);
public static void LoadResourceRewriteRules(string rules){
@@ -22,7 +21,11 @@ namespace TweetDuck.Core.Handling{
TweetDeckHashes.Clear();
foreach(string rule in rules.Replace(" ", "").ToLower().Split(',')){
var (key, hash) = StringUtils.SplitInTwo(rule, '=') ?? throw new ArgumentException("A rule must have one '=' character: " + rule);
string[] split = rule.Split('=');
if (split.Length == 2){
string key = split[0];
string hash = split[1];
if (hash.All(chr => char.IsDigit(chr) || (chr >= 'a' && chr <= 'f'))){
TweetDeckHashes.Add(key, hash);
@@ -31,6 +34,10 @@ namespace TweetDuck.Core.Handling{
throw new ArgumentException("Invalid hash characters: "+rule);
}
}
else{
throw new ArgumentException("A rule must have exactly one '=' character: "+rule);
}
}
}
private readonly bool autoReload;

View File

@@ -2,7 +2,6 @@
using CefSharp;
using TweetDuck.Core.Handling.Filters;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Handling{
sealed class RequestHandlerBrowser : RequestHandlerBase{
@@ -16,7 +15,7 @@ namespace TweetDuck.Core.Handling{
public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){
if (request.ResourceType == ResourceType.MainFrame){
if (request.Url.EndsWith("//twitter.com/")){
request.Url = TwitterUrls.TweetDeck; // redirect plain twitter.com requests, fixes bugs with login 2FA
request.Url = TwitterUtils.TweetDeckURL; // redirect plain twitter.com requests, fixes bugs with login 2FA
}
}
else if (request.ResourceType == ResourceType.Script){
@@ -42,9 +41,6 @@ namespace TweetDuck.Core.Handling{
BlockNextUserNavUrl = string.Empty;
return block;
}
else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)){
return true;
}
return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
}

View File

@@ -1,6 +1,6 @@
using System;
using CefSharp;
using TweetLib.Core.Utils;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Management{
sealed class ContextInfo{
@@ -107,7 +107,7 @@ namespace TweetDuck.Core.Management{
private string unsafeLinkUrl = string.Empty;
private string mediaUrl = string.Empty;
private ChirpInfo chirp = default;
private ChirpInfo chirp = default(ChirpInfo);
public void AddContext(IContextMenuParams parameters){
ContextMenuType flags = parameters.TypeFlags;

View File

@@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using TweetDuck.Core.Other;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
namespace TweetDuck.Core.Management{
sealed class ProfileManager{
@@ -73,7 +73,7 @@ namespace TweetDuck.Core.Management{
Items items = Items.None;
try{
using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None));
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))){
string key;
while((key = stream.SkipFile()) != null){
@@ -96,6 +96,7 @@ namespace TweetDuck.Core.Management{
break;
}
}
}
}catch(Exception){
items = Items.None;
}
@@ -139,7 +140,7 @@ namespace TweetDuck.Core.Management{
entry.WriteToFile(Path.Combine(Program.PluginDataPath, value[0], value[1]), true);
if (!plugins.Plugins.Any(plugin => plugin.Identifier.Equals(value[0]))){
if (!plugins.IsPluginInstalled(value[0])){
missingPlugins.Add(value[0]);
}
}

View File

@@ -2,17 +2,17 @@
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Controls;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetDuck.Plugins;
using TweetDuck.Resources;
namespace TweetDuck.Core.Notification.Example{
sealed class FormNotificationExample : FormNotificationMain{
public override bool RequiresResize => true;
protected override bool CanDragWindow => Config.NotificationPosition == DesktopNotification.Position.Custom;
protected override bool CanDragWindow => Config.NotificationPosition == TweetNotification.Position.Custom;
protected override FormBorderStyle NotificationBorderStyle{
get{
if (Config.NotificationSize == DesktopNotification.Size.Custom){
if (Config.NotificationSize == TweetNotification.Size.Custom){
switch(base.NotificationBorderStyle){
case FormBorderStyle.FixedSingle: return FormBorderStyle.Sizable;
case FormBorderStyle.FixedToolWindow: return FormBorderStyle.SizableToolWindow;
@@ -27,18 +27,18 @@ namespace TweetDuck.Core.Notification.Example{
public event EventHandler Ready;
private readonly DesktopNotification exampleNotification;
private readonly TweetNotification exampleNotification;
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false){
browser.LoadingStateChanged += browser_LoadingStateChanged;
string exampleTweetHTML = Program.Resources.LoadSilent("pages/example.html")?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
string exampleTweetHTML = ScriptLoader.LoadResourceSilent("pages/example.html")?.Replace("{avatar}", TweetNotification.AppLogo.Url) ?? string.Empty;
#if DEBUG
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
#endif
exampleNotification = new DesktopNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
exampleNotification = new TweetNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
}
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){

View File

@@ -1,34 +1,28 @@
using CefSharp.WinForms;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Configuration;
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.Utils;
using TweetDuck.Data;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Notification{
abstract partial class FormNotificationBase : Form, AnalyticsFile.IProvider{
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandler.FromByteArray(Properties.Resources.avatar, "image/png"));
public static string FontSize = null;
public static string HeadLayout = null;
protected static UserConfig Config => Program.Config.User;
protected static int FontSizeLevel{
get => FontSize switch{
"largest" => 4,
"large" => 3,
"small" => 1,
"smallest" => 0,
_ => 2
};
get{
switch(TweetDeckBridge.FontSize){
case "largest": return 4;
case "large": return 3;
case "small": return 1;
case "smallest": return 0;
default: return 2;
}
}
}
protected virtual Point PrimaryLocation{
@@ -45,19 +39,19 @@ namespace TweetDuck.Core.Notification{
int edgeDist = Config.NotificationEdgeDistance;
switch(Config.NotificationPosition){
case DesktopNotification.Position.TopLeft:
case TweetNotification.Position.TopLeft:
return new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+edgeDist);
case DesktopNotification.Position.TopRight:
case TweetNotification.Position.TopRight:
return new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
case DesktopNotification.Position.BottomLeft:
case TweetNotification.Position.BottomLeft:
return new Point(screen.WorkingArea.X+edgeDist, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
case DesktopNotification.Position.BottomRight:
case TweetNotification.Position.BottomRight:
return new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+screen.WorkingArea.Height-edgeDist-Height);
case DesktopNotification.Position.Custom:
case TweetNotification.Position.Custom:
if (!Config.IsCustomNotificationPositionSet){
Config.CustomNotificationPosition = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-edgeDist-Width, screen.WorkingArea.Y+edgeDist);
Config.Save();
@@ -107,7 +101,7 @@ namespace TweetDuck.Core.Notification{
private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification();
private DesktopNotification currentNotification;
private TweetNotification currentNotification;
private int pauseCounter;
public string CurrentTweetUrl => currentNotification?.TweetUrl;
@@ -128,8 +122,8 @@ namespace TweetDuck.Core.Notification{
this.owner.FormClosed += owner_FormClosed;
ResourceHandlerFactory resourceHandlerFactory = new ResourceHandlerFactory();
resourceHandlerFactory.RegisterHandler(TwitterUrls.TweetDeck, this.resourceHandler);
resourceHandlerFactory.RegisterHandler(AppLogo);
resourceHandlerFactory.RegisterHandler(TwitterUtils.TweetDeckURL, this.resourceHandler);
resourceHandlerFactory.RegisterHandler(TweetNotification.AppLogo);
this.browser = new ChromiumWebBrowser("about:blank"){
MenuHandler = new ContextMenuNotification(this, enableContextMenu),
@@ -194,13 +188,13 @@ namespace TweetDuck.Core.Notification{
}
}
protected abstract string GetTweetHTML(DesktopNotification tweet);
protected abstract string GetTweetHTML(TweetNotification tweet);
protected virtual void LoadTweet(DesktopNotification tweet){
protected virtual void LoadTweet(TweetNotification tweet){
currentNotification = tweet;
resourceHandler.SetHTML(GetTweetHTML(tweet));
browser.Load(TwitterUrls.TweetDeck);
browser.Load(TwitterUtils.TweetDeckURL);
DisplayTooltip(null);
}

View File

@@ -2,16 +2,14 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetDuck.Plugins.Enums;
using TweetDuck.Resources;
namespace TweetDuck.Core.Notification{
abstract partial class FormNotificationMain : FormNotificationBase{
@@ -46,17 +44,27 @@ namespace TweetDuck.Core.Notification{
}
private int BaseClientWidth{
get => Config.NotificationSize switch{
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Width,
_ => BrowserUtils.Scale(284, SizeScale * (1.0 + 0.05 * FontSizeLevel))
};
get{
switch(Config.NotificationSize){
default:
return BrowserUtils.Scale(284, SizeScale*(1.0+0.05*FontSizeLevel));
case TweetNotification.Size.Custom:
return Config.CustomNotificationSize.Width;
}
}
}
private int BaseClientHeight{
get => Config.NotificationSize switch{
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Height,
_ => BrowserUtils.Scale(122, SizeScale * (1.0 + 0.08 * FontSizeLevel))
};
get{
switch(Config.NotificationSize){
default:
return BrowserUtils.Scale(122, SizeScale*(1.0+0.08*FontSizeLevel));
case TweetNotification.Size.Custom:
return Config.CustomNotificationSize.Height;
}
}
}
protected virtual string BodyClasses => IsCursorOverBrowser ? "td-notification td-hover" : "td-notification";
@@ -75,7 +83,7 @@ namespace TweetDuck.Core.Notification{
browser.LoadingStateChanged += Browser_LoadingStateChanged;
browser.FrameLoadEnd += Browser_FrameLoadEnd;
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser));
plugins.Register(browser, PluginEnvironment.Notification, this);
mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook(true);
@@ -156,7 +164,7 @@ namespace TweetDuck.Core.Notification{
if (frame.IsMain && browser.Address != "about:blank"){
frame.ExecuteJavaScriptAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification));
CefScriptExecutor.RunFile(frame, "notification.js");
ScriptLoader.ExecuteFile(frame, "notification.js", this);
}
}
@@ -166,16 +174,7 @@ namespace TweetDuck.Core.Notification{
}
private void timerHideProgress_Tick(object sender, EventArgs e){
bool isCursorInside = Bounds.Contains(Cursor.Position);
if (isCursorInside){
StartMouseHook();
}
else{
StopMouseHook(false);
}
if (isCursorInside || FreezeTimer || ContextMenuOpen){
if (Bounds.Contains(Cursor.Position) || FreezeTimer || ContextMenuOpen){
return;
}
@@ -191,7 +190,7 @@ namespace TweetDuck.Core.Notification{
// notification methods
public virtual void ShowNotification(DesktopNotification notification){
public virtual void ShowNotification(TweetNotification notification){
LoadTweet(notification);
}
@@ -228,8 +227,8 @@ namespace TweetDuck.Core.Notification{
}
}
protected override string GetTweetHTML(DesktopNotification tweet){
string html = tweet.GenerateHtml(BodyClasses, HeadLayout, Config.CustomNotificationCSS);
protected override string GetTweetHTML(TweetNotification tweet){
string html = tweet.GenerateHtml(BodyClasses, this);
foreach(InjectedHTML injection in plugins.NotificationInjections){
html = injection.InjectInto(html);
@@ -238,7 +237,7 @@ namespace TweetDuck.Core.Notification{
return html;
}
protected override void LoadTweet(DesktopNotification tweet){
protected override void LoadTweet(TweetNotification tweet){
timerProgress.Stop();
totalTime = timeLeft = tweet.GetDisplayDuration(Config.NotificationDurationValue);
progressBarTimer.Value = Config.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum;
@@ -266,6 +265,7 @@ namespace TweetDuck.Core.Notification{
}
MoveToVisibleLocation();
StartMouseHook();
}
protected virtual void OnNotificationReady(){

View File

@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using TweetDuck.Plugins;
using System.Windows.Forms;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Notification{
sealed partial class FormNotificationTweet : FormNotificationMain{
@@ -26,7 +25,7 @@ namespace TweetDuck.Core.Notification{
}
}
private readonly Queue<DesktopNotification> tweetQueue = new Queue<DesktopNotification>(4);
private readonly Queue<TweetNotification> tweetQueue = new Queue<TweetNotification>(4);
private bool needsTrim;
private bool hasTemporarilyMoved;
@@ -82,7 +81,7 @@ namespace TweetDuck.Core.Notification{
// notification methods
public override void ShowNotification(DesktopNotification notification){
public override void ShowNotification(TweetNotification notification){
tweetQueue.Enqueue(notification);
if (!IsPaused){

View File

@@ -3,13 +3,12 @@ using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Resources;
namespace TweetDuck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotificationBase{
@@ -30,23 +29,24 @@ namespace TweetDuck.Core.Notification.Screenshot{
return;
}
string script = Program.Resources.LoadSilent("screenshot.js");
string script = ScriptLoader.LoadResourceSilent("screenshot.js");
if (script == null){
this.InvokeAsyncSafe(callback);
return;
}
using IFrame frame = args.Browser.MainFrame;
CefScriptExecutor.RunScript(frame, script.Replace("{width}", realWidth.ToString()).Replace("{frames}", TweetScreenshotManager.WaitFrames.ToString()), "gen:screenshot");
using(IFrame frame = args.Browser.MainFrame){
ScriptLoader.ExecuteScript(frame, script.Replace("{width}", realWidth.ToString()).Replace("{frames}", TweetScreenshotManager.WaitFrames.ToString()), "gen:screenshot");
}
};
SetNotificationSize(realWidth, 1024);
LoadTweet(new DesktopNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty));
LoadTweet(new TweetNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty));
}
protected override string GetTweetHTML(DesktopNotification tweet){
string html = tweet.GenerateHtml("td-screenshot", HeadLayout, Config.CustomNotificationCSS);
protected override string GetTweetHTML(TweetNotification tweet){
string html = tweet.GenerateHtml("td-screenshot", this);
foreach(InjectedHTML injection in plugins.NotificationInjections){
html = injection.InjectInto(html);

View File

@@ -1,10 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
namespace TweetDuck.Core.Notification.Screenshot{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
sealed class ScreenshotBridge{
private readonly Control owner;

View File

@@ -9,7 +9,7 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetLib.Core.Features.Plugins;
using TweetDuck.Plugins;
#if GEN_SCREENSHOT_FRAMES
using System.Drawing.Imaging;

View File

@@ -11,16 +11,18 @@ namespace TweetDuck.Core.Notification{
public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
public static IResourceHandler CreateFileHandler(string path){
string mimeType = Path.GetExtension(path) switch{
".weba" => "audio/webm",
".webm" => "audio/webm",
".wav" => "audio/wav",
".ogg" => "audio/ogg",
".mp3" => "audio/mp3",
".flac" => "audio/flac",
".opus" => "audio/ogg; codecs=opus",
_ => null
};
string mimeType;
switch(Path.GetExtension(path)){
case ".weba":
case ".webm": mimeType = "audio/webm"; break;
case ".wav": mimeType = "audio/wav"; break;
case ".ogg": mimeType = "audio/ogg"; break;
case ".mp3": mimeType = "audio/mp3"; break;
case ".flac": mimeType = "audio/flac"; break;
case ".opus": mimeType = "audio/ogg; codecs=opus"; break;
default: mimeType = null; break;
}
try{
return ResourceHandler.FromFilePath(path, mimeType);

View File

@@ -1,9 +1,15 @@
using System;
using System.Text;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Bridge;
using TweetDuck.Data;
using TweetDuck.Resources;
namespace TweetLib.Core.Features.Notifications{
public sealed class DesktopNotification{
namespace TweetDuck.Core.Notification{
sealed class TweetNotification{
private const string DefaultHeadLayout = @"<html class=""scroll-v os-windows dark txt-size--14"" lang=""en-US"" id=""tduck"" data-td-font=""medium"" data-td-theme=""dark""><head><meta charset=""utf-8""><link href=""https://ton.twimg.com/tweetdeck-web/web/dist/bundle.4b1f87e09d.css"" rel=""stylesheet""><style type='text/css'>body { background: rgb(34, 36, 38) !important }</style>";
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandler.FromByteArray(Properties.Resources.avatar, "image/png"));
public enum Position{
TopLeft, TopRight, BottomLeft, BottomRight, Custom
@@ -23,7 +29,7 @@ namespace TweetLib.Core.Features.Notifications{
private readonly string html;
private readonly int characters;
public DesktopNotification(string columnId, string chirpId, string title, string html, int characters, string tweetUrl, string quoteUrl){
public TweetNotification(string columnId, string chirpId, string title, string html, int characters, string tweetUrl, string quoteUrl){
this.ColumnId = columnId;
this.ChirpId = chirpId;
@@ -39,19 +45,18 @@ namespace TweetLib.Core.Features.Notifications{
return 2000+Math.Max(1000, value*characters);
}
public string GenerateHtml(string bodyClasses, string? headLayout, string? customStyles){ // TODO
headLayout ??= DefaultHeadLayout;
customStyles ??= string.Empty;
public string GenerateHtml(string bodyClasses, Control sync){
string headLayout = TweetDeckBridge.NotificationHeadLayout ?? DefaultHeadLayout;
string mainCSS = ScriptLoader.LoadResource("styles/notification.css", sync) ?? string.Empty;
string customCSS = Program.Config.User.CustomNotificationCSS ?? string.Empty;
string mainCSS = App.ResourceHandler.Load("styles/notification.css") ?? string.Empty;
StringBuilder build = new StringBuilder(320 + headLayout.Length + mainCSS.Length + customStyles.Length + html.Length);
StringBuilder build = new StringBuilder(320 + headLayout.Length + mainCSS.Length + customCSS.Length + html.Length);
build.Append("<!DOCTYPE html>");
build.Append(headLayout);
build.Append("<style type='text/css'>").Append(mainCSS).Append("</style>");
if (!string.IsNullOrWhiteSpace(customStyles)){
build.Append("<style type='text/css'>").Append(customStyles).Append("</style>");
if (!string.IsNullOrWhiteSpace(customCSS)){
build.Append("<style type='text/css'>").Append(customCSS).Append("</style>");
}
build.Append("</head><body class='scroll-styled-v");

View File

@@ -2,8 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using TweetLib.Core.Serialization;
using TweetLib.Core.Serialization.Converters;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Core.Other.Analytics{
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local")]

View File

@@ -8,9 +8,7 @@ using System.Threading.Tasks;
using System.Timers;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils;
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other.Analytics{
sealed class AnalyticsManager : IDisposable{
@@ -82,7 +80,7 @@ namespace TweetDuck.Core.Other.Analytics{
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", Lib.Culture);
File.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture);
File.Save();
RestartTimer();
@@ -119,7 +117,7 @@ namespace TweetDuck.Core.Other.Analytics{
System.Diagnostics.Debugger.Break();
#endif
WebUtils.NewClient(BrowserUtils.UserAgentVanilla).UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
}).ContinueWith(task => browser.InvokeAsyncSafe(() => {
if (task.Status == TaskStatus.RanToCompletion){
SetLastDataCollectionTime(DateTime.Now);

View File

@@ -8,12 +8,10 @@ using TweetDuck.Configuration;
using System.Linq;
using System.Management;
using System.Text.RegularExpressions;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils;
using TweetLib.Core;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
namespace TweetDuck.Core.Other.Analytics{
static class AnalyticsReportGenerator{
@@ -29,7 +27,7 @@ namespace TweetDuck.Core.Other.Analytics{
{ "System Edition" , SystemEdition },
{ "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" },
{ "System Build" , SystemBuild },
{ "System Locale" , Lib.Culture.Name.ToLower() },
{ "System Locale" , Program.Culture.Name.ToLower() },
0,
{ "RAM" , Exact(RamSize) },
{ "GPU" , GpuVendor },
@@ -81,7 +79,7 @@ namespace TweetDuck.Core.Other.Analytics{
{ "Custom Notification CSS" , RoundUp((UserConfig.CustomNotificationCSS ?? string.Empty).Length, 50) },
0,
{ "Plugins All" , List(plugins.Plugins.Select(Plugin)) },
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugins.Config.IsEnabled).Select(Plugin)) },
{ "Plugins Enabled" , List(plugins.Plugins.Where(plugin => plugins.Config.IsEnabled(plugin)).Select(Plugin)) },
0,
{ "Theme" , Dict(editLayoutDesign, "_theme", "light/def") },
{ "Column Width" , Dict(editLayoutDesign, "columnWidth", "310px/def") },
@@ -143,8 +141,7 @@ namespace TweetDuck.Core.Other.Analytics{
string osName, osEdition, osBuild;
try{
using RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false);
using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false)){
// ReSharper disable once PossibleNullReferenceException
osName = key.GetValue("ProductName") as string;
osBuild = key.GetValue("CurrentBuild") as string;
@@ -158,6 +155,7 @@ namespace TweetDuck.Core.Other.Analytics{
osEdition = match.Groups[2].Value;
}
}
}
}catch{
osName = osEdition = osBuild = null;
}
@@ -167,11 +165,11 @@ namespace TweetDuck.Core.Other.Analytics{
SystemBuild = osBuild ?? "(unknown)";
try{
using ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Capacity FROM Win32_PhysicalMemory");
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;
}
@@ -179,8 +177,7 @@ namespace TweetDuck.Core.Other.Analytics{
string gpu = null;
try{
using ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController");
using(ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_VideoController")){
foreach(ManagementBaseObject obj in searcher.Get()){
string vendor = obj["Caption"] as string;
@@ -188,6 +185,7 @@ namespace TweetDuck.Core.Other.Analytics{
gpu = vendor;
}
}
}
}catch{
// rip
}
@@ -206,30 +204,36 @@ namespace TweetDuck.Core.Other.Analytics{
}
private static string TrayMode{
get => UserConfig.TrayBehavior switch{
TrayIcon.Behavior.DisplayOnly => "icon",
TrayIcon.Behavior.MinimizeToTray => "minimize",
TrayIcon.Behavior.CloseToTray => "close",
TrayIcon.Behavior.Combined => "combined",
_ => "off"
};
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 => UserConfig.NotificationPosition switch{
DesktopNotification.Position.TopLeft => "top left",
DesktopNotification.Position.TopRight => "top right",
DesktopNotification.Position.BottomLeft => "bottom left",
DesktopNotification.Position.BottomRight => "bottom right",
_ => "custom"
};
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 => UserConfig.NotificationSize switch{
DesktopNotification.Size.Auto => "auto",
_ => RoundUp(UserConfig.CustomNotificationSize.Width, 20) + "x" + RoundUp(UserConfig.CustomNotificationSize.Height, 20)
};
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{

View File

@@ -7,8 +7,8 @@ using TweetDuck.Core.Handling;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Utils;
using System.Text.RegularExpressions;
using TweetDuck.Core.Adapters;
using TweetDuck.Data;
using TweetDuck.Resources;
namespace TweetDuck.Core.Other{
sealed partial class FormGuide : Form, FormManager.IAppDialog{
@@ -116,7 +116,7 @@ namespace TweetDuck.Core.Other{
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
CefScriptExecutor.RunScript(e.Frame, "Array.prototype.forEach.call(document.getElementsByTagName('A'), ele => ele.addEventListener('click', e => { e.preventDefault(); window.open(ele.getAttribute('href')); }))", "gen:links");
ScriptLoader.ExecuteScript(e.Frame, "Array.prototype.forEach.call(document.getElementsByTagName('A'), ele => ele.addEventListener('click', e => { e.preventDefault(); window.open(ele.getAttribute('href')); }))", "gen:links");
}
}
}

View File

@@ -1,6 +1,4 @@
using TweetDuck.Core.Controls;
namespace TweetDuck.Core.Other {
namespace TweetDuck.Core.Other {
partial class FormPlugins {
/// <summary>
/// Required designer variable.
@@ -29,7 +27,7 @@ namespace TweetDuck.Core.Other {
this.btnClose = new System.Windows.Forms.Button();
this.btnReload = new System.Windows.Forms.Button();
this.btnOpenFolder = new System.Windows.Forms.Button();
this.flowLayoutPlugins = new FlowLayoutPanelNoHScroll();
this.flowLayoutPlugins = new TweetDuck.Plugins.Controls.PluginListFlowLayout();
this.timerLayout = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout();
//
@@ -119,7 +117,7 @@ namespace TweetDuck.Core.Other {
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.Button btnReload;
private System.Windows.Forms.Button btnOpenFolder;
private FlowLayoutPanelNoHScroll flowLayoutPlugins;
private Plugins.Controls.PluginListFlowLayout flowLayoutPlugins;
private System.Windows.Forms.Timer timerLayout;
}
}

View File

@@ -1,11 +1,11 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Plugins;
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetDuck.Plugins.Controls;
namespace TweetDuck.Core.Other{
sealed partial class FormPlugins : Form, FormManager.IAppDialog{
@@ -95,7 +95,7 @@ namespace TweetDuck.Core.Other{
}
private void btnOpenFolder_Click(object sender, EventArgs e){
App.SystemHandler.OpenFileExplorer(pluginManager.PathCustomPlugins);
using(Process.Start("explorer.exe", '"'+pluginManager.PathCustomPlugins+'"')){}
}
private void btnReload_Click(object sender, EventArgs e){

View File

@@ -9,8 +9,8 @@ using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Updates;
using TweetDuck.Plugins;
using TweetDuck.Updates;
namespace TweetDuck.Core.Other{
sealed partial class FormSettings : Form, FormManager.IAppDialog{
@@ -195,7 +195,7 @@ namespace TweetDuck.Core.Other{
private sealed class SettingsTab{
public Button Button { get; }
public BaseTabSettings Control => control ??= constructor();
public BaseTabSettings Control => control ?? (control = constructor());
public bool IsInitialized => control != null;
private readonly Func<BaseTabSettings> constructor;

View File

@@ -9,7 +9,7 @@ namespace TweetDuck.Core.Other.Settings{
public IEnumerable<Control> InteractiveControls{
get{
static IEnumerable<Control> FindInteractiveControls(Control parent){
IEnumerable<Control> FindInteractiveControls(Control parent){
foreach(Control control in parent.Controls){
if (control is Panel subPanel){
foreach(Control subControl in FindInteractiveControls(subPanel)){

View File

@@ -5,6 +5,8 @@ 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();

View File

@@ -2,7 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetLib.Core.Collections;
using TweetDuck.Data;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsCefArgs : Form{

View File

@@ -4,8 +4,8 @@ using System.IO;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Management;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsManage : Form{

View File

@@ -1,7 +1,7 @@
using System;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetLib.Core.Collections;
using TweetDuck.Data;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsRestart : Form{
@@ -18,7 +18,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
tbDataFolder.Enabled = false;
}
else{
tbDataFolder.Text = currentArgs.GetValue(Arguments.ArgDataFolder) ?? string.Empty;
tbDataFolder.Text = currentArgs.GetValue(Arguments.ArgDataFolder, string.Empty);
tbDataFolder.TextChanged += control_Change;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Forms;
using TweetDuck.Configuration;
@@ -6,7 +7,6 @@ using TweetDuck.Core.Controls;
using TweetDuck.Core.Management;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetLib.Core;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsAdvanced : BaseTabSettings{
@@ -67,11 +67,11 @@ namespace TweetDuck.Core.Other.Settings{
#region Application
private void btnOpenAppFolder_Click(object sender, EventArgs e){
App.SystemHandler.OpenFileExplorer(Program.ProgramPath);
using(Process.Start("explorer.exe", "\""+Program.ProgramPath+"\"")){}
}
private void btnOpenDataFolder_Click(object sender, EventArgs e){
App.SystemHandler.OpenFileExplorer(Program.StoragePath);
using(Process.Start("explorer.exe", "\""+Program.StoragePath+"\"")){}
}
private void btnRestart_Click(object sender, EventArgs e){

View File

@@ -3,7 +3,7 @@ using System.Windows.Forms;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Plugins;
using TweetDuck.Plugins;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsFeedback : BaseTabSettings{

View File

@@ -6,8 +6,7 @@ using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Updates;
using TweetLib.Core.Utils;
using TweetDuck.Updates;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsGeneral : BaseTabSettings{

View File

@@ -1,8 +1,8 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Example;
using TweetLib.Core.Features.Notifications;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsNotifications : BaseTabSettings{
@@ -66,18 +66,18 @@ namespace TweetDuck.Core.Other.Settings{
toolTip.SetToolTip(radioLocCustom, "Drag the example notification window to the desired location.");
switch(Config.NotificationPosition){
case DesktopNotification.Position.TopLeft: radioLocTL.Checked = true; break;
case DesktopNotification.Position.TopRight: radioLocTR.Checked = true; break;
case DesktopNotification.Position.BottomLeft: radioLocBL.Checked = true; break;
case DesktopNotification.Position.BottomRight: radioLocBR.Checked = true; break;
case DesktopNotification.Position.Custom: radioLocCustom.Checked = true; break;
case TweetNotification.Position.TopLeft: radioLocTL.Checked = true; break;
case TweetNotification.Position.TopRight: radioLocTR.Checked = true; break;
case TweetNotification.Position.BottomLeft: radioLocBL.Checked = true; break;
case TweetNotification.Position.BottomRight: radioLocBR.Checked = true; break;
case TweetNotification.Position.Custom: radioLocCustom.Checked = true; break;
}
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = !radioLocCustom.Checked;
comboBoxDisplay.Items.Add("(Same as TweetDuck)");
foreach(Screen screen in Screen.AllScreens){
comboBoxDisplay.Items.Add($"{screen.DeviceName.TrimStart('\\', '.')} ({screen.Bounds.Width}x{screen.Bounds.Height})");
comboBoxDisplay.Items.Add(screen.DeviceName.TrimStart('\\', '.')+" ("+screen.Bounds.Width+"x"+screen.Bounds.Height+")");
}
comboBoxDisplay.SelectedIndex = Math.Min(comboBoxDisplay.Items.Count-1, Config.NotificationDisplay);
@@ -91,8 +91,8 @@ namespace TweetDuck.Core.Other.Settings{
toolTip.SetToolTip(radioSizeCustom, "Resize the example notification window to the desired size.");
switch(Config.NotificationSize){
case DesktopNotification.Size.Auto: radioSizeAuto.Checked = true; break;
case DesktopNotification.Size.Custom: radioSizeCustom.Checked = true; break;
case TweetNotification.Size.Auto: radioSizeAuto.Checked = true; break;
case TweetNotification.Size.Custom: radioSizeCustom.Checked = true; break;
}
trackBarScrollSpeed.SetValueSafe(Config.NotificationScrollSpeed);
@@ -219,10 +219,10 @@ namespace TweetDuck.Core.Other.Settings{
#region Location
private void radioLoc_CheckedChanged(object sender, EventArgs e){
if (radioLocTL.Checked)Config.NotificationPosition = DesktopNotification.Position.TopLeft;
else if (radioLocTR.Checked)Config.NotificationPosition = DesktopNotification.Position.TopRight;
else if (radioLocBL.Checked)Config.NotificationPosition = DesktopNotification.Position.BottomLeft;
else if (radioLocBR.Checked)Config.NotificationPosition = DesktopNotification.Position.BottomRight;
if (radioLocTL.Checked)Config.NotificationPosition = TweetNotification.Position.TopLeft;
else if (radioLocTR.Checked)Config.NotificationPosition = TweetNotification.Position.TopRight;
else if (radioLocBL.Checked)Config.NotificationPosition = TweetNotification.Position.BottomLeft;
else if (radioLocBR.Checked)Config.NotificationPosition = TweetNotification.Position.BottomRight;
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = true;
notification.ShowExampleNotification(false);
@@ -233,18 +233,18 @@ namespace TweetDuck.Core.Other.Settings{
Config.CustomNotificationPosition = notification.Location;
}
Config.NotificationPosition = DesktopNotification.Position.Custom;
Config.NotificationPosition = TweetNotification.Position.Custom;
comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = 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 = DesktopNotification.Position.TopRight;
Config.NotificationPosition = TweetNotification.Position.TopRight;
notification.MoveToVisibleLocation();
Config.CustomNotificationPosition = notification.Location;
Config.NotificationPosition = DesktopNotification.Position.Custom;
Config.NotificationPosition = TweetNotification.Position.Custom;
notification.MoveToVisibleLocation();
}
}
@@ -265,7 +265,7 @@ namespace TweetDuck.Core.Other.Settings{
private void radioSize_CheckedChanged(object sender, EventArgs e){
if (radioSizeAuto.Checked){
Config.NotificationSize = DesktopNotification.Size.Auto;
Config.NotificationSize = TweetNotification.Size.Auto;
}
notification.ShowExampleNotification(false);
@@ -276,7 +276,7 @@ namespace TweetDuck.Core.Other.Settings{
Config.CustomNotificationSize = notification.BrowserSize;
}
Config.NotificationSize = DesktopNotification.Size.Custom;
Config.NotificationSize = TweetNotification.Size.Custom;
notification.ShowExampleNotification(false);
}

49
Core/Other/TaskbarIcon.cs Normal file
View File

@@ -0,0 +1,49 @@
using System;
using Microsoft.WindowsAPICodePack.Taskbar;
using TweetDuck.Configuration;
using Res = TweetDuck.Properties.Resources;
namespace TweetDuck.Core.Other{
sealed class TaskbarIcon : IDisposable{
private static UserConfig Config => Program.Config.User;
public bool HasNotifications{
get{
return hasNotifications;
}
set{
if (hasNotifications != value){
hasNotifications = value;
UpdateIcon();
}
}
}
private bool hasNotifications;
public TaskbarIcon(){
Config.MuteToggled += Config_MuteToggled;
}
public void Dispose(){
Config.MuteToggled -= Config_MuteToggled;
}
private void Config_MuteToggled(object sender, EventArgs e){
UpdateIcon();
}
public void UpdateIcon(){
if (hasNotifications){
TaskbarManager.Instance.SetOverlayIcon(Res.overlay_notification, "Unread Notifications");
}
else if (Config.MuteNotifications){
TaskbarManager.Instance.SetOverlayIcon(Res.overlay_muted, "Notifications Muted");
}
else{
TaskbarManager.Instance.SetOverlayIcon(null, string.Empty);
}
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Configuration;
using TweetDuck.Core.Adapters;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
@@ -13,10 +12,8 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
using TweetDuck.Plugins.Enums;
using TweetDuck.Resources;
namespace TweetDuck.Core{
sealed class TweetDeckBrowser : IDisposable{
@@ -38,8 +35,9 @@ namespace TweetDuck.Core{
return false;
}
using IFrame frame = browser.GetBrowser().MainFrame;
return TwitterUrls.IsTweetDeck(frame.Url);
using(IFrame frame = browser.GetBrowser().MainFrame){
return TwitterUtils.IsTweetDeckWebsite(frame);
}
}
}
@@ -49,12 +47,12 @@ namespace TweetDuck.Core{
private string prevSoundNotificationPath = null;
public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge tdBridge, UpdateBridge updateBridge){
resourceHandlerFactory.RegisterHandler(FormNotificationBase.AppLogo);
resourceHandlerFactory.RegisterHandler(TweetNotification.AppLogo);
resourceHandlerFactory.RegisterHandler(TwitterUtils.LoadingSpinner);
RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck){
this.browser = new ChromiumWebBrowser(TwitterUtils.TweetDeckURL){
DialogHandler = new FileDialogHandler(),
DragHandler = new DragHandlerBrowser(requestHandler),
MenuHandler = new ContextMenuBrowser(owner),
@@ -79,7 +77,7 @@ namespace TweetDuck.Core{
this.browser.SetupZoomEvents();
owner.Controls.Add(browser);
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser));
plugins.Register(browser, PluginEnvironment.Browser, owner, true);
Config.MuteToggled += Config_MuteToggled;
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
@@ -123,16 +121,14 @@ namespace TweetDuck.Core{
IFrame frame = e.Frame;
if (frame.IsMain){
string url = frame.Url;
if (TwitterUrls.IsTwitter(url)){
string css = Program.Resources.Load("styles/twitter.css");
if (TwitterUtils.IsTwitterWebsite(frame)){
string css = ScriptLoader.LoadResource("styles/twitter.css", browser);
resourceHandlerFactory.RegisterHandler(TwitterStyleUrl, ResourceHandler.FromString(css, mimeType: "text/css"));
CefScriptExecutor.RunFile(frame, "twitter.js");
ScriptLoader.ExecuteFile(frame, "twitter.js", browser);
}
if (!TwitterUrls.IsTwitterLogin2Factor(url)){
if (!TwitterUtils.IsTwitterLogin2FactorWebsite(frame)){
frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorOverride);
}
}
@@ -140,12 +136,11 @@ namespace TweetDuck.Core{
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
IFrame frame = e.Frame;
string url = frame.Url;
if (frame.IsMain){
if (TwitterUrls.IsTweetDeck(url)){
if (TwitterUtils.IsTweetDeckWebsite(frame)){
UpdateProperties();
CefScriptExecutor.RunFile(frame, "code.js");
ScriptLoader.ExecuteFile(frame, "code.js", browser);
InjectBrowserCSS();
ReinjectCustomCSS(Config.CustomBrowserCSS);
@@ -154,18 +149,18 @@ namespace TweetDuck.Core{
TweetDeckBridge.ResetStaticProperties();
if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)){
CefScriptExecutor.RunScript(frame, "TD.storage.Account.prototype.requiresConsent = function(){ return false; }", "gen:gdpr");
ScriptLoader.ExecuteScript(frame, "TD.storage.Account.prototype.requiresConsent = function(){ return false; }", "gen:gdpr");
}
if (Config.FirstRun){
CefScriptExecutor.RunFile(frame, "introduction.js");
ScriptLoader.ExecuteFile(frame, "introduction.js", browser);
}
}
CefScriptExecutor.RunFile(frame, "update.js");
ScriptLoader.ExecuteFile(frame, "update.js", browser);
}
if (url == ErrorUrl){
if (frame.Url == ErrorUrl){
resourceHandlerFactory.UnregisterHandler(ErrorUrl);
}
}
@@ -176,13 +171,10 @@ namespace TweetDuck.Core{
}
if (!e.FailedUrl.StartsWith("http://td/", StringComparison.Ordinal)){
string errorPage = Program.Resources.LoadSilent("pages/error.html");
string errorPage = ScriptLoader.LoadResourceSilent("pages/error.html");
if (errorPage != null){
string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
resourceHandlerFactory.RegisterHandler(ErrorUrl, ResourceHandler.FromString(errorPage.Replace("{err}", errorTitle)));
resourceHandlerFactory.RegisterHandler(ErrorUrl, ResourceHandler.FromString(errorPage.Replace("{err}", BrowserUtils.GetErrorName(e.ErrorCode))));
browser.Load(ErrorUrl);
}
}
@@ -225,7 +217,7 @@ namespace TweetDuck.Core{
// javascript calls
public void ReloadToTweetDeck(){
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUrls.TweetDeck}'");
browser.ExecuteScriptAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUtils.TweetDeckURL}'");
}
public void UpdateProperties(){
@@ -233,7 +225,7 @@ namespace TweetDuck.Core{
}
public void InjectBrowserCSS(){
browser.ExecuteScriptAsync("TDGF_injectBrowserCSS", Program.Resources.Load("styles/browser.css")?.TrimEnd() ?? string.Empty);
browser.ExecuteScriptAsync("TDGF_injectBrowserCSS", ScriptLoader.LoadResource("styles/browser.css", browser)?.TrimEnd() ?? string.Empty);
}
public void ReinjectCustomCSS(string css){

View File

@@ -3,11 +3,11 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Windows.Forms;
using CefSharp.WinForms;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Core.Utils{
static class BrowserUtils{
@@ -60,12 +60,8 @@ namespace TweetDuck.Core.Utils{
}
public static void SetupZoomEvents(this ChromiumWebBrowser browser){
static void SetZoomLevel(IBrowserHost host, int percentage){
host.SetZoomLevel(Math.Log(percentage / 100.0, 1.2));
}
void UpdateZoomLevel(object sender, EventArgs args){
SetZoomLevel(browser.GetBrowserHost(), Config.ZoomLevel);
SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
}
Config.ZoomLevelChanged += UpdateZoomLevel;
@@ -73,16 +69,34 @@ namespace TweetDuck.Core.Utils{
browser.FrameLoadStart += (sender, args) => {
if (args.Frame.IsMain && Config.ZoomLevel != 100){
SetZoomLevel(args.Browser.GetHost(), Config.ZoomLevel);
SetZoomLevel(args.Browser, Config.ZoomLevel);
}
};
}
private const string TwitterTrackingUrl = "t.co";
public enum UrlCheckResult{
Invalid, Tracking, Fine
}
public static UrlCheckResult CheckUrl(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? UrlCheckResult.Tracking : UrlCheckResult.Fine;
}
}
return UrlCheckResult.Invalid;
}
public static void OpenExternalBrowser(string url){
if (string.IsNullOrWhiteSpace(url))return;
switch(TwitterUrls.Check(url)){
case TwitterUrls.UrlType.Fine:
switch(CheckUrl(url)){
case UrlCheckResult.Fine:
if (FormGuide.CheckGuideUrl(url, out string hash)){
FormGuide.Show(hash);
}
@@ -103,9 +117,9 @@ namespace TweetDuck.Core.Utils{
break;
case TwitterUrls.UrlType.Tracking:
case UrlCheckResult.Tracking:
if (Config.IgnoreTrackingUrlWarning){
goto case TwitterUrls.UrlType.Fine;
goto case UrlCheckResult.Fine;
}
using(FormMessage form = new FormMessage("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n"+url, MessageBoxIcon.Warning)){
@@ -121,22 +135,20 @@ namespace TweetDuck.Core.Utils{
}
if (result == DialogResult.Ignore || result == DialogResult.Yes){
goto case TwitterUrls.UrlType.Fine;
goto case UrlCheckResult.Fine;
}
}
break;
case TwitterUrls.UrlType.Invalid:
case UrlCheckResult.Invalid:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
break;
}
}
public static void OpenExternalSearch(string query){
if (string.IsNullOrWhiteSpace(query)){
return;
}
if (string.IsNullOrWhiteSpace(query))return;
string searchUrl = Config.SearchEngineUrl;
@@ -162,8 +174,56 @@ namespace TweetDuck.Core.Utils{
}
}
public static string GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
public static string GetErrorName(CefErrorCode code){
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
}
public static WebClient CreateWebClient(){
WindowsUtils.EnsureTLS12();
WebClient client = new WebClient{ Proxy = null };
client.Headers[HttpRequestHeader.UserAgent] = UserAgentVanilla;
return client;
}
public static WebClient DownloadFileAsync(string url, string target, string cookie, Action onSuccess, Action<Exception> onFailure){
WebClient client = CreateWebClient();
if (cookie != null){
client.Headers[HttpRequestHeader.Cookie] = cookie;
}
client.DownloadFileCompleted += (sender, args) => {
if (args.Cancelled){
try{
File.Delete(target);
}catch{
// didn't want it deleted anyways
}
}
else if (args.Error != null){
onFailure?.Invoke(args.Error);
}
else{
onSuccess?.Invoke();
}
};
client.DownloadFileAsync(new Uri(url), target);
return client;
}
public static int Scale(int baseValue, double scaleFactor){
return (int)Math.Round(baseValue*scaleFactor);
}
public static void SetZoomLevel(IBrowser browser, int percentage){
browser.GetHost().SetZoomLevel(Math.Log(percentage/100.0, 1.2));
}
}
}

View File

@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace TweetLib.Core.Utils{
public static class LocaleUtils{
namespace TweetDuck.Core.Utils{
static class LocaleUtils{
// https://cs.chromium.org/chromium/src/third_party/hunspell_dictionaries/
public static IEnumerable<Item> SpellCheckLanguages { get; } = new List<string>{
"af-ZA", "bg-BG", "ca-ES", "cs-CZ", "da-DK", "de-DE",
@@ -33,9 +33,9 @@ namespace TweetLib.Core.Utils{
private string Name => info?.NativeName ?? Code;
private readonly CultureInfo? info;
private readonly CultureInfo info;
public Item(string code, string? alt = null){
public Item(string code, string alt = null){
this.Code = code;
try{

View File

@@ -2,20 +2,10 @@
using System.Linq;
using System.Text.RegularExpressions;
namespace TweetLib.Core.Utils{
public static class StringUtils{
namespace TweetDuck.Core.Utils{
static class StringUtils{
public static readonly string[] EmptyArray = new string[0];
public static (string before, string after)? SplitInTwo(string str, char search, int startIndex = 0){
int index = str.IndexOf(search, startIndex);
if (index == -1){
return null;
}
return (str.Substring(0, index), str.Substring(index + 1));
}
public static string ExtractBefore(string str, char search, int startIndex = 0){
int index = str.IndexOf(search, startIndex);
return index == -1 ? str : str.Substring(0, index);

View File

@@ -2,33 +2,83 @@
using CefSharp;
using System.Drawing;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using TweetDuck.Core.Management;
using TweetDuck.Core.Other;
using TweetDuck.Data;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
using Cookie = CefSharp.Cookie;
namespace TweetDuck.Core.Utils{
static class TwitterUtils{
public const string TweetDeckURL = "https://tweetdeck.twitter.com";
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153);
public const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
public static readonly ResourceLink LoadingSpinner = new ResourceLink("https://ton.twimg.com/tduck/spinner", ResourceHandler.FromByteArray(Properties.Resources.spinner, "image/apng"));
private static readonly Lazy<Regex> RegexAccountLazy = new Lazy<Regex>(() => new Regex(@"^https?://twitter\.com/(?!signup$|tos$|privacy$|search$|search-)([^/?]+)/?$", RegexOptions.Compiled), false);
public static Regex RegexAccount => RegexAccountLazy.Value;
public static readonly string[] DictionaryWords = {
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
};
public static readonly string[] ValidImageExtensions = {
".jpg", ".jpeg", ".png", ".gif"
};
public enum ImageQuality{
Default, Orig
}
public static bool IsTweetDeckWebsite(IFrame frame){
return frame.Url.Contains("//tweetdeck.twitter.com/");
}
public static bool IsTwitterWebsite(IFrame frame){
return frame.Url.Contains("//twitter.com/");
}
public static bool IsTwitterLogin2FactorWebsite(IFrame frame){
return frame.Url.Contains("//twitter.com/account/login_verification");
}
private static string ExtractMediaBaseLink(string url){
int slash = url.LastIndexOf('/');
return slash == -1 ? url : StringUtils.ExtractBefore(url, ':', slash);
}
public static string GetMediaLink(string url, ImageQuality quality){
if (quality == ImageQuality.Orig){
string result = ExtractMediaBaseLink(url);
if (url.Contains("//ton.twitter.com/") && url.Contains("/ton/data/dm/")){
result += ":large";
}
else if (result != url || url.Contains("//pbs.twimg.com/media/")){
result += ":orig";
}
return result;
}
else{
return url;
}
}
public static string GetImageFileName(string url){
return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
}
public static void ViewImage(string url, ImageQuality quality){
static void ViewImageInternal(string path){
void ViewImageInternal(string path){
string ext = Path.GetExtension(path);
if (ImageUrl.ValidExtensions.Contains(ext)){
if (ValidImageExtensions.Contains(ext)){
WindowsUtils.OpenAssociatedProgram(path);
}
else{
@@ -36,13 +86,13 @@ namespace TweetDuck.Core.Utils{
}
}
string file = Path.Combine(BrowserCache.CacheFolder, TwitterUrls.GetImageFileName(url) ?? Path.GetRandomFileName());
string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
if (FileUtils.FileExistsAndNotEmpty(file)){
if (WindowsUtils.FileExistsAndNotEmpty(file)){
ViewImageInternal(file);
}
else{
DownloadFileAuth(TwitterUrls.GetMediaLink(url, quality), file, () => {
DownloadFileAuth(GetMediaLink(url, quality), file, () => {
ViewImageInternal(file);
}, ex => {
FormMessage.Error("Image Download", "An error occurred while downloading the image: "+ex.Message, FormMessage.OK);
@@ -59,10 +109,10 @@ namespace TweetDuck.Core.Utils{
return;
}
string firstImageLink = TwitterUrls.GetMediaLink(urls[0], quality);
string firstImageLink = GetMediaLink(urls[0], quality);
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
string filename = TwitterUrls.GetImageFileName(firstImageLink);
string filename = GetImageFileName(firstImageLink);
string ext = Path.GetExtension(filename); // includes dot
using(SaveFileDialog dialog = new SaveFileDialog{
@@ -73,7 +123,7 @@ namespace TweetDuck.Core.Utils{
Filter = (urls.Length == 1 ? "Image" : "Images")+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){
if (dialog.ShowDialog() == DialogResult.OK){
static void OnFailure(Exception ex){
void OnFailure(Exception ex){
FormMessage.Error("Image Download", "An error occurred while downloading the image: "+ex.Message, FormMessage.OK);
}
@@ -85,7 +135,7 @@ namespace TweetDuck.Core.Utils{
string pathExt = Path.GetExtension(dialog.FileName);
for(int index = 0; index < urls.Length; index++){
DownloadFileAuth(TwitterUrls.GetMediaLink(urls[index], quality), $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
DownloadFileAuth(GetMediaLink(urls[index], quality), $"{pathBase} {index+1}{pathExt}", null, OnFailure);
}
}
}
@@ -93,7 +143,7 @@ namespace TweetDuck.Core.Utils{
}
public static void DownloadVideo(string url, string username){
string filename = TwitterUrls.GetFileNameFromUrl(url);
string filename = BrowserUtils.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename);
using(SaveFileDialog dialog = new SaveFileDialog{
@@ -128,10 +178,7 @@ namespace TweetDuck.Core.Utils{
}
}
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentChrome);
client.Headers[HttpRequestHeader.Cookie] = cookieStr;
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(target, onSuccess, onFailure);
client.DownloadFileAsync(new Uri(url), target);
BrowserUtils.DownloadFileAsync(url, target, cookieStr, onSuccess, onFailure);
}, scheduler);
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
@@ -11,17 +12,66 @@ using Microsoft.Win32;
namespace TweetDuck.Core.Utils{
static class WindowsUtils{
private static readonly bool IsWindows8OrNewer = OSVersionEquals(major: 6, minor: 2); // windows 8/10
public static bool ShouldAvoidToolWindow { get; } = IsWindows8OrNewer;
public static bool IsAeroEnabled => IsWindows8OrNewer || (NativeMethods.DwmIsCompositionEnabled(out bool isCompositionEnabled) == 0 && isCompositionEnabled);
private static readonly Lazy<Regex> RegexStripHtmlStyles = new Lazy<Regex>(() => new Regex(@"\s?(?:style|class)="".*?"""), false);
private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false);
private static bool OSVersionEquals(int major, int minor){
private static readonly bool IsWindows8OrNewer;
private static bool HasMicrosoftBeenBroughtTo2008Yet;
public static int CurrentProcessID { get; }
public static bool ShouldAvoidToolWindow { get; }
public static bool IsAeroEnabled => IsWindows8OrNewer || (NativeMethods.DwmIsCompositionEnabled(out bool isCompositionEnabled) == 0 && isCompositionEnabled);
static WindowsUtils(){
using(Process me = Process.GetCurrentProcess()){
CurrentProcessID = me.Id;
}
Version ver = Environment.OSVersion.Version;
return ver.Major == major && ver.Minor == minor;
IsWindows8OrNewer = ver.Major == 6 && ver.Minor == 2; // windows 8/10
ShouldAvoidToolWindow = IsWindows8OrNewer;
}
public static void EnsureTLS12(){
if (!HasMicrosoftBeenBroughtTo2008Yet){
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
HasMicrosoftBeenBroughtTo2008Yet = true;
}
}
public static void CreateDirectoryForFile(string file){
string dir = Path.GetDirectoryName(file);
if (dir == null){
throw new ArgumentException("Invalid file path: "+file);
}
else if (dir.Length > 0){
Directory.CreateDirectory(dir);
}
}
public static bool CheckFolderWritePermission(string path){
string testFile = Path.Combine(path, ".test");
try{
Directory.CreateDirectory(path);
using(File.Create(testFile)){}
File.Delete(testFile);
return true;
}catch{
return false;
}
}
public static bool FileExistsAndNotEmpty(string path){
try{
return new FileInfo(path).Length > 0;
}catch{
return false;
}
}
public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
@@ -107,18 +157,16 @@ namespace TweetDuck.Core.Utils{
}
public static IEnumerable<Browser> FindInstalledBrowsers(){
static IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive){
using RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default);
using RegistryKey browserList = root.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet", false);
IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive){
using(RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default))
using(RegistryKey browserList = root.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet", false)){
if (browserList == null){
yield break;
}
foreach(string sub in browserList.GetSubKeyNames()){
using RegistryKey browserKey = browserList.OpenSubKey(sub, false);
using RegistryKey shellKey = browserKey?.OpenSubKey(@"shell\open\command");
using(RegistryKey browserKey = browserList.OpenSubKey(sub, false))
using(RegistryKey shellKey = browserKey?.OpenSubKey(@"shell\open\command")){
if (shellKey == null){
continue;
}
@@ -137,6 +185,8 @@ namespace TweetDuck.Core.Utils{
yield return new Browser(browserName, browserPath);
}
}
}
}
HashSet<Browser> browsers = new HashSet<Browser>();

View File

@@ -1,11 +1,11 @@
using System;
using System.IO;
using System.Text;
using TweetLib.Core.Utils;
using TweetDuck.Core.Utils;
namespace TweetLib.Core.Data{
public sealed class CombinedFileStream : IDisposable{
private const char KeySeparator = '|';
namespace TweetDuck.Data{
sealed class CombinedFileStream : IDisposable{
public const char KeySeparator = '|';
private readonly Stream stream;
@@ -45,7 +45,7 @@ namespace TweetLib.Core.Data{
stream.Write(contents, 0, contents.Length);
}
public Entry? ReadFile(){
public Entry ReadFile(){
int nameLength = stream.ReadByte();
if (nameLength == -1){
@@ -64,7 +64,7 @@ namespace TweetLib.Core.Data{
return new Entry(Encoding.UTF8.GetString(name), contents);
}
public string? SkipFile(){
public string SkipFile(){
int nameLength = stream.ReadByte();
if (nameLength == -1){
@@ -120,7 +120,7 @@ namespace TweetLib.Core.Data{
public void WriteToFile(string path, bool createDirectory){
if (createDirectory){
FileUtils.CreateDirectoryForFile(path);
WindowsUtils.CreateDirectoryForFile(path);
}
File.WriteAllBytes(path, contents);

View File

@@ -2,8 +2,8 @@
using System.Text;
using System.Text.RegularExpressions;
namespace TweetLib.Core.Collections{
public sealed class CommandLineArgs{
namespace TweetDuck.Data{
sealed class CommandLineArgs{
public static CommandLineArgs FromStringArray(char entryChar, string[] array){
CommandLineArgs args = new CommandLineArgs();
ReadStringArray(entryChar, array, args);
@@ -84,8 +84,12 @@ namespace TweetLib.Core.Collections{
values[key.ToLower()] = value;
}
public string? GetValue(string key){
return values.TryGetValue(key.ToLower(), out string val) ? val : null;
public bool HasValue(string key){
return values.ContainsKey(key.ToLower());
}
public string GetValue(string key, string defaultValue){
return values.TryGetValue(key.ToLower(), out string val) ? val : defaultValue;
}
public void RemoveValue(string key){
@@ -99,7 +103,7 @@ namespace TweetLib.Core.Collections{
copy.AddFlag(flag);
}
foreach(var kvp in values){
foreach(KeyValuePair<string, string> kvp in values){
copy.SetValue(kvp.Key, kvp.Value);
}
@@ -111,7 +115,7 @@ namespace TweetLib.Core.Collections{
target[flag] = "1";
}
foreach(var kvp in values){
foreach(KeyValuePair<string, string> kvp in values){
target[kvp.Key] = kvp.Value;
}
}
@@ -123,7 +127,7 @@ namespace TweetLib.Core.Collections{
build.Append(flag).Append(' ');
}
foreach(var kvp in values){
foreach(KeyValuePair<string, string> kvp in values){
build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" ");
}

View File

@@ -1,7 +1,7 @@
using System;
namespace TweetLib.Core.Data{
public sealed class InjectedHTML{
namespace TweetDuck.Data{
sealed class InjectedHTML{
public enum Position{
Before, After
}

View File

@@ -1,14 +1,14 @@
using System;
namespace TweetLib.Core.Data{
public sealed class Result<T>{
namespace TweetDuck.Data{
sealed class Result<T>{
public bool HasValue => exception == null;
public T Value => HasValue ? value : throw new InvalidOperationException("Requested value from a failed result.");
public Exception Exception => exception ?? throw new InvalidOperationException("Requested exception from a successful result.");
private readonly T value;
private readonly Exception? exception;
private readonly Exception exception;
public Result(T value){
this.value = value;
@@ -16,7 +16,7 @@ namespace TweetLib.Core.Data{
}
public Result(Exception exception){
this.value = default!;
this.value = default(T);
this.exception = exception ?? throw new ArgumentNullException(nameof(exception));
}
@@ -25,12 +25,12 @@ namespace TweetLib.Core.Data{
onSuccess(value);
}
else{
onException(exception!);
onException(exception);
}
}
public Result<R> Select<R>(Func<T, R> map){
return HasValue ? new Result<R>(map(value)) : new Result<R>(exception!);
return HasValue ? new Result<R>(map(value)) : new Result<R>(exception);
}
}
}

View File

@@ -4,11 +4,10 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
using TweetDuck.Core.Utils;
namespace TweetLib.Core.Serialization{
public sealed class FileSerializer<T>{
namespace TweetDuck.Data.Serialization{
sealed class FileSerializer<T>{
private const string NewLineReal = "\r\n";
private const string NewLineCustom = "\r~\n";
@@ -50,6 +49,8 @@ namespace TweetLib.Core.Serialization{
return build.Append(data.Substring(index)).ToString();
}
private static readonly ITypeConverter BasicSerializerObj = new BasicTypeConverter();
private readonly Dictionary<string, PropertyInfo> props;
private readonly Dictionary<Type, ITypeConverter> converters;
@@ -65,7 +66,7 @@ namespace TweetLib.Core.Serialization{
public void Write(string file, T obj){
LinkedList<string> errors = new LinkedList<string>();
FileUtils.CreateDirectoryForFile(file);
WindowsUtils.CreateDirectoryForFile(file);
using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
foreach(KeyValuePair<string, PropertyInfo> prop in props){
@@ -73,10 +74,10 @@ namespace TweetLib.Core.Serialization{
object value = prop.Value.GetValue(obj);
if (!converters.TryGetValue(type, out ITypeConverter serializer)){
serializer = ClrTypeConverter.Instance;
serializer = BasicSerializerObj;
}
if (serializer.TryWriteType(type, value, out string? converted)){
if (serializer.TryWriteType(type, value, out string converted)){
if (converted != null){
writer.Write(prop.Key);
writer.Write(' ');
@@ -141,10 +142,10 @@ namespace TweetLib.Core.Serialization{
if (props.TryGetValue(property, out PropertyInfo info)){
if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)){
serializer = ClrTypeConverter.Instance;
serializer = BasicSerializerObj;
}
if (serializer.TryReadType(info.PropertyType, value, out object? converted)){
if (serializer.TryReadType(info.PropertyType, value, out object converted)){
info.SetValue(obj, converted);
}
else{
@@ -164,5 +165,53 @@ namespace TweetLib.Core.Serialization{
}catch(FileNotFoundException){
}catch(DirectoryNotFoundException){}
}
private sealed class BasicTypeConverter : ITypeConverter{
bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
switch(Type.GetTypeCode(type)){
case TypeCode.Boolean:
converted = value.ToString();
return true;
case TypeCode.Int32:
converted = ((int)value).ToString(); // cast required for enums
return true;
case TypeCode.String:
converted = value?.ToString();
return true;
default:
converted = null;
return false;
}
}
bool ITypeConverter.TryReadType(Type type, string value, out object converted){
switch(Type.GetTypeCode(type)){
case TypeCode.Boolean:
if (bool.TryParse(value, out bool b)){
converted = b;
return true;
}
else goto default;
case TypeCode.Int32:
if (int.TryParse(value, out int i)){
converted = i;
return true;
}
else goto default;
case TypeCode.String:
converted = value;
return true;
default:
converted = null;
return false;
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
using System;
namespace TweetDuck.Data.Serialization{
interface ITypeConverter{
bool TryWriteType(Type type, object value, out string converted);
bool TryReadType(Type type, string value, out object converted);
}
}

View File

@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
namespace TweetLib.Core.Serialization{
public sealed class SerializationSoftException : Exception{
namespace TweetDuck.Data.Serialization{
sealed class SerializationSoftException : Exception{
public IList<string> Errors { get; }
public SerializationSoftException(IList<string> errors) : base(string.Join(Environment.NewLine, errors)){

View File

@@ -1,11 +1,11 @@
using System;
namespace TweetLib.Core.Serialization.Converters{
public sealed class SingleTypeConverter<T> : ITypeConverter{
namespace TweetDuck.Data.Serialization{
sealed class SingleTypeConverter<T> : ITypeConverter{
public Func<T, string> ConvertToString { get; set; }
public Func<string, T> ConvertToObject { get; set; }
bool ITypeConverter.TryWriteType(Type type, object value, out string? converted){
bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
try{
converted = ConvertToString((T)value);
return true;
@@ -15,7 +15,7 @@ namespace TweetLib.Core.Serialization.Converters{
}
}
bool ITypeConverter.TryReadType(Type type, string value, out object? converted){
bool ITypeConverter.TryReadType(Type type, string value, out object converted){
try{
converted = ConvertToObject(value);
return true;

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
namespace TweetLib.Core.Collections{
public sealed class TwoKeyDictionary<K1, K2, V>{
namespace TweetDuck.Data{
sealed class TwoKeyDictionary<K1, K2, V>{
private readonly Dictionary<K1, Dictionary<K2, V>> dict;
private readonly int innerCapacity;
@@ -85,8 +85,7 @@ namespace TweetLib.Core.Collections{
return true;
}
return false;
else return false;
}
public bool TryGetValue(K1 outerKey, K2 innerKey, out V value){
@@ -94,7 +93,7 @@ namespace TweetLib.Core.Collections{
return innerDict.TryGetValue(innerKey, out value);
}
else{
value = default!;
value = default(V);
return false;
}
}

View File

@@ -1,8 +1,8 @@
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
using TweetDuck.Core.Utils;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Data{
sealed class WindowState{

View File

@@ -1,58 +0,0 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using TweetDuck.Core.Utils;
using TweetLib.Core.Application;
namespace TweetDuck.Impl{
class LockHandler : IAppLockHandler{
private const int WaitRetryDelay = 250;
private const int RestoreFailTimeout = 2000;
private const int CloseNaturallyTimeout = 10000;
private const int CloseKillTimeout = 5000;
bool IAppLockHandler.RestoreProcess(Process process){
if (process.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)process.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)){
return true;
}
}
return false;
}
bool IAppLockHandler.CloseProcess(Process process){
try{
if (process.CloseMainWindow()){
// ReSharper disable once AccessToDisposedClosure
WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseNaturallyTimeout, WaitRetryDelay);
}
if (!process.HasExited){
process.Kill();
// ReSharper disable once AccessToDisposedClosure
WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseKillTimeout, WaitRetryDelay);
}
if (process.HasExited){
process.Dispose();
return true;
}
else{
return false;
}
}catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){
bool hasExited = CheckProcessExited(process);
process.Dispose();
return hasExited;
}
}
private static bool CheckProcessExited(Process process){
process.Refresh();
return process.HasExited;
}
}
}

View File

@@ -1,16 +0,0 @@
using System.Diagnostics;
using System.IO;
using TweetLib.Core.Application;
namespace TweetDuck.Impl{
class SystemHandler : IAppSystemHandler{
void IAppSystemHandler.OpenFileExplorer(string path){
if (File.Exists(path)){
using(Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")){}
}
else if (Directory.Exists(path)){
using(Process.Start("explorer.exe", '"' + path.Replace('/', '\\') + '"')){}
}
}
}
}

View File

@@ -1,4 +1,4 @@
namespace TweetDuck.Plugins {
namespace TweetDuck.Plugins.Controls {
partial class PluginControl {
/// <summary>
/// Required designer variable.

View File

@@ -3,10 +3,9 @@ using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetDuck.Plugins.Enums;
namespace TweetDuck.Plugins{
namespace TweetDuck.Plugins.Controls{
sealed partial class PluginControl : UserControl{
private readonly PluginManager pluginManager;
private readonly Plugin plugin;
@@ -64,11 +63,11 @@ namespace TweetDuck.Plugins{
int requiredLines = Math.Max(descriptionLines, 1+(string.IsNullOrEmpty(labelVersion.Text) ? 0 : 1)+(isConfigurable ? 1 : 0));
nextHeight = requiredLines switch{
1 => MaximumSize.Height - 2 * (font.Height - 1),
2 => MaximumSize.Height - 1 * (font.Height - 1),
_ => MaximumSize.Height
};
switch(requiredLines){
case 1: nextHeight = MaximumSize.Height-2*(font.Height-1); break;
case 2: nextHeight = MaximumSize.Height-(font.Height-1); break;
default: nextHeight = MaximumSize.Height; break;
}
if (nextHeight != Height){
timerLayout.Start();

View File

@@ -1,8 +1,13 @@
using System.Windows.Forms;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Controls{
sealed class FlowLayoutPanelNoHScroll : FlowLayoutPanel{
namespace TweetDuck.Plugins.Controls{
sealed class PluginListFlowLayout : FlowLayoutPanel{
public PluginListFlowLayout(){
FlowDirection = FlowDirection.TopDown;
WrapContents = false;
}
protected override void WndProc(ref Message m){
if (m.Msg == 0x85){ // WM_NCPAINT
NativeMethods.ShowScrollBar(Handle, NativeMethods.SB_HORZ, false); // basically fuck the horizontal scrollbar very much

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace TweetDuck.Plugins.Enums{
[Flags]
enum PluginEnvironment{
None = 0,
Browser = 1,
Notification = 2
}
static class PluginEnvironmentExtensions{
public static IEnumerable<PluginEnvironment> Values{
get{
yield return PluginEnvironment.Browser;
yield return PluginEnvironment.Notification;
}
}
public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
return environment == PluginEnvironment.Browser;
}
public static string GetPluginScriptFile(this PluginEnvironment environment){
switch(environment){
case PluginEnvironment.Browser: return "browser.js";
case PluginEnvironment.Notification: return "notification.js";
default: return null;
}
}
public static string GetPluginScriptVariables(this PluginEnvironment environment){
switch(environment){
case PluginEnvironment.Browser: return "$,$TD,$TDP,TD";
case PluginEnvironment.Notification: return "$TD,$TDP";
default: return string.Empty;
}
}
public static IReadOnlyDictionary<PluginEnvironment, T> Map<T>(T forNone, T forBrowser, T forNotification){
return new PluginEnvironmentDictionary<T>(forNone, forBrowser, forNotification);
}
[SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")]
private sealed class PluginEnvironmentDictionary<T> : IReadOnlyDictionary<PluginEnvironment, T>{
private const int TotalKeys = 3;
public IEnumerable<PluginEnvironment> Keys => Enum.GetValues(typeof(PluginEnvironment)).Cast<PluginEnvironment>();
public IEnumerable<T> Values => data;
public int Count => TotalKeys;
public T this[PluginEnvironment key] => data[(int)key];
private readonly T[] data;
public PluginEnvironmentDictionary(T forNone, T forBrowser, T forNotification){
this.data = new T[TotalKeys];
this.data[(int)PluginEnvironment.None] = forNone;
this.data[(int)PluginEnvironment.Browser] = forBrowser;
this.data[(int)PluginEnvironment.Notification] = forNotification;
}
public bool ContainsKey(PluginEnvironment key){
return key >= 0 && (int)key < TotalKeys;
}
public bool TryGetValue(PluginEnvironment key, out T value){
if (ContainsKey(key)){
value = this[key];
return true;
}
else{
value = default(T);
return false;
}
}
public IEnumerator<KeyValuePair<PluginEnvironment, T>> GetEnumerator(){
return Keys.Select(key => new KeyValuePair<PluginEnvironment, T>(key, this[key])).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@@ -0,0 +1,5 @@
namespace TweetDuck.Plugins.Enums{
enum PluginFolder{
Root, Data
}
}

View File

@@ -0,0 +1,23 @@
namespace TweetDuck.Plugins.Enums{
enum PluginGroup{
Official, Custom
}
static class PluginGroupExtensions{
public static string GetIdentifierPrefix(this PluginGroup group){
switch(group){
case PluginGroup.Official: return "official/";
case PluginGroup.Custom: return "custom/";
default: return "unknown/";
}
}
public static string GetIdentifierPrefixShort(this PluginGroup group){
switch(group){
case PluginGroup.Official: return "o/";
case PluginGroup.Custom: return "c/";
default: return "?/";
}
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
namespace TweetLib.Core.Features.Plugins.Events{
public sealed class PluginChangedStateEventArgs : EventArgs{
namespace TweetDuck.Plugins.Events{
sealed class PluginChangedStateEventArgs : EventArgs{
public Plugin Plugin { get; }
public bool IsEnabled { get; }

View File

@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
namespace TweetLib.Core.Features.Plugins.Events{
public sealed class PluginErrorEventArgs : EventArgs{
namespace TweetDuck.Plugins.Events{
sealed class PluginErrorEventArgs : EventArgs{
public bool HasErrors => Errors.Count > 0;
public IList<string> Errors { get; }

View File

@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetLib.Core.Features.Plugins.Config{
public interface IPluginConfig{
event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
using TweetDuck.Plugins.Events;
namespace TweetDuck.Plugins{
interface IPluginConfig{
IEnumerable<string> DisabledPlugins { get; }
void Reset(IEnumerable<string> newDisabledPlugins);
event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
void SetEnabled(Plugin plugin, bool enabled);
bool IsEnabled(Plugin plugin);

View File

@@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TweetLib.Core.Features.Plugins.Enums;
using TweetDuck.Plugins.Enums;
namespace TweetLib.Core.Features.Plugins{
public sealed class Plugin{
private static readonly Version AppVersion = new Version(Lib.VersionTag);
namespace TweetDuck.Plugins{
sealed class Plugin{
private static readonly Version AppVersion = new Version(Program.VersionTag);
public string Identifier { get; }
public PluginGroup Group { get; }
public PluginEnvironment Environments { get; }
public string Name { get; }
public string Description { get; }
@@ -40,15 +39,14 @@ namespace TweetLib.Core.Features.Plugins{
private readonly string pathRoot;
private readonly string pathData;
private readonly ISet<PluginEnvironment> environments;
private Plugin(PluginGroup group, string identifier, string pathRoot, string pathData, Builder builder){
this.pathRoot = pathRoot;
this.pathData = pathData;
this.environments = builder.Environments;
this.Group = group;
this.Identifier = identifier;
this.Environments = builder.Environments;
this.Name = builder.Name;
this.Description = builder.Description;
@@ -62,13 +60,9 @@ namespace TweetLib.Core.Features.Plugins{
this.CanRun = AppVersion >= RequiredVersion;
}
public bool HasEnvironment(PluginEnvironment environment){
return environments.Contains(environment);
}
public string GetScriptPath(PluginEnvironment environment){
if (environments.Contains(environment)){
string? file = environment.GetPluginScriptFile();
if (Environments.HasFlag(environment)){
string file = environment.GetPluginScriptFile();
return file != null ? Path.Combine(pathRoot, file) : string.Empty;
}
else{
@@ -77,11 +71,11 @@ namespace TweetLib.Core.Features.Plugins{
}
public string GetPluginFolder(PluginFolder folder){
return folder switch{
PluginFolder.Root => pathRoot,
PluginFolder.Data => pathData,
_ => string.Empty
};
switch(folder){
case PluginFolder.Root: return pathRoot;
case PluginFolder.Data: return pathData;
default: return string.Empty;
}
}
public string GetFullPathIfSafe(PluginFolder folder, string relativePath){
@@ -130,7 +124,7 @@ namespace TweetLib.Core.Features.Plugins{
public sealed class Builder{
private static readonly Version DefaultRequiredVersion = new Version(0, 0, 0, 0);
public string Name { get; set; } = string.Empty;
public string Name { get; set; }
public string Description { get; set; } = string.Empty;
public string Author { get; set; } = "(anonymous)";
public string Version { get; set; } = string.Empty;
@@ -139,7 +133,7 @@ namespace TweetLib.Core.Features.Plugins{
public string ConfigDefault { get; set; } = string.Empty;
public Version RequiredVersion { get; set; } = DefaultRequiredVersion;
public ISet<PluginEnvironment> Environments { get; } = new HashSet<PluginEnvironment>();
public PluginEnvironment Environments { get; private set; } = PluginEnvironment.None;
private readonly PluginGroup group;
private readonly string pathRoot;
@@ -154,7 +148,7 @@ namespace TweetLib.Core.Features.Plugins{
}
public void AddEnvironment(PluginEnvironment environment){
Environments.Add(environment);
this.Environments |= environment;
}
public Plugin BuildAndSetup(){
@@ -164,7 +158,7 @@ namespace TweetLib.Core.Features.Plugins{
throw new InvalidOperationException("Plugin is missing a name in the .meta file");
}
if (!PluginEnvironments.All.Any(plugin.HasEnvironment)){
if (plugin.Environments == PluginEnvironment.None){
throw new InvalidOperationException("Plugin has no script files");
}

127
Plugins/PluginBridge.cs Normal file
View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
namespace TweetDuck.Plugins{
sealed class PluginBridge{
private static string SanitizeCacheKey(string key){
return key.Replace('\\', '/').Trim();
}
private readonly PluginManager manager;
private readonly TwoKeyDictionary<int, string, string> fileCache = new TwoKeyDictionary<int, string, string>(4, 2);
private readonly TwoKeyDictionary<int, string, InjectedHTML> notificationInjections = new TwoKeyDictionary<int, string, InjectedHTML>(4, 1);
public IEnumerable<InjectedHTML> NotificationInjections => notificationInjections.InnerValues;
public HashSet<Plugin> WithConfigureFunction { get; } = new HashSet<Plugin>();
public PluginBridge(PluginManager manager){
this.manager = manager;
this.manager.Reloaded += manager_Reloaded;
this.manager.Config.PluginChangedState += Config_PluginChangedState;
}
// Event handlers
private void manager_Reloaded(object sender, PluginErrorEventArgs e){
fileCache.Clear();
}
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
if (!e.IsEnabled){
int token = manager.GetTokenFromPlugin(e.Plugin);
fileCache.Remove(token);
notificationInjections.Remove(token);
}
}
// Utility methods
private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
Plugin plugin = manager.GetPluginFromToken(token);
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(folder, path);
if (fullPath.Length == 0){
switch(folder){
case PluginFolder.Data: throw new ArgumentException("File path has to be relative to the plugin data folder.");
case PluginFolder.Root: throw new ArgumentException("File path has to be relative to the plugin root folder.");
default: throw new ArgumentException("Invalid folder type "+folder+", this is a TweetDuck error.");
}
}
else{
return fullPath;
}
}
private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool readCached){
cacheKey = SanitizeCacheKey(cacheKey);
if (readCached && fileCache.TryGetValue(token, cacheKey, out string cachedContents)){
return cachedContents;
}
try{
return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
}catch(FileNotFoundException){
throw new FileNotFoundException("File not found.");
}catch(DirectoryNotFoundException){
throw new DirectoryNotFoundException("Directory not found.");
}
}
// Public methods
public void WriteFile(int token, string path, string contents){
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
WindowsUtils.CreateDirectoryForFile(fullPath);
File.WriteAllText(fullPath, contents, Encoding.UTF8);
fileCache[token, SanitizeCacheKey(path)] = contents;
}
public string ReadFile(int token, string path, bool cache){
return ReadFileUnsafe(token, path, GetFullPathOrThrow(token, PluginFolder.Data, path), cache);
}
public void DeleteFile(int token, string path){
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
fileCache.Remove(token, SanitizeCacheKey(path));
File.Delete(fullPath);
}
public bool CheckFileExists(int token, string path){
return File.Exists(GetFullPathOrThrow(token, PluginFolder.Data, path));
}
public string ReadFileRoot(int token, string path){
return ReadFileUnsafe(token, "root*"+path, GetFullPathOrThrow(token, PluginFolder.Root, path), true);
}
public bool CheckFileExistsRoot(int token, string path){
return File.Exists(GetFullPathOrThrow(token, PluginFolder.Root, path));
}
public void InjectIntoNotificationsBefore(int token, string key, string search, string html){
notificationInjections[token, key] = new InjectedHTML(InjectedHTML.Position.Before, search, html);
}
public void InjectIntoNotificationsAfter(int token, string key, string search, string html){
notificationInjections[token, key] = new InjectedHTML(InjectedHTML.Position.After, search, html);
}
public void SetConfigurable(int token){
Plugin plugin = manager.GetPluginFromToken(token);
if (plugin != null){
WithConfigureFunction.Add(plugin);
}
}
}
}

View File

@@ -1,34 +0,0 @@
using System;
using CefSharp;
using TweetDuck.Core.Adapters;
using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Plugins{
sealed class PluginDispatcher : IPluginDispatcher{
public event EventHandler<PluginDispatchEventArgs> Ready;
private readonly IWebBrowser browser;
private readonly IScriptExecutor executor;
public PluginDispatcher(IWebBrowser browser){
this.browser = browser;
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.executor = new CefScriptExecutor(browser);
}
void IPluginDispatcher.AttachBridge(string name, object bridge){
browser.RegisterAsyncJsObject(name, bridge);
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
IFrame frame = e.Frame;
if (frame.IsMain && TwitterUrls.IsTweetDeck(frame.Url)){
Ready?.Invoke(this, new PluginDispatchEventArgs(executor));
}
}
}
}

70
Plugins/PluginLoader.cs Normal file
View File

@@ -0,0 +1,70 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using TweetDuck.Plugins.Enums;
namespace TweetDuck.Plugins{
static class PluginLoader{
private static readonly string[] EndTag = { "[END]" };
public static Plugin FromFolder(string path, PluginGroup group){
string name = Path.GetFileName(path);
if (string.IsNullOrEmpty(name)){
throw new ArgumentException("Could not extract directory name from path: "+path);
}
Plugin.Builder builder = new Plugin.Builder(group, name, path, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name));
foreach(string file in Directory.EnumerateFiles(path, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName)){
builder.AddEnvironment(PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal)));
}
string metaFile = Path.Combine(path, ".meta");
if (!File.Exists(metaFile)){
throw new ArgumentException("Plugin is missing a .meta file");
}
string currentTag = null, currentContents = string.Empty;
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){
SetProperty(builder, currentTag, currentContents);
}
currentTag = line.Substring(1, line.Length-2).ToUpper();
currentContents = string.Empty;
if (line.Equals(EndTag[0])){
break;
}
}
else if (currentTag != null){
currentContents = currentContents.Length == 0 ? line : currentContents+Environment.NewLine+line;
}
else{
throw new FormatException("Missing metadata tag before value: "+line);
}
}
return builder.BuildAndSetup();
}
private static void SetProperty(Plugin.Builder builder, string tag, string value){
switch(tag){
case "NAME": builder.Name = value; break;
case "DESCRIPTION": builder.Description = value; break;
case "AUTHOR": builder.Author = value; break;
case "VERSION": builder.Version = value; break;
case "WEBSITE": builder.Website = value; break;
case "CONFIGFILE": builder.ConfigFile = value; break;
case "CONFIGDEFAULT": builder.ConfigDefault = value; break;
case "REQUIRES": builder.RequiredVersion = Version.TryParse(value, out Version version) ? version : throw new FormatException("Invalid required minimum version: "+value); break;
default: throw new FormatException("Invalid metadata tag: "+tag);
}
}
}
}

183
Plugins/PluginManager.cs Normal file
View File

@@ -0,0 +1,183 @@
using CefSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
namespace TweetDuck.Plugins{
sealed class PluginManager{
private static readonly IReadOnlyDictionary<PluginEnvironment, string> PluginSetupScriptNames = PluginEnvironmentExtensions.Map(null, "plugins.browser.js", "plugins.notification.js");
public string PathOfficialPlugins => Path.Combine(rootPath, "official");
public string PathCustomPlugins => Path.Combine(rootPath, "user");
public IEnumerable<Plugin> Plugins => plugins;
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
public IPluginConfig Config { get; }
public event EventHandler<PluginErrorEventArgs> Reloaded;
public event EventHandler<PluginErrorEventArgs> Executed;
private readonly string rootPath;
private readonly PluginBridge bridge;
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
private readonly Random rand = new Random();
private IWebBrowser mainBrowser;
public PluginManager(IPluginConfig config, string rootPath){
this.Config = config;
this.Config.PluginChangedState += Config_PluginChangedState;
this.rootPath = rootPath;
this.bridge = new PluginBridge(this);
}
public void Register(IWebBrowser browser, PluginEnvironment environment, Control sync, bool asMainBrowser = false){
browser.FrameLoadEnd += (sender, args) => {
IFrame frame = args.Frame;
if (frame.IsMain && TwitterUtils.IsTweetDeckWebsite(frame)){
ExecutePlugins(frame, environment, sync);
}
};
browser.RegisterAsyncJsObject("$TDP", bridge);
if (asMainBrowser){
mainBrowser = browser;
}
}
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e){
mainBrowser?.ExecuteScriptAsync("TDPF_setPluginState", e.Plugin, e.IsEnabled);
}
public bool IsPluginInstalled(string identifier){
return plugins.Any(plugin => plugin.Identifier.Equals(identifier));
}
public bool HasAnyPlugin(PluginEnvironment environment){
return plugins.Any(plugin => plugin.Environments.HasFlag(environment));
}
public bool IsPluginConfigurable(Plugin plugin){
return plugin.HasConfig || bridge.WithConfigureFunction.Contains(plugin);
}
public void ConfigurePlugin(Plugin plugin){
if (bridge.WithConfigureFunction.Contains(plugin)){
mainBrowser?.ExecuteScriptAsync("TDPF_configurePlugin", plugin);
}
else if (plugin.HasConfig){
if (File.Exists(plugin.ConfigPath)){
using(Process.Start("explorer.exe", "/select,\""+plugin.ConfigPath.Replace('/', '\\')+"\"")){}
}
else{
using(Process.Start("explorer.exe", '"'+plugin.GetPluginFolder(PluginFolder.Data).Replace('/', '\\')+'"')){}
}
}
}
public int GetTokenFromPlugin(Plugin plugin){
foreach(KeyValuePair<int, Plugin> kvp in tokens){
if (kvp.Value.Equals(plugin)){
return kvp.Key;
}
}
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){
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
}
public void Reload(){
plugins.Clear();
tokens.Clear();
List<string> loadErrors = new List<string>(2);
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 = PluginLoader.FromFolder(fullDir, group);
}catch(Exception e){
loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+e.Message);
continue;
}
yield return plugin;
}
}
plugins.UnionWith(LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official));
plugins.UnionWith(LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom));
Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
}
private void ExecutePlugins(IFrame frame, PluginEnvironment environment, Control sync){
if (!HasAnyPlugin(environment) || !ScriptLoader.ExecuteFile(frame, PluginSetupScriptNames[environment], sync)){
return;
}
bool includeDisabled = environment.IncludesDisabledPlugins();
if (includeDisabled){
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GenerateConfig(Config), "gen:pluginconfig");
}
List<string> failedPlugins = new List<string>(1);
foreach(Plugin plugin in Plugins){
string path = plugin.GetScriptPath(environment);
if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun){
continue;
}
string script;
try{
script = File.ReadAllText(path);
}catch(Exception e){
failedPlugins.Add(plugin.Identifier+" ("+Path.GetFileName(path)+"): "+e.Message);
continue;
}
ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, GetTokenFromPlugin(plugin), environment), "plugin:"+plugin);
}
Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
}
}
}

View File

@@ -1,11 +1,10 @@
using System.Linq;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Enums;
using TweetDuck.Plugins.Enums;
namespace TweetLib.Core.Features.Plugins{
public static class PluginScriptGenerator{
namespace TweetDuck.Plugins{
static class PluginScriptGenerator{
public static string GenerateConfig(IPluginConfig config){
return "window.TD_PLUGINS.disabled = [" + string.Join(",", config.DisabledPlugins.Select(id => '"' + id + '"')) + "]";
return "window.TD_PLUGINS.disabled = ["+string.Join(",", config.DisabledPlugins.Select(id => $"\"{id}\""))+"]";
}
public static string GeneratePlugin(string pluginIdentifier, string pluginContents, int pluginToken, PluginEnvironment environment){

View File

@@ -2,8 +2,10 @@ using CefSharp;
using CefSharp.WinForms;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core;
@@ -12,20 +14,15 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other;
using TweetDuck.Core.Management;
using TweetDuck.Core.Utils;
using TweetDuck.Impl;
using TweetDuck.Resources;
using TweetLib.Core;
using TweetLib.Core.Application.Helpers;
using TweetLib.Core.Collections;
using TweetLib.Core.Utils;
using TweetDuck.Data;
namespace TweetDuck{
static class Program{
public const string BrandName = Lib.BrandName;
public const string VersionTag = Lib.VersionTag;
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.17.4";
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
@@ -51,28 +48,23 @@ namespace TweetDuck{
private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
private static bool HasCleanedUp;
public static CultureInfo Culture { get; }
public static Reporter Reporter { get; }
public static ConfigManager Config { get; }
public static ScriptLoader Resources { get; }
static Program(){
Culture = CultureInfo.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
#if DEBUG
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
#endif
Reporter = new Reporter(ErrorLogFilePath);
Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :(");
Config = new ConfigManager();
#if DEBUG
Resources = new ScriptLoaderDebug();
#else
Resources = new ScriptLoader();
#endif
Lib.Initialize(new App.Builder{
ErrorHandler = Reporter,
LockHandler = new LockHandler(),
SystemHandler = new SystemHandler(),
ResourceHandler = Resources
});
}
[STAThread]
@@ -83,7 +75,7 @@ namespace TweetDuck{
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
if (!FileUtils.CheckFolderWritePermission(StoragePath)){
if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
return;
}
@@ -107,17 +99,15 @@ namespace TweetDuck{
LockManager.Result lockResult = LockManager.Lock();
if (lockResult == LockManager.Result.HasProcess){
if (!LockManager.RestoreLockingProcess() && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){
if (!LockManager.CloseLockingProcess()){
if (!LockManager.RestoreLockingProcess(2000) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){
if (!LockManager.CloseLockingProcess(10000, 5000)){
FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
return;
}
lockResult = LockManager.Lock();
}
else{
return;
}
else return;
}
if (lockResult != LockManager.Result.Success){
@@ -141,7 +131,7 @@ namespace TweetDuck{
}
try{
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze, null));
}catch(Exception e){
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: "+e.Message, FormMessage.OK);
return;
@@ -171,7 +161,6 @@ namespace TweetDuck{
Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser();
Resources.Initialize(mainForm);
Application.Run(mainForm);
if (mainForm.UpdateInstallerPath != null){
@@ -179,7 +168,7 @@ namespace TweetDuck{
// ProgramPath has a trailing backslash
string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !FileUtils.CheckFolderWritePermission(ProgramPath);
bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath);
if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
Application.Exit();
@@ -191,7 +180,7 @@ namespace TweetDuck{
}
private static string GetDataStoragePath(){
string custom = Arguments.GetValue(Arguments.ArgDataFolder);
string custom = Arguments.GetValue(Arguments.ArgDataFolder, null);
if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))){
if (Path.GetInvalidPathChars().Any(custom.Contains)){

View File

@@ -120,6 +120,26 @@ namespace TweetDuck.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon overlay_muted {
get {
object obj = ResourceManager.GetObject("overlay_muted", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon overlay_notification {
get {
object obj = ResourceManager.GetObject("overlay_notification", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>

View File

@@ -136,6 +136,12 @@
<data name="icon_tray_new" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Images\icon-tray-new.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="overlay_muted" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Images\overlay-muted.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="overlay_notification" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Images\overlay-notification.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="spinner" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Images\spinner.apng;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>

View File

@@ -1,28 +1,30 @@
# Support
[Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) &nbsp;|&nbsp; [Support via Ko-fi](https://ko-fi.com/chylex) &nbsp;|&nbsp; [Support via Patreon](https://www.patreon.com/chylex)
[Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) &nbsp;|&nbsp; [Support via PayPal](https://paypal.me/chylex) &nbsp;|&nbsp; [Support via Patreon](https://www.patreon.com/chylex)
# Build Instructions
### Setup
The program can be built using Visual Studio 2019. Before opening the solution, please make sure you have the following workloads and components installed (optional components that are not listed can be deselected to save space):
The program can be built using Visual Studio 2017 or newer. Before opening the solution, please make sure you have the following workloads and components installed (optional components that are not listed can be deselected to save space):
* **.NET desktop development**
* .NET Framework 4.7.2 SDK
* .NET Framework 4 4.6 development tools
* F# desktop language support
* **Desktop development with C++**
* MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.20)
* *(VS 2017)* VC++ 2017 version 15.9 v14.16 latest v141 tools
* *(VS 2019)* MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.20)
After opening the solution, right-click the solution and select **Restore NuGet Packages**, or manually run this command in the **Package Manager Console**:
```
PM> Install-Package CefSharp.WinForms -Version 67.0.0
PM> Install-Package WindowsAPICodePack-Shell -Version 1.1.1
```
### Debug
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads.
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads. An F# compiler must be present when building the project to enable this feature: `C:\Program Files (x86)\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe`
### Release
@@ -42,13 +44,18 @@ If you decide to publicly release a custom version, please make it clear that it
- 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
#### Error: The "EmbedAllSources" parameter is not supported by the "Csc" task
1. Open `C:\Program Files (x86)\Visual Studio\2017\<edition>\MSBuild\15.0\Bin\Roslyn\Microsoft.CSharp.Core.targets` in a text editor
2. Remove line that says `EmbedAllSources="$(EmbedAllSources)"`
3. Hope the next Visual Studio update fixes it...
### Installers
TweetDuck uses **Inno Setup** for installers and updates. First, download and install [InnoSetup 5.6.1](http://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) (with Preprocessor support) and the [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list).
TweetDuck uses **Inno Setup** for installers and updates. 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 and Visual Studio for the change to take place.
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 by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio first!
Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio!
After the window closes, three installers will be generated inside the `bld/Output` folder:
* **TweetDuck.exe**

View File

@@ -6,11 +6,9 @@ using System.Text;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using TweetLib.Core;
using TweetLib.Core.Application;
namespace TweetDuck{
sealed class Reporter : IAppErrorHandler{
sealed class Reporter{
private readonly string logFile;
public Reporter(string logFile){
@@ -30,12 +28,8 @@ namespace TweetDuck{
}
public bool LogImportant(string data){
return ((IAppErrorHandler)this).Log(data);
}
bool IAppErrorHandler.Log(string text){
#if DEBUG
Debug.WriteLine(text);
Debug.WriteLine(data);
#endif
StringBuilder build = new StringBuilder();
@@ -44,8 +38,8 @@ namespace TweetDuck{
build.Append("Please, report all issues to: https://github.com/chylex/TweetDuck/issues\r\n\r\n");
}
build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("]\r\n");
build.Append(text).Append("\r\n\r\n");
build.Append("[").Append(DateTime.Now.ToString("G", Program.Culture)).Append("]\r\n");
build.Append(data).Append("\r\n\r\n");
try{
File.AppendAllText(logFile, build.ToString(), Encoding.UTF8);

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -8,7 +8,6 @@ enabled(){
_theme: "light",
themeOverride: false,
columnWidth: "310px",
composerWidth: "default",
fontSize: "12px",
hideTweetActions: true,
moveTweetActionsToRight: true,
@@ -381,15 +380,8 @@ 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(`html[data-td-font] { font-size: ${this.config.fontSize} !important }`);
this.css.insert(`.avatar { border-radius: ${this.config.avatarRadius}% !important }`);
if (this.config.composerWidth !== "default"){
const width = this.config.composerWidth;
this.css.insert(`.js-app-content.is-open { margin-right: ${width} !important; transform: translateX(${width}) !important }`);
this.css.insert(`#tduck .js-app-content.tduck-is-opening { margin-right: 0 !important }`);
this.css.insert(`.js-drawer { width: ${width} !important; left: -${width} !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 currentTheme = TD.settings.getTheme();
@@ -578,7 +570,7 @@ ${iconData.map(entry => `#tduck .icon-${entry[0]}:before{content:\"\\f0${entry[1
let cols = this.config.columnWidth.slice(1);
this.css.insert(".column { width: calc((100vw - 205px) / "+cols+" - 6px) !important; min-width: 160px }");
this.css.insert(".is-condensed .column { width: calc((100vw - 65px) / "+cols+" - 6px) !important }");
this.css.insert(".is-condensed .column { width: calc((100vw - 55px) / "+cols+" - 6px) !important }");
}
else{
this.css.insert(".column { width: "+this.config.columnWidth+" !important }");
@@ -646,13 +638,6 @@ ${notificationScrollbarColor ? `
$(".js-dropdown.pos-r").toggleClass("pos-r pos-l");
}
};
this.uiDrawerActiveEvent = (e, data) => {
return if data.activeDrawer === null || this.config.composerWidth === "default";
const ele = $(".js-app-content").addClass("tduck-is-opening");
setTimeout(() => ele.removeClass("tduck-is-opening"), 250);
};
}
ready(){
@@ -662,7 +647,6 @@ ready(){
// layout events
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
$(document).on("uiDrawerActive", this.uiDrawerActiveEvent);
// modal
$("[data-action='settings-menu']").on("click", this.onSettingsMenuClickedEvent);
@@ -725,12 +709,10 @@ disabled(){
window.clearTimeout(this.optimizationTimer);
}
$(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent);
$(window).off("focus", this.onWindowFocusEvent);
$(window).off("blur", this.onWindowBlurEvent);
$(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent);
$(document).off("uiDrawerActive", this.uiDrawerActiveEvent);
TD.components.GlobalSettings.prototype.getInfo = this.prevFuncSettingsGetInfo;
TD.components.GlobalSettings.prototype.switchTab = this.prevFuncSettingsSwitchTab;

View File

@@ -55,20 +55,6 @@
<option disabled></option>
</select>
<!-- COMPOSER SIZE -->
<label class="txt-uppercase touch-larger-label">
<b>New Tweet panel</b>
</label>
<select data-td-key="composerWidth">
<option value="default">Default</option>
<option value="270px">Narrow (270px)</option>
<option value="310px">Medium (310px)</option>
<option value="350px">Wide (350px)</option>
<option value="custom-px">Custom</option>
<option value="change-custom-px">Change custom value...</option>
</select>
<!-- FONT SIZE -->
<label class="txt-uppercase touch-larger-label">
@@ -83,6 +69,10 @@
<option value="custom-px">Custom</option>
<option value="change-custom-px">Change custom value...</option>
</select>
<label class="checkbox">
<input data-td-key="forceArialFont" class="js-theme-checkbox touch-larger-label" type="checkbox">
Use Arial as default font
</label>
<label class="checkbox">
<input data-td-key="increaseQuoteTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox">
Increase quoted tweet font size
@@ -126,10 +116,6 @@
<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="forceArialFont" class="js-theme-checkbox touch-larger-label" type="checkbox">
Use Arial as default font
</label>
<!-- ADVANCED -->
@@ -186,7 +172,7 @@
#edit-design-panel {
width: 693px;
height: 424px;
height: 380px;
background-color: #FFF;
box-shadow: 0 0 10px rgba(17, 17, 17, 0.5);
}
@@ -231,7 +217,7 @@
#edit-design-panel-content label.radio {
display: inline-block;
margin: 0 16px 0 4px;
margin: 0 16px 5px 4px;
cursor: pointer;
}

View File

@@ -506,6 +506,18 @@ html.dark .lst-group .selected a:hover{background:#55acee}
html.dark .lst-group .selected .fullname,html.dark .lst-group .selected .inner strong,html.dark .lst-group .selected .list-link,html.dark .lst-group .selected .list-twitter-list,html.dark .lst-group .selected .list-subtitle,html.dark .lst-group .selected .list-account,html.dark .lst-group .selected .list-listmember,html.dark .lst-group .selected .txt-ellipsis{color:#F5F8FA}
html.dark .lst-group .selected .username,html.dark .lst-group .selected .bytext,html.dark .lst-group .selected .subtitle,html.dark .lst-group .selected .icon-protected{color:#eef3f7}
html.dark .itm-remove{border-top:1px solid #ddd}
html.dark .caret-outer{border-bottom:7px solid rgba(17,17,17,0.1)}
html.dark .caret-inner{border-bottom:6px solid #fff}
html.dark .drp-h-divider{border-bottom:1px solid #ddd}
html.dark .dropdown-menu .typeahead-item,html.dark .dropdown-menu [data-action]{color:#292F33}
html.dark .dropdown-menu .is-selected{background:#55acee;color:#fff}
html.dark .dropdown-menu .is-selected [data-action]{color:#fff}
html.dark .dropdown-menu .is-selected a:not(:hover):not(:focus){color:#fff}
html.dark .dropdown-menu a:not(:hover):not(:focus){color:#292F33}
html.dark .dropdown-menu-old li:hover{background:#55acee}
html.dark .dropdown-menu-old li:hover a{color:#fff}
html.dark .dropdown-menu-old li:hover .attribution{color:#fff}
html.dark .non-selectable-item{color:#292F33}
html.dark .update-available-item:before{background-color:#FFAD1F}
html.dark .is-selected .update-available-item:before{background-color:rgba(41,47,51,0.2)}
html.dark .popover{background-color:#fff;box-shadow:0 0 10px rgba(17,17,17,0.7)}

View File

@@ -380,9 +380,13 @@ enabled(){
hideKeyboard();
};
this.composerActiveEvent = function(e){
this.drawerToggleEvent = function(e, data){
if (data.activeDrawer === "compose"){
setTimeout(function(){
$(".emoji-keyboard-popup-btn", me.composeDrawer).on("click", me.emojiKeyboardButtonClickEvent);
$(".js-docked-compose .js-compose-scroller > .scroll-v", me.composeDrawer).on("scroll", me.composerScrollEvent);
}, 0);
}
};
this.documentClickEvent = function(e){
@@ -410,7 +414,7 @@ enabled(){
if (maybeDockedComposePanel.length){
maybeDockedComposePanel.find(".cf.margin-t--12.margin-b--30").first().append(buttonHTML);
this.composerActiveEvent();
this.drawerToggleEvent({}, { activeDrawer: "compose" });
}
}
@@ -420,7 +424,7 @@ ready(){
$(document).on("click", this.documentClickEvent);
$(document).on("keydown", this.documentKeyEvent);
$(document).on("tduckOldComposerActive", this.composerActiveEvent);
$(document).on("uiDrawerActive", this.drawerToggleEvent);
$(document).on("uiComposeImageAdded", this.uploadFilesEvent);
this.composeDrawer.on("uiComposeTweetSending", this.composerSendingEvent);
@@ -546,7 +550,7 @@ disabled(){
$(document).off("click", this.documentClickEvent);
$(document).off("keydown", this.documentKeyEvent);
$(document).off("tduckOldComposerActive", this.composerActiveEvent);
$(document).off("uiDrawerActive", this.drawerToggleEvent);
$(document).off("uiComposeImageAdded", this.uploadFilesEvent);
this.composeDrawer.off("uiComposeTweetSending", this.composerSendingEvent);

View File

@@ -376,7 +376,7 @@ enabled(){
};
this.drawerToggleEvent = function(e, data){
if (typeof data === "undefined" || data.activeDrawer !== "compose"){
if (data.activeDrawer === null){
hideTemplateModal();
}
};
@@ -385,7 +385,6 @@ enabled(){
ready(){
$(".js-drawer[data-drawer='compose']").on("click", ".manage-templates-btn", this.manageTemplatesButtonClickEvent);
$(document).on("uiDrawerActive", this.drawerToggleEvent);
$(document).on("click", ".js-new-composer-opt-in", this.drawerToggleEvent);
}
disabled(){
@@ -394,7 +393,6 @@ disabled(){
$(".js-drawer[data-drawer='compose']").off("click", ".manage-templates-btn", this.manageTemplatesButtonClickEvent);
$(document).off("uiDrawerActive", this.drawerToggleEvent);
$(document).off("click", ".js-new-composer-opt-in", this.drawerToggleEvent);
TD.mustaches["compose/docked_compose.mustache"] = this.prevComposeMustache;
}

Some files were not shown because too many files have changed in this diff Show More