1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-08-21 03:54:07 +02:00

Update .NET & begin refactoring code into a core lib ()

* Switch to .NET Framework 4.7.2 & C# 8.0, update libraries

* Add TweetLib.Core project targeting .NET Standard 2.0

* Enable reference nullability checks for TweetLib.Core

* Move a bunch of utility classes into TweetLib.Core & refactor

* Partially move TweetDuck plugin & update system to TweetLib.Core

* Move some constants and CultureInfo setup to TweetLib.Core

* Move some configuration classes to TweetLib.Core

* Minor refactoring and warning suppression

* Add App to TweetLib.Core

* Add IAppErrorHandler w/ implementation

* Continue moving config, plugin, and update classes to TweetLib.Core

* Fix a few nullability checks

* Update installers to check for .NET Framework 4.7.2
This commit is contained in:
2019-05-26 14:55:12 +02:00
committed by GitHub
parent aca438b837
commit 1ccefe853a
106 changed files with 926 additions and 794 deletions
Configuration
Core
Data
Plugins
Program.cs
Properties
Reporter.csTweetDuck.csprojTweetDuck.sln
Updates
bld
lib
packages.config
subprocess
video

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

@@ -1,13 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using TweetDuck.Configuration.Instance;
using TweetDuck.Core.Utils;
using TweetDuck.Data; using TweetDuck.Data;
using TweetDuck.Data.Serialization; using TweetLib.Core.Features.Configuration;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
namespace TweetDuck.Configuration{ namespace TweetDuck.Configuration{
sealed class ConfigManager{ sealed class ConfigManager : IConfigManager{
public UserConfig User { get; } public UserConfig User { get; }
public SystemConfig System { get; } public SystemConfig System { get; }
public PluginConfig Plugins { get; } public PluginConfig Plugins { get; }
@@ -16,7 +16,7 @@ namespace TweetDuck.Configuration{
private readonly FileConfigInstance<UserConfig> infoUser; private readonly FileConfigInstance<UserConfig> infoUser;
private readonly FileConfigInstance<SystemConfig> infoSystem; private readonly FileConfigInstance<SystemConfig> infoSystem;
private readonly PluginConfigInstance infoPlugins; private readonly PluginConfigInstance<PluginConfig> infoPlugins;
private readonly IConfigInstance<BaseConfig>[] infoList; private readonly IConfigInstance<BaseConfig>[] infoList;
@@ -28,7 +28,7 @@ namespace TweetDuck.Configuration{
infoList = new IConfigInstance<BaseConfig>[]{ infoList = new IConfigInstance<BaseConfig>[]{
infoUser = new FileConfigInstance<UserConfig>(Program.UserConfigFilePath, User, "program options"), infoUser = new FileConfigInstance<UserConfig>(Program.UserConfigFilePath, User, "program options"),
infoSystem = new FileConfigInstance<SystemConfig>(Program.SystemConfigFilePath, System, "system options"), infoSystem = new FileConfigInstance<SystemConfig>(Program.SystemConfigFilePath, System, "system options"),
infoPlugins = new PluginConfigInstance(Program.PluginConfigFilePath, Plugins) infoPlugins = new PluginConfigInstance<PluginConfig>(Program.PluginConfigFilePath, Plugins)
}; };
// TODO refactor further // TODO refactor further
@@ -70,59 +70,13 @@ namespace TweetDuck.Configuration{
infoPlugins.Reload(); infoPlugins.Reload();
} }
private void TriggerProgramRestartRequested(){ void IConfigManager.TriggerProgramRestartRequested(){
ProgramRestartRequested?.Invoke(this, EventArgs.Empty); ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
} }
private IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance){ IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance){
Type instanceType = instance.GetType(); Type instanceType = instance.GetType();
return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null 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();
}
}
}
} }
} }

@@ -1,69 +0,0 @@
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();
}
}
}

@@ -1,20 +1,41 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using TweetDuck.Plugins; using TweetLib.Core.Features.Configuration;
using TweetDuck.Plugins.Events; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetDuck.Configuration{ namespace TweetDuck.Configuration{
sealed class PluginConfig : ConfigManager.BaseConfig, IPluginConfig{ sealed class PluginConfig : BaseConfig, IPluginConfig{
private static readonly string[] DefaultDisabled = { private static readonly string[] DefaultDisabled = {
"official/clear-columns", "official/clear-columns",
"official/reply-account" "official/reply-account"
}; };
// CONFIGURATION // CONFIGURATION DATA
public IEnumerable<string> DisabledPlugins => disabled; private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
// EVENTS
public event EventHandler<PluginChangedStateEventArgs> PluginChangedState; 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){ public void SetEnabled(Plugin plugin, bool enabled){
if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))){ if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))){
@@ -26,20 +47,5 @@ namespace TweetDuck.Configuration{
public bool IsEnabled(Plugin plugin){ public bool IsEnabled(Plugin plugin){
return !disabled.Contains(plugin.Identifier); 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);
}
} }
} }

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

@@ -5,9 +5,10 @@ using TweetDuck.Core.Notification;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Data; using TweetDuck.Data;
using TweetLib.Core.Features.Configuration;
namespace TweetDuck.Configuration{ namespace TweetDuck.Configuration{
sealed class UserConfig : ConfigManager.BaseConfig{ sealed class UserConfig : BaseConfig{
// CONFIGURATION DATA // CONFIGURATION DATA
@@ -135,9 +136,9 @@ namespace TweetDuck.Configuration{
// END OF CONFIG // END OF CONFIG
public UserConfig(ConfigManager configManager) : base(configManager){} public UserConfig(IConfigManager configManager) : base(configManager){}
protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){ protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
return new UserConfig(configManager); return new UserConfig(configManager);
} }
} }

@@ -1,4 +1,5 @@
using System.Windows.Forms; using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Management; using TweetDuck.Core.Management;
using TweetDuck.Core.Notification; using TweetDuck.Core.Notification;
@@ -6,6 +7,7 @@ using TweetDuck.Core.Other;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Bridge{ namespace TweetDuck.Core.Bridge{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class TweetDeckBridge{ class TweetDeckBridge{
public static string FontSize { get; private set; } public static string FontSize { get; private set; }
public static string NotificationHeadLayout { get; private set; } public static string NotificationHeadLayout { get; private set; }
@@ -63,7 +65,7 @@ namespace TweetDuck.Core.Bridge{
} }
// Notification only // Notification only
public sealed class Notification : TweetDeckBridge{ public sealed class Notification : TweetDeckBridge{
public Notification(FormBrowser form, FormNotificationMain notification) : base(form, notification){} public Notification(FormBrowser form, FormNotificationMain notification) : base(form, notification){}

@@ -1,9 +1,11 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Updates; using TweetLib.Core.Features.Updates;
namespace TweetDuck.Core.Bridge{ namespace TweetDuck.Core.Bridge{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class UpdateBridge{ class UpdateBridge{
private readonly UpdateHandler updates; private readonly UpdateHandler updates;
private readonly Control sync; private readonly Control sync;

@@ -1,5 +1,6 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Core.Bridge; using TweetDuck.Core.Bridge;
@@ -14,9 +15,10 @@ using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Other.Settings.Dialogs; using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetDuck.Updates; using TweetDuck.Updates;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Updates;
namespace TweetDuck.Core{ namespace TweetDuck.Core{
sealed partial class FormBrowser : Form, AnalyticsFile.IProvider{ sealed partial class FormBrowser : Form, AnalyticsFile.IProvider{
@@ -71,7 +73,7 @@ namespace TweetDuck.Core{
this.notification = new FormNotificationTweet(this, plugins); this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show(); this.notification.Show();
this.updates = new UpdateHandler(Program.InstallerPath); this.updates = new UpdateHandler(new UpdateCheckClient(Program.InstallerPath), TaskScheduler.FromCurrentSynchronizationContext());
this.updates.CheckFinished += updates_CheckFinished; this.updates.CheckFinished += updates_CheckFinished;
this.updateBridge = new UpdateBridge(updates, this); this.updateBridge = new UpdateBridge(updates, this);

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

@@ -12,6 +12,7 @@ using TweetDuck.Core.Notification;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics; using TweetDuck.Core.Other.Analytics;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{ namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{ abstract class ContextMenuBase : IContextMenuHandler{

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

@@ -3,9 +3,10 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Data;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums; using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Core.Management{ namespace TweetDuck.Core.Management{
sealed class ProfileManager{ sealed class ProfileManager{

@@ -6,10 +6,10 @@ using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling; using TweetDuck.Core.Handling;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Core.Notification{ namespace TweetDuck.Core.Notification{
abstract partial class FormNotificationMain : FormNotificationBase{ abstract partial class FormNotificationMain : FormNotificationBase{

@@ -6,9 +6,9 @@ using CefSharp;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Data;
namespace TweetDuck.Core.Notification.Screenshot{ namespace TweetDuck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotificationBase{ sealed class FormNotificationScreenshotable : FormNotificationBase{

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

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

@@ -9,6 +9,8 @@ using System.Timers;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetLib.Core;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Analytics{ namespace TweetDuck.Core.Other.Analytics{
sealed class AnalyticsManager : IDisposable{ sealed class AnalyticsManager : IDisposable{
@@ -20,7 +22,7 @@ namespace TweetDuck.Core.Other.Analytics{
#else #else
"https://tweetduck.chylex.com/breadcrumb/report" "https://tweetduck.chylex.com/breadcrumb/report"
#endif #endif
); );
public AnalyticsFile File { get; } public AnalyticsFile File { get; }
@@ -80,7 +82,7 @@ namespace TweetDuck.Core.Other.Analytics{
private void SetLastDataCollectionTime(DateTime dt, string message = null){ 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.LastDataCollection = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
File.LastCollectionVersion = Program.VersionTag; File.LastCollectionVersion = Program.VersionTag;
File.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture); File.LastCollectionMessage = message ?? dt.ToString("g", Lib.Culture);
File.Save(); File.Save();
RestartTimer(); RestartTimer();
@@ -117,7 +119,7 @@ namespace TweetDuck.Core.Other.Analytics{
System.Diagnostics.Debugger.Break(); System.Diagnostics.Debugger.Break();
#endif #endif
BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection()); WebUtils.NewClient(BrowserUtils.UserAgentVanilla).UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
}).ContinueWith(task => browser.InvokeAsyncSafe(() => { }).ContinueWith(task => browser.InvokeAsyncSafe(() => {
if (task.Status == TaskStatus.RanToCompletion){ if (task.Status == TaskStatus.RanToCompletion){
SetLastDataCollectionTime(DateTime.Now); SetLastDataCollectionTime(DateTime.Now);

@@ -11,7 +11,10 @@ using System.Text.RegularExpressions;
using TweetDuck.Core.Notification; using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums; using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Analytics{ namespace TweetDuck.Core.Other.Analytics{
static class AnalyticsReportGenerator{ static class AnalyticsReportGenerator{
@@ -27,7 +30,7 @@ namespace TweetDuck.Core.Other.Analytics{
{ "System Edition" , SystemEdition }, { "System Edition" , SystemEdition },
{ "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" }, { "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" },
{ "System Build" , SystemBuild }, { "System Build" , SystemBuild },
{ "System Locale" , Program.Culture.Name.ToLower() }, { "System Locale" , Lib.Culture.Name.ToLower() },
0, 0,
{ "RAM" , Exact(RamSize) }, { "RAM" , Exact(RamSize) },
{ "GPU" , GpuVendor }, { "GPU" , GpuVendor },

@@ -6,6 +6,7 @@ using System.Windows.Forms;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Plugins.Controls; using TweetDuck.Plugins.Controls;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Core.Other{ namespace TweetDuck.Core.Other{
sealed partial class FormPlugins : Form, FormManager.IAppDialog{ sealed partial class FormPlugins : Form, FormManager.IAppDialog{

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

@@ -5,8 +5,6 @@ using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Other.Settings.Dialogs{ namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsAnalytics : Form{ sealed partial class DialogSettingsAnalytics : Form{
public string CefArgs => textBoxReport.Text;
public DialogSettingsAnalytics(AnalyticsReport report){ public DialogSettingsAnalytics(AnalyticsReport report){
InitializeComponent(); InitializeComponent();

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

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

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

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

@@ -12,8 +12,8 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Notification; using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins; using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Core{ namespace TweetDuck.Core{
sealed class TweetDeckBrowser : IDisposable{ sealed class TweetDeckBrowser : IDisposable{

@@ -3,16 +3,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net;
using System.Windows.Forms; using System.Windows.Forms;
using CefSharp.WinForms; using CefSharp.WinForms;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Utils{ namespace TweetDuck.Core.Utils{
static class BrowserUtils{ static class BrowserUtils{
public static string UserAgentVanilla => Program.BrandName+" "+Application.ProductVersion; public static string UserAgentVanilla => Program.BrandName + " " + Application.ProductVersion;
public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"+Cef.ChromiumVersion+" Safari/537.36"; public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + Cef.ChromiumVersion + " Safari/537.36";
public static readonly bool HasDevTools = File.Exists(Path.Combine(Program.ProgramPath, "devtools_resources.pak")); public static readonly bool HasDevTools = File.Exists(Path.Combine(Program.ProgramPath, "devtools_resources.pak"));
@@ -74,29 +74,11 @@ namespace TweetDuck.Core.Utils{
}; };
} }
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){ public static void OpenExternalBrowser(string url){
if (string.IsNullOrWhiteSpace(url))return; if (string.IsNullOrWhiteSpace(url))return;
switch(CheckUrl(url)){ switch(UrlUtils.Check(url)){
case UrlCheckResult.Fine: case UrlUtils.CheckResult.Fine:
if (FormGuide.CheckGuideUrl(url, out string hash)){ if (FormGuide.CheckGuideUrl(url, out string hash)){
FormGuide.Show(hash); FormGuide.Show(hash);
} }
@@ -117,9 +99,9 @@ namespace TweetDuck.Core.Utils{
break; break;
case UrlCheckResult.Tracking: case UrlUtils.CheckResult.Tracking:
if (Config.IgnoreTrackingUrlWarning){ if (Config.IgnoreTrackingUrlWarning){
goto case UrlCheckResult.Fine; goto case UrlUtils.CheckResult.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)){ 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)){
@@ -135,13 +117,13 @@ namespace TweetDuck.Core.Utils{
} }
if (result == DialogResult.Ignore || result == DialogResult.Yes){ if (result == DialogResult.Ignore || result == DialogResult.Yes){
goto case UrlCheckResult.Fine; goto case UrlUtils.CheckResult.Fine;
} }
} }
break; break;
case UrlCheckResult.Invalid: case UrlUtils.CheckResult.Invalid:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK); FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
break; break;
} }
@@ -174,50 +156,10 @@ 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){ public static string GetErrorName(CefErrorCode code){
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty); 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){ public static int Scale(int baseValue, double scaleFactor){
return (int)Math.Round(baseValue*scaleFactor); return (int)Math.Round(baseValue*scaleFactor);
} }

@@ -8,7 +8,9 @@ using TweetDuck.Core.Management;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Data; using TweetDuck.Data;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using TweetLib.Core.Utils;
using Cookie = CefSharp.Cookie; using Cookie = CefSharp.Cookie;
namespace TweetDuck.Core.Utils{ namespace TweetDuck.Core.Utils{
@@ -71,7 +73,7 @@ namespace TweetDuck.Core.Utils{
} }
public static string GetImageFileName(string url){ public static string GetImageFileName(string url){
return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url)); return UrlUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
} }
public static void ViewImage(string url, ImageQuality quality){ public static void ViewImage(string url, ImageQuality quality){
@@ -88,7 +90,7 @@ namespace TweetDuck.Core.Utils{
string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName()); string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
if (WindowsUtils.FileExistsAndNotEmpty(file)){ if (FileUtils.FileExistsAndNotEmpty(file)){
ViewImageInternal(file); ViewImageInternal(file);
} }
else{ else{
@@ -143,7 +145,7 @@ namespace TweetDuck.Core.Utils{
} }
public static void DownloadVideo(string url, string username){ public static void DownloadVideo(string url, string username){
string filename = BrowserUtils.GetFileNameFromUrl(url); string filename = UrlUtils.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename); string ext = Path.GetExtension(filename);
using(SaveFileDialog dialog = new SaveFileDialog{ using(SaveFileDialog dialog = new SaveFileDialog{
@@ -178,7 +180,10 @@ namespace TweetDuck.Core.Utils{
} }
} }
BrowserUtils.DownloadFileAsync(url, target, cookieStr, onSuccess, onFailure); WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentChrome);
client.Headers[HttpRequestHeader.Cookie] = cookieStr;
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(target, onSuccess, onFailure);
client.DownloadFileAsync(new Uri(url), target);
}, scheduler); }, scheduler);
} }
} }

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
@@ -16,7 +15,6 @@ namespace TweetDuck.Core.Utils{
private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false); private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false);
private static readonly bool IsWindows8OrNewer; private static readonly bool IsWindows8OrNewer;
private static bool HasMicrosoftBeenBroughtTo2008Yet;
public static int CurrentProcessID { get; } public static int CurrentProcessID { get; }
public static bool ShouldAvoidToolWindow { get; } public static bool ShouldAvoidToolWindow { get; }
@@ -32,47 +30,6 @@ namespace TweetDuck.Core.Utils{
ShouldAvoidToolWindow = IsWindows8OrNewer; 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){ public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
try{ try{

@@ -1,8 +0,0 @@
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);
}
}

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

@@ -3,7 +3,8 @@ using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Plugins.Enums; using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Plugins.Controls{ namespace TweetDuck.Plugins.Controls{
sealed partial class PluginControl : UserControl{ sealed partial class PluginControl : UserControl{

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

@@ -1,13 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Text; using System.Text;
using TweetDuck.Core.Utils; using TweetLib.Core.Collections;
using TweetDuck.Data; using TweetLib.Core.Data;
using TweetDuck.Plugins.Enums; using TweetLib.Core.Features.Plugins;
using TweetDuck.Plugins.Events; using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Utils;
namespace TweetDuck.Plugins{ namespace TweetDuck.Plugins{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
sealed class PluginBridge{ sealed class PluginBridge{
private static string SanitizeCacheKey(string key){ private static string SanitizeCacheKey(string key){
return key.Replace('\\', '/').Trim(); return key.Replace('\\', '/').Trim();
@@ -80,7 +84,7 @@ namespace TweetDuck.Plugins{
public void WriteFile(int token, string path, string contents){ public void WriteFile(int token, string path, string contents){
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path); string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
WindowsUtils.CreateDirectoryForFile(fullPath); FileUtils.CreateDirectoryForFile(fullPath);
File.WriteAllText(fullPath, contents, Encoding.UTF8); File.WriteAllText(fullPath, contents, Encoding.UTF8);
fileCache[token, SanitizeCacheKey(path)] = contents; fileCache[token, SanitizeCacheKey(path)] = contents;
} }

@@ -6,10 +6,12 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources; using TweetDuck.Resources;
using TweetLib.Core.Data;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetDuck.Plugins{ namespace TweetDuck.Plugins{
sealed class PluginManager{ sealed class PluginManager{
@@ -126,12 +128,19 @@ namespace TweetDuck.Plugins{
} }
foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){ foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
string name = Path.GetFileName(fullDir);
if (string.IsNullOrEmpty(name)){
loadErrors.Add($"{group.GetIdentifierPrefix()}(?): Could not extract directory name from path: {fullDir}");
continue;
}
Plugin plugin; Plugin plugin;
try{ try{
plugin = PluginLoader.FromFolder(fullDir, group); plugin = PluginLoader.FromFolder(name, fullDir, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name), group);
}catch(Exception e){ }catch(Exception e){
loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+e.Message); loadErrors.Add($"{group.GetIdentifierPrefix()}{name}: {e.Message}");
continue; continue;
} }

@@ -2,10 +2,8 @@ using CefSharp;
using CefSharp.WinForms; using CefSharp.WinForms;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using TweetDuck.Configuration; using TweetDuck.Configuration;
using TweetDuck.Core; using TweetDuck.Core;
@@ -14,14 +12,16 @@ using TweetDuck.Core.Handling.General;
using TweetDuck.Core.Other; using TweetDuck.Core.Other;
using TweetDuck.Core.Management; using TweetDuck.Core.Management;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetDuck.Data; using TweetLib.Core;
using TweetLib.Core.Collections;
using TweetLib.Core.Utils;
namespace TweetDuck{ namespace TweetDuck{
static class Program{ static class Program{
public const string BrandName = "TweetDuck"; public const string BrandName = Lib.BrandName;
public const string Website = "https://tweetduck.chylex.com"; public const string VersionTag = Lib.VersionTag;
public const string VersionTag = "1.17.4"; public const string Website = "https://tweetduck.chylex.com";
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory; public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable")); public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
@@ -48,23 +48,18 @@ namespace TweetDuck{
private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock")); private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
private static bool HasCleanedUp; private static bool HasCleanedUp;
public static CultureInfo Culture { get; }
public static Reporter Reporter { get; } public static Reporter Reporter { get; }
public static ConfigManager Config { get; } public static ConfigManager Config { get; }
static Program(){ 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 = new Reporter(ErrorLogFilePath);
Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :("); Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :(");
Config = new ConfigManager(); Config = new ConfigManager();
Lib.Initialize(new App.Builder{
ErrorHandler = Reporter
});
} }
[STAThread] [STAThread]
@@ -75,7 +70,7 @@ namespace TweetDuck{
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore"); WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){ if (!FileUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK); FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
return; return;
} }
@@ -131,7 +126,7 @@ namespace TweetDuck{
} }
try{ try{
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze, null)); RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
}catch(Exception e){ }catch(Exception e){
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: "+e.Message, FormMessage.OK); FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: "+e.Message, FormMessage.OK);
return; return;
@@ -168,7 +163,7 @@ namespace TweetDuck{
// ProgramPath has a trailing backslash // ProgramPath has a trailing backslash
string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : ""); string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath); bool runElevated = !IsPortable || !FileUtils.CheckFolderWritePermission(ProgramPath);
if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){ if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
Application.Exit(); Application.Exit();
@@ -180,7 +175,7 @@ namespace TweetDuck{
} }
private static string GetDataStoragePath(){ private static string GetDataStoragePath(){
string custom = Arguments.GetValue(Arguments.ArgDataFolder, null); string custom = Arguments.GetValue(Arguments.ArgDataFolder);
if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))){ if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))){
if (Path.GetInvalidPathChars().Any(custom.Contains)){ if (Path.GetInvalidPathChars().Any(custom.Contains)){

@@ -19,7 +19,7 @@ namespace TweetDuck.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources { internal class Resources {

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

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" /> <Import Project="packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" /> <Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props')" /> <Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props')" />
<Import Project="packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props" Condition="Exists('packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props')" /> <Import Project="packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props" Condition="Exists('packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props')" />
@@ -14,7 +14,8 @@
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetDuck</RootNamespace> <RootNamespace>TweetDuck</RootNamespace>
<AssemblyName>TweetDuck</AssemblyName> <AssemblyName>TweetDuck</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<UseVSHostingProcess>false</UseVSHostingProcess> <UseVSHostingProcess>false</UseVSHostingProcess>
<ApplicationIcon>Resources\Images\icon.ico</ApplicationIcon> <ApplicationIcon>Resources\Images\icon.ico</ApplicationIcon>
@@ -33,7 +34,6 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
<LangVersion>7</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<OutputPath>bin\x86\Release\</OutputPath> <OutputPath>bin\x86\Release\</OutputPath>
@@ -43,7 +43,6 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
<LangVersion>7</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
@@ -55,10 +54,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Configuration\Arguments.cs" /> <Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\Instance\FileConfigInstance.cs" />
<Compile Include="Configuration\ConfigManager.cs" /> <Compile Include="Configuration\ConfigManager.cs" />
<Compile Include="Configuration\Instance\IConfigInstance.cs" />
<Compile Include="Configuration\Instance\PluginConfigInstance.cs" />
<Compile Include="Configuration\LockManager.cs" /> <Compile Include="Configuration\LockManager.cs" />
<Compile Include="Configuration\SystemConfig.cs" /> <Compile Include="Configuration\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" /> <Compile Include="Configuration\UserConfig.cs" />
@@ -198,10 +194,7 @@
<DependentUpon>TabSettingsFeedback.cs</DependentUpon> <DependentUpon>TabSettingsFeedback.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Core\TweetDeckBrowser.cs" /> <Compile Include="Core\TweetDeckBrowser.cs" />
<Compile Include="Core\Utils\LocaleUtils.cs" />
<Compile Include="Core\Utils\StringUtils.cs" />
<Compile Include="Core\Utils\TwitterUtils.cs" /> <Compile Include="Core\Utils\TwitterUtils.cs" />
<Compile Include="Data\CombinedFileStream.cs" />
<Compile Include="Core\Management\ProfileManager.cs" /> <Compile Include="Core\Management\ProfileManager.cs" />
<Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs"> <Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs">
<SubType>UserControl</SubType> <SubType>UserControl</SubType>
@@ -231,19 +224,11 @@
<DependentUpon>TabSettingsNotifications.cs</DependentUpon> <DependentUpon>TabSettingsNotifications.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Core\Notification\Screenshot\ScreenshotBridge.cs" /> <Compile Include="Core\Notification\Screenshot\ScreenshotBridge.cs" />
<Compile Include="Data\CommandLineArgs.cs" />
<Compile Include="Core\Notification\Screenshot\FormNotificationScreenshotable.cs"> <Compile Include="Core\Notification\Screenshot\FormNotificationScreenshotable.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" /> <Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Data\ResourceLink.cs" /> <Compile Include="Data\ResourceLink.cs" />
<Compile Include="Data\Result.cs" />
<Compile Include="Data\Serialization\FileSerializer.cs" />
<Compile Include="Data\InjectedHTML.cs" />
<Compile Include="Data\Serialization\ITypeConverter.cs" />
<Compile Include="Data\Serialization\SerializationSoftException.cs" />
<Compile Include="Data\Serialization\SingleTypeConverter.cs" />
<Compile Include="Data\TwoKeyDictionary.cs" />
<Compile Include="Data\WindowState.cs" /> <Compile Include="Data\WindowState.cs" />
<Compile Include="Core\Utils\WindowsUtils.cs" /> <Compile Include="Core\Utils\WindowsUtils.cs" />
<Compile Include="Core\Bridge\TweetDeckBridge.cs" /> <Compile Include="Core\Bridge\TweetDeckBridge.cs" />
@@ -262,25 +247,15 @@
<Compile Include="Plugins\Controls\PluginListFlowLayout.cs"> <Compile Include="Plugins\Controls\PluginListFlowLayout.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
<Compile Include="Plugins\Enums\PluginFolder.cs" />
<Compile Include="Plugins\IPluginConfig.cs" />
<Compile Include="Plugins\Plugin.cs" />
<Compile Include="Plugins\Events\PluginChangedStateEventArgs.cs" />
<Compile Include="Plugins\PluginBridge.cs" /> <Compile Include="Plugins\PluginBridge.cs" />
<Compile Include="Configuration\PluginConfig.cs" /> <Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Plugins\Enums\PluginEnvironment.cs" />
<Compile Include="Plugins\Enums\PluginGroup.cs" />
<Compile Include="Plugins\Events\PluginErrorEventArgs.cs" />
<Compile Include="Plugins\PluginLoader.cs" />
<Compile Include="Plugins\PluginManager.cs" /> <Compile Include="Plugins\PluginManager.cs" />
<Compile Include="Plugins\PluginScriptGenerator.cs" />
<Compile Include="Properties\Resources.Designer.cs"> <Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon> <DependentUpon>Resources.resx</DependentUpon>
</Compile> </Compile>
<Compile Include="Reporter.cs" /> <Compile Include="Reporter.cs" />
<Compile Include="Updates\UpdateCheckEventArgs.cs" />
<Compile Include="Updates\FormUpdateDownload.cs"> <Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -297,9 +272,6 @@
<Compile Include="Core\Utils\BrowserUtils.cs" /> <Compile Include="Core\Utils\BrowserUtils.cs" />
<Compile Include="Core\Utils\NativeMethods.cs" /> <Compile Include="Core\Utils\NativeMethods.cs" />
<Compile Include="Updates\UpdateCheckClient.cs" /> <Compile Include="Updates\UpdateCheckClient.cs" />
<Compile Include="Updates\UpdateDownloadStatus.cs" />
<Compile Include="Updates\UpdateHandler.cs" />
<Compile Include="Updates\UpdateInfo.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Resources\ScriptLoader.cs" /> <Compile Include="Resources\ScriptLoader.cs" />
@@ -380,6 +352,10 @@
<None Include="Resources\Scripts\update.js" /> <None Include="Resources\Scripts\update.js" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj">
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
<Name>TweetLib.Core</Name>
</ProjectReference>
<ProjectReference Include="subprocess\TweetDuck.Browser.csproj"> <ProjectReference Include="subprocess\TweetDuck.Browser.csproj">
<Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project> <Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
<Name>TweetDuck.Browser</Name> <Name>TweetDuck.Browser</Name>
@@ -431,7 +407,7 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
<Error Condition="!Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets'))" /> <Error Condition="!Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props'))" /> <Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props'))" />
<Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets'))" /> <Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets'))" />
<Error Condition="!Exists('packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" /> <Error Condition="!Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
</Target> </Target>
<Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" /> <Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" />
<Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" /> <Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" />

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio Version 16
VisualStudioVersion = 15.0.27130.2027 VisualStudioVersion = 16.0.28729.10
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck", "TweetDuck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck", "TweetDuck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}"
EndProject EndProject
@@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetTest.System", "lib\Twe
EndProject EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Unit", "lib\TweetTest.Unit\TweetTest.Unit.fsproj", "{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}" Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Unit", "lib\TweetTest.Unit\TweetTest.Unit.fsproj", "{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86 Debug|x86 = Debug|x86
@@ -42,6 +44,10 @@ Global
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.ActiveCfg = Debug|x86 {EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.ActiveCfg = Debug|x86
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.Build.0 = Debug|x86 {EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.Build.0 = Debug|x86
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Release|x86.ActiveCfg = Debug|x86 {EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Release|x86.ActiveCfg = Debug|x86
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Debug|x86.ActiveCfg = Debug|x86
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Debug|x86.Build.0 = Debug|x86
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Release|x86.ActiveCfg = Release|x86
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Release|x86.Build.0 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

@@ -1,5 +1,6 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using TweetLib.Core.Features.Updates;
namespace TweetDuck.Updates{ namespace TweetDuck.Updates{
sealed partial class FormUpdateDownload : Form{ sealed partial class FormUpdateDownload : Form{

@@ -6,10 +6,12 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web.Script.Serialization; using System.Web.Script.Serialization;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Updates;
using TweetLib.Core.Utils;
using JsonObject = System.Collections.Generic.IDictionary<string, object>; using JsonObject = System.Collections.Generic.IDictionary<string, object>;
namespace TweetDuck.Updates{ namespace TweetDuck.Updates{
sealed class UpdateCheckClient{ sealed class UpdateCheckClient : IUpdateCheckClient{
private const string ApiLatestRelease = "https://api.github.com/repos/chylex/TweetDuck/releases/latest"; private const string ApiLatestRelease = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
private const string UpdaterAssetName = "TweetDuck.Update.exe"; private const string UpdaterAssetName = "TweetDuck.Update.exe";
@@ -19,10 +21,12 @@ namespace TweetDuck.Updates{
this.installerFolder = installerFolder; this.installerFolder = installerFolder;
} }
public Task<UpdateInfo> Check(){ bool IUpdateCheckClient.CanCheck => Program.Config.User.EnableUpdateCheck;
Task<UpdateInfo> IUpdateCheckClient.Check(){
TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>(); TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>();
WebClient client = BrowserUtils.CreateWebClient(); WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json"; client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json";
client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => { client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {
@@ -65,10 +69,9 @@ namespace TweetDuck.Updates{
private static Exception ExpandWebException(Exception e){ private static Exception ExpandWebException(Exception e){
if (e is WebException we && we.Response is HttpWebResponse response){ if (e is WebException we && we.Response is HttpWebResponse response){
try{ try{
using(Stream stream = response.GetResponseStream()) using var stream = response.GetResponseStream();
using(StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"))){ using var reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"));
return new Reporter.ExpandedLogException(e, reader.ReadToEnd()); return new Reporter.ExpandedLogException(e, reader.ReadToEnd());
}
}catch{ }catch{
// whatever // whatever
} }

@@ -76,14 +76,14 @@ function TDGetNetFrameworkVersion: Cardinal; forward;
function TDIsVCMissing: Boolean; forward; function TDIsVCMissing: Boolean; forward;
procedure TDInstallVCRedist; forward; procedure TDInstallVCRedist; forward;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. } { Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
function InitializeSetup: Boolean; function InitializeSetup: Boolean;
begin begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}') UpdatePath := ExpandConstant('{param:UPDATEPATH}')
ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}') ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}')
VisitedTasksPage := False VisitedTasksPage := False
if (TDGetNetFrameworkVersion() < 379893) and (MsgBox('{#MyAppName} requires .NET Framework 4.5.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
begin begin
Result := False Result := False
Exit Exit

@@ -64,14 +64,14 @@ function TDGetNetFrameworkVersion: Cardinal; forward;
function TDIsVCMissing: Boolean; forward; function TDIsVCMissing: Boolean; forward;
procedure TDInstallVCRedist; forward; procedure TDInstallVCRedist; forward;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. } { Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
function InitializeSetup: Boolean; function InitializeSetup: Boolean;
begin begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}') UpdatePath := ExpandConstant('{param:UPDATEPATH}')
ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}') ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}')
VisitedTasksPage := False VisitedTasksPage := False
if (TDGetNetFrameworkVersion() < 379893) and (MsgBox('{#MyAppName} requires .NET Framework 4.5.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
begin begin
Result := False Result := False
Exit Exit

@@ -81,7 +81,7 @@ procedure TDExecuteFullDownload; forward;
var IsPortable: Boolean; var IsPortable: Boolean;
var UpdatePath: String; var UpdatePath: String;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. Prepare full download package if required. } { Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. Prepare full download package if required. }
function InitializeSetup: Boolean; function InitializeSetup: Boolean;
begin begin
IsPortable := ExpandConstant('{param:PORTABLE}') = '1' IsPortable := ExpandConstant('{param:PORTABLE}') = '1'
@@ -99,7 +99,7 @@ begin
idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe')) idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe'))
end; end;
if (TDGetNetFrameworkVersion() < 379893) and (MsgBox('{#MyAppName} requires .NET Framework 4.5.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
begin begin
Result := False Result := False
Exit Exit

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" /> <Import Project="..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -10,7 +10,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetLib.Communication</RootNamespace> <RootNamespace>TweetLib.Communication</RootNamespace>
<AssemblyName>TweetLib.Communication</AssemblyName> <AssemblyName>TweetLib.Communication</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp> </NuGetPackageImportStamp>
@@ -23,7 +23,7 @@
<PlatformTarget>x86</PlatformTarget> <PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>7</LangVersion> <LangVersion>8.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath> <OutputPath>bin\x86\Release\</OutputPath>
@@ -51,6 +51,6 @@
<PropertyGroup> <PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup> </PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" /> <Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
</Target> </Target>
</Project> </Project>

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" /> <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
</packages> </packages>

28
lib/TweetLib.Core/App.cs Normal file

@@ -0,0 +1,28 @@
using System;
using TweetLib.Core.Application;
namespace TweetLib.Core{
public sealed class App{
public static IAppErrorHandler ErrorHandler { get; private set; }
// Builder
public sealed class Builder{
public IAppErrorHandler? ErrorHandler { get; set; }
// Validation
internal void Initialize(){
App.ErrorHandler = Validate(ErrorHandler, nameof(ErrorHandler))!;
}
private T Validate<T>(T obj, string name){
if (obj == null){
throw new InvalidOperationException("Missing property " + name + " on the provided App.");
}
return obj;
}
}
}
}

@@ -0,0 +1,8 @@
using System;
namespace TweetLib.Core.Application{
public interface IAppErrorHandler{
bool Log(string text);
void HandleException(string caption, string message, bool canIgnore, Exception e);
}
}

@@ -2,8 +2,8 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace TweetDuck.Data{ namespace TweetLib.Core.Collections{
sealed class CommandLineArgs{ public sealed class CommandLineArgs{
public static CommandLineArgs FromStringArray(char entryChar, string[] array){ public static CommandLineArgs FromStringArray(char entryChar, string[] array){
CommandLineArgs args = new CommandLineArgs(); CommandLineArgs args = new CommandLineArgs();
ReadStringArray(entryChar, array, args); ReadStringArray(entryChar, array, args);
@@ -15,7 +15,7 @@ namespace TweetDuck.Data{
string entry = array[index]; string entry = array[index];
if (entry.Length > 0 && entry[0] == entryChar){ if (entry.Length > 0 && entry[0] == entryChar){
if (index < array.Length-1){ if (index < array.Length - 1){
string potentialValue = array[index+1]; string potentialValue = array[index+1];
if (potentialValue.Length > 0 && potentialValue[0] == entryChar){ if (potentialValue.Length > 0 && potentialValue[0] == entryChar){
@@ -52,7 +52,7 @@ namespace TweetDuck.Data{
} }
else{ else{
key = matchValue.Substring(0, indexEquals).TrimStart('-'); key = matchValue.Substring(0, indexEquals).TrimStart('-');
value = matchValue.Substring(indexEquals+1).Trim('"'); value = matchValue.Substring(indexEquals + 1).Trim('"');
} }
if (key.Length != 0){ if (key.Length != 0){
@@ -66,7 +66,7 @@ namespace TweetDuck.Data{
private readonly HashSet<string> flags = new HashSet<string>(); private readonly HashSet<string> flags = new HashSet<string>();
private readonly Dictionary<string, string> values = new Dictionary<string, string>(); private readonly Dictionary<string, string> values = new Dictionary<string, string>();
public int Count => flags.Count+values.Count; public int Count => flags.Count + values.Count;
public void AddFlag(string flag){ public void AddFlag(string flag){
flags.Add(flag.ToLower()); flags.Add(flag.ToLower());
@@ -84,12 +84,8 @@ namespace TweetDuck.Data{
values[key.ToLower()] = value; values[key.ToLower()] = value;
} }
public bool HasValue(string key){ public string? GetValue(string key){
return values.ContainsKey(key.ToLower()); return values.TryGetValue(key.ToLower(), out string val) ? val : null;
}
public string GetValue(string key, string defaultValue){
return values.TryGetValue(key.ToLower(), out string val) ? val : defaultValue;
} }
public void RemoveValue(string key){ public void RemoveValue(string key){
@@ -103,7 +99,7 @@ namespace TweetDuck.Data{
copy.AddFlag(flag); copy.AddFlag(flag);
} }
foreach(KeyValuePair<string, string> kvp in values){ foreach(var kvp in values){
copy.SetValue(kvp.Key, kvp.Value); copy.SetValue(kvp.Key, kvp.Value);
} }
@@ -115,7 +111,7 @@ namespace TweetDuck.Data{
target[flag] = "1"; target[flag] = "1";
} }
foreach(KeyValuePair<string, string> kvp in values){ foreach(var kvp in values){
target[kvp.Key] = kvp.Value; target[kvp.Key] = kvp.Value;
} }
} }
@@ -127,11 +123,11 @@ namespace TweetDuck.Data{
build.Append(flag).Append(' '); build.Append(flag).Append(' ');
} }
foreach(KeyValuePair<string, string> kvp in values){ foreach(var kvp in values){
build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" "); build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" ");
} }
return build.Length == 0 ? string.Empty : build.Remove(build.Length-1, 1).ToString(); return build.Length == 0 ? string.Empty : build.Remove(build.Length - 1, 1).ToString();
} }
} }
} }

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

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

@@ -1,7 +1,7 @@
using System; using System;
namespace TweetDuck.Data{ namespace TweetLib.Core.Data{
sealed class InjectedHTML{ public sealed class InjectedHTML{
public enum Position{ public enum Position{
Before, After Before, After
} }
@@ -27,7 +27,7 @@ namespace TweetDuck.Data{
switch(position){ switch(position){
case Position.Before: cutIndex = index; break; case Position.Before: cutIndex = index; break;
case Position.After: cutIndex = index+search.Length; break; case Position.After: cutIndex = index + search.Length; break;
default: return targetHTML; default: return targetHTML;
} }

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

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace TweetLib.Core.Features.Configuration{
public abstract class BaseConfig{
private readonly IConfigManager configManager;
protected BaseConfig(IConfigManager 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 (T)ConstructWithDefaults(configManager);
}
protected abstract BaseConfig ConstructWithDefaults(IConfigManager 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();
}
}
}
}

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

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

@@ -0,0 +1,6 @@
namespace TweetLib.Core.Features.Configuration{
public interface IConfigManager{
void TriggerProgramRestartRequested();
IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
}
}

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

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TweetLib.Core.Features.Configuration;
namespace TweetLib.Core.Features.Plugins.Config{
public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig{
public T Instance { get; }
private readonly string filename;
public PluginConfigInstance(string filename, T instance){
this.filename = filename;
this.Instance = instance;
}
public void Load(){
try{
using var 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.Reset(newDisabled);
}
}catch(FileNotFoundException){
}catch(DirectoryNotFoundException){
}catch(Exception e){
OnException("Could not read the plugin configuration file. If you continue, the list of disabled plugins will be reset to default.", e);
}
}
public void Save(){
try{
using var 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){
OnException("Could not save the plugin configuration file.", e);
}
}
public void Reload(){
Load();
}
public void Reset(){
try{
File.Delete(filename);
Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins);
}catch(Exception e){
OnException("Could not delete the plugin configuration file.", e);
return;
}
Reload();
}
private static void OnException(string message, Exception e){
App.ErrorHandler.HandleException("Plugin Configuration Error", message, true, e);
}
}
}

@@ -4,15 +4,15 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace TweetDuck.Plugins.Enums{ namespace TweetLib.Core.Features.Plugins.Enums{
[Flags] [Flags]
enum PluginEnvironment{ public enum PluginEnvironment{
None = 0, None = 0,
Browser = 1, Browser = 1,
Notification = 2 Notification = 2
} }
static class PluginEnvironmentExtensions{ public static class PluginEnvironmentExtensions{
public static IEnumerable<PluginEnvironment> Values{ public static IEnumerable<PluginEnvironment> Values{
get{ get{
yield return PluginEnvironment.Browser; yield return PluginEnvironment.Browser;
@@ -24,7 +24,7 @@ namespace TweetDuck.Plugins.Enums{
return environment == PluginEnvironment.Browser; return environment == PluginEnvironment.Browser;
} }
public static string GetPluginScriptFile(this PluginEnvironment environment){ public static string? GetPluginScriptFile(this PluginEnvironment environment){
switch(environment){ switch(environment){
case PluginEnvironment.Browser: return "browser.js"; case PluginEnvironment.Browser: return "browser.js";
case PluginEnvironment.Notification: return "notification.js"; case PluginEnvironment.Notification: return "notification.js";
@@ -73,7 +73,7 @@ namespace TweetDuck.Plugins.Enums{
return true; return true;
} }
else{ else{
value = default(T); value = default;
return false; return false;
} }
} }

@@ -0,0 +1,5 @@
namespace TweetLib.Core.Features.Plugins.Enums{
public enum PluginFolder{
Root, Data
}
}

@@ -1,9 +1,9 @@
namespace TweetDuck.Plugins.Enums{ namespace TweetLib.Core.Features.Plugins.Enums{
enum PluginGroup{ public enum PluginGroup{
Official, Custom Official, Custom
} }
static class PluginGroupExtensions{ public static class PluginGroupExtensions{
public static string GetIdentifierPrefix(this PluginGroup group){ public static string GetIdentifierPrefix(this PluginGroup group){
switch(group){ switch(group){
case PluginGroup.Official: return "official/"; case PluginGroup.Official: return "official/";

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

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

@@ -1,10 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using TweetDuck.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Plugins{ namespace TweetLib.Core.Features.Plugins{
sealed class Plugin{ public sealed class Plugin{
private static readonly Version AppVersion = new Version(Program.VersionTag); private static readonly Version AppVersion = new Version(Lib.VersionTag);
public string Identifier { get; } public string Identifier { get; }
public PluginGroup Group { get; } public PluginGroup Group { get; }
@@ -62,7 +62,7 @@ namespace TweetDuck.Plugins{
public string GetScriptPath(PluginEnvironment environment){ public string GetScriptPath(PluginEnvironment environment){
if (Environments.HasFlag(environment)){ if (Environments.HasFlag(environment)){
string file = environment.GetPluginScriptFile(); string? file = environment.GetPluginScriptFile();
return file != null ? Path.Combine(pathRoot, file) : string.Empty; return file != null ? Path.Combine(pathRoot, file) : string.Empty;
} }
else{ else{
@@ -124,7 +124,7 @@ namespace TweetDuck.Plugins{
public sealed class Builder{ public sealed class Builder{
private static readonly Version DefaultRequiredVersion = new Version(0, 0, 0, 0); private static readonly Version DefaultRequiredVersion = new Version(0, 0, 0, 0);
public string Name { get; set; } public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public string Author { get; set; } = "(anonymous)"; public string Author { get; set; } = "(anonymous)";
public string Version { get; set; } = string.Empty; public string Version { get; set; } = string.Empty;
@@ -144,7 +144,7 @@ namespace TweetDuck.Plugins{
this.group = group; this.group = group;
this.pathRoot = pathRoot; this.pathRoot = pathRoot;
this.pathData = pathData; this.pathData = pathData;
this.identifier = group.GetIdentifierPrefix()+name; this.identifier = group.GetIdentifierPrefix() + name;
} }
public void AddEnvironment(PluginEnvironment environment){ public void AddEnvironment(PluginEnvironment environment){

@@ -2,40 +2,35 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using TweetDuck.Plugins.Enums; using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Plugins{ namespace TweetLib.Core.Features.Plugins{
static class PluginLoader{ public static class PluginLoader{
private static readonly string[] EndTag = { "[END]" }; private static readonly string[] EndTag = { "[END]" };
public static Plugin FromFolder(string path, PluginGroup group){ public static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group){
string name = Path.GetFileName(path); Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
if (string.IsNullOrEmpty(name)){ foreach(var environment in Directory.EnumerateFiles(pathRoot, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).Select(EnvironmentFromFileName)){
throw new ArgumentException("Could not extract directory name from path: "+path); builder.AddEnvironment(environment);
}
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"); string metaFile = Path.Combine(pathRoot, ".meta");
if (!File.Exists(metaFile)){ if (!File.Exists(metaFile)){
throw new ArgumentException("Plugin is missing a .meta file"); throw new ArgumentException("Plugin is missing a .meta file");
} }
string currentTag = null, currentContents = string.Empty; string? currentTag = null;
string currentContents = string.Empty;
foreach(string line in File.ReadAllLines(metaFile, Encoding.UTF8).Concat(EndTag).Select(line => line.TrimEnd()).Where(line => line.Length > 0)){ foreach(string line in File.ReadAllLines(metaFile, Encoding.UTF8).Concat(EndTag).Select(line => line.TrimEnd()).Where(line => line.Length > 0)){
if (line[0] == '[' && line[line.Length-1] == ']'){ if (line[0] == '[' && line[line.Length - 1] == ']'){
if (currentTag != null){ if (currentTag != null){
SetProperty(builder, currentTag, currentContents); SetProperty(builder, currentTag, currentContents);
} }
currentTag = line.Substring(1, line.Length-2).ToUpper(); currentTag = line.Substring(1, line.Length - 2).ToUpper();
currentContents = string.Empty; currentContents = string.Empty;
if (line.Equals(EndTag[0])){ if (line.Equals(EndTag[0])){
@@ -43,16 +38,20 @@ namespace TweetDuck.Plugins{
} }
} }
else if (currentTag != null){ else if (currentTag != null){
currentContents = currentContents.Length == 0 ? line : currentContents+Environment.NewLine+line; currentContents = currentContents.Length == 0 ? line : currentContents + Environment.NewLine + line;
} }
else{ else{
throw new FormatException("Missing metadata tag before value: "+line); throw new FormatException($"Missing metadata tag before value: {line}");
} }
} }
return builder.BuildAndSetup(); return builder.BuildAndSetup();
} }
private static PluginEnvironment EnvironmentFromFileName(string file){
return PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
}
private static void SetProperty(Plugin.Builder builder, string tag, string value){ private static void SetProperty(Plugin.Builder builder, string tag, string value){
switch(tag){ switch(tag){
case "NAME": builder.Name = value; break; case "NAME": builder.Name = value; break;
@@ -62,8 +61,8 @@ namespace TweetDuck.Plugins{
case "WEBSITE": builder.Website = value; break; case "WEBSITE": builder.Website = value; break;
case "CONFIGFILE": builder.ConfigFile = value; break; case "CONFIGFILE": builder.ConfigFile = value; break;
case "CONFIGDEFAULT": builder.ConfigDefault = 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; 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); default: throw new FormatException($"Invalid metadata tag: {tag}");
} }
} }
} }

@@ -1,10 +1,11 @@
using System.Linq; using System.Linq;
using TweetDuck.Plugins.Enums; using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetDuck.Plugins{ namespace TweetLib.Core.Features.Plugins{
static class PluginScriptGenerator{ public static class PluginScriptGenerator{
public static string GenerateConfig(IPluginConfig config){ 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){ public static string GeneratePlugin(string pluginIdentifier, string pluginContents, int pluginToken, PluginEnvironment environment){

@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace TweetLib.Core.Features.Updates{
public interface IUpdateCheckClient{
bool CanCheck { get; }
Task<UpdateInfo> Check();
}
}

@@ -1,8 +1,8 @@
using System; using System;
using TweetDuck.Data; using TweetLib.Core.Data;
namespace TweetDuck.Updates{ namespace TweetLib.Core.Features.Updates{
sealed class UpdateCheckEventArgs : EventArgs{ public sealed class UpdateCheckEventArgs : EventArgs{
public int EventId { get; } public int EventId { get; }
public Result<UpdateInfo> Result { get; } public Result<UpdateInfo> Result { get; }

@@ -1,4 +1,4 @@
namespace TweetDuck.Updates{ namespace TweetLib.Core.Features.Updates{
public enum UpdateDownloadStatus{ public enum UpdateDownloadStatus{
None = 0, None = 0,
InProgress, InProgress,

@@ -1,34 +1,38 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TweetDuck.Data; using System.Timers;
using Timer = System.Windows.Forms.Timer; using TweetLib.Core.Data;
using Timer = System.Timers.Timer;
namespace TweetDuck.Updates{ namespace TweetLib.Core.Features.Updates{
sealed class UpdateHandler : IDisposable{ public sealed class UpdateHandler : IDisposable{
public const int CheckCodeUpdatesDisabled = -1; public const int CheckCodeUpdatesDisabled = -1;
private readonly UpdateCheckClient client; private readonly IUpdateCheckClient client;
private readonly TaskScheduler scheduler; private readonly TaskScheduler scheduler;
private readonly Timer timer; private readonly Timer timer;
public event EventHandler<UpdateCheckEventArgs> CheckFinished; public event EventHandler<UpdateCheckEventArgs> CheckFinished;
private ushort lastEventId; private ushort lastEventId;
public UpdateHandler(string installerFolder){ public UpdateHandler(IUpdateCheckClient client, TaskScheduler scheduler){
this.client = new UpdateCheckClient(installerFolder); this.client = client;
this.scheduler = TaskScheduler.FromCurrentSynchronizationContext(); this.scheduler = scheduler;
this.timer = new Timer(); this.timer = new Timer{
this.timer.Tick += timer_Tick; AutoReset = false,
Enabled = false
};
this.timer.Elapsed += timer_Elapsed;
} }
public void Dispose(){ public void Dispose(){
timer.Dispose(); timer.Dispose();
} }
private void timer_Tick(object sender, EventArgs e){ private void timer_Elapsed(object sender, ElapsedEventArgs e){
timer.Stop();
Check(false); Check(false);
} }
@@ -39,9 +43,9 @@ namespace TweetDuck.Updates{
timer.Stop(); timer.Stop();
if (Program.Config.User.EnableUpdateCheck){ if (client.CanCheck){
DateTime now = DateTime.Now; DateTime now = DateTime.Now;
TimeSpan nextHour = now.AddSeconds(60*(60-now.Minute)-now.Second)-now; TimeSpan nextHour = now.AddSeconds(60 * (60 - now.Minute) - now.Second) - now;
if (nextHour.TotalMinutes < 15){ if (nextHour.TotalMinutes < 15){
nextHour = nextHour.Add(TimeSpan.FromHours(1)); nextHour = nextHour.Add(TimeSpan.FromHours(1));
@@ -53,7 +57,7 @@ namespace TweetDuck.Updates{
} }
public int Check(bool force){ public int Check(bool force){
if (Program.Config.User.EnableUpdateCheck || force){ if (client.CanCheck || force){
int nextEventId = unchecked(++lastEventId); int nextEventId = unchecked(++lastEventId);
Task<UpdateInfo> checkTask = client.Check(); Task<UpdateInfo> checkTask = client.Check();

@@ -1,20 +1,20 @@
using System; using System;
using System.IO; using System.IO;
using System.Net; using System.Net;
using TweetDuck.Core.Utils; using TweetLib.Core.Utils;
namespace TweetDuck.Updates{ namespace TweetLib.Core.Features.Updates{
sealed class UpdateInfo{ public sealed class UpdateInfo{
public string VersionTag { get; } public string VersionTag { get; }
public string ReleaseNotes { get; } public string ReleaseNotes { get; }
public string InstallerPath { get; } public string InstallerPath { get; }
public UpdateDownloadStatus DownloadStatus { get; private set; } public UpdateDownloadStatus DownloadStatus { get; private set; }
public Exception DownloadError { get; private set; } public Exception? DownloadError { get; private set; }
private readonly string downloadUrl; private readonly string downloadUrl;
private readonly string installerFolder; private readonly string installerFolder;
private WebClient currentDownload; private WebClient? currentDownload;
public UpdateInfo(string versionTag, string releaseNotes, string downloadUrl, string installerFolder){ public UpdateInfo(string versionTag, string releaseNotes, string downloadUrl, string installerFolder){
this.downloadUrl = downloadUrl; this.downloadUrl = downloadUrl;
@@ -22,11 +22,11 @@ namespace TweetDuck.Updates{
this.VersionTag = versionTag; this.VersionTag = versionTag;
this.ReleaseNotes = releaseNotes; this.ReleaseNotes = releaseNotes;
this.InstallerPath = Path.Combine(installerFolder, $"TweetDuck.{versionTag}.exe"); this.InstallerPath = Path.Combine(installerFolder, $"{Lib.BrandName}.{versionTag}.exe");
} }
public void BeginSilentDownload(){ public void BeginSilentDownload(){
if (WindowsUtils.FileExistsAndNotEmpty(InstallerPath)){ if (FileUtils.FileExistsAndNotEmpty(InstallerPath)){
DownloadStatus = UpdateDownloadStatus.Done; DownloadStatus = UpdateDownloadStatus.Done;
return; return;
} }
@@ -48,7 +48,9 @@ namespace TweetDuck.Updates{
return; return;
} }
currentDownload = BrowserUtils.DownloadFileAsync(downloadUrl, InstallerPath, null, () => { WebClient client = WebUtils.NewClient($"{Lib.BrandName} {Lib.VersionTag}");
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
DownloadStatus = UpdateDownloadStatus.Done; DownloadStatus = UpdateDownloadStatus.Done;
currentDownload = null; currentDownload = null;
}, e => { }, e => {
@@ -56,6 +58,8 @@ namespace TweetDuck.Updates{
DownloadStatus = UpdateDownloadStatus.Failed; DownloadStatus = UpdateDownloadStatus.Failed;
currentDownload = null; currentDownload = null;
}); });
client.DownloadFileAsync(new Uri(downloadUrl), InstallerPath);
} }
} }

24
lib/TweetLib.Core/Lib.cs Normal file

@@ -0,0 +1,24 @@
using System.Globalization;
using System.Threading;
namespace TweetLib.Core{
public static class Lib{
public const string BrandName = "TweetDuck";
public const string VersionTag = "1.17.4";
public static CultureInfo Culture { get; private set; }
public static void Initialize(App.Builder app){
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
app.Initialize();
}
}
}

@@ -0,0 +1,55 @@
using System;
namespace TweetLib.Core.Serialization.Converters{
internal sealed class ClrTypeConverter : ITypeConverter{
public static ITypeConverter Instance { get; } = new ClrTypeConverter();
private ClrTypeConverter(){}
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;
}
}
}
}

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

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

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

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

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Platforms>x86</Platforms>
<LangVersion>8.0</LangVersion>
<NullableContextOptions>enable</NullableContextOptions>
</PropertyGroup>
</Project>

@@ -0,0 +1,39 @@
using System;
using System.IO;
namespace TweetLib.Core.Utils{
public static class FileUtils{
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;
}
}
}
}

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

@@ -2,8 +2,8 @@
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace TweetDuck.Core.Utils{ namespace TweetLib.Core.Utils{
static class StringUtils{ public static class StringUtils{
public static readonly string[] EmptyArray = new string[0]; public static readonly string[] EmptyArray = new string[0];
public static string ExtractBefore(string str, char search, int startIndex = 0){ public static string ExtractBefore(string str, char search, int startIndex = 0){
@@ -23,7 +23,7 @@ namespace TweetDuck.Core.Utils{
return Regex.Replace(str, @"[a-zA-Z]", match => { return Regex.Replace(str, @"[a-zA-Z]", match => {
int code = match.Value[0]; int code = match.Value[0];
int start = code <= 90 ? 65 : 97; int start = code <= 90 ? 65 : 97;
return ((char)(start+(code-start+13)%26)).ToString(); return ((char)(start + (code - start + 13) % 26)).ToString();
}); });
} }

@@ -0,0 +1,29 @@
using System;
using System.IO;
namespace TweetLib.Core.Utils{
public static class UrlUtils{
private const string TwitterTrackingUrl = "t.co";
public enum CheckResult{
Invalid, Tracking, Fine
}
public static CheckResult Check(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 ? CheckResult.Tracking : CheckResult.Fine;
}
}
return CheckResult.Invalid;
}
public static string? GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
}
}

@@ -0,0 +1,44 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
namespace TweetLib.Core.Utils{
public static class WebUtils{
private static bool HasMicrosoftBeenBroughtTo2008Yet;
private static void EnsureTLS12(){
if (!HasMicrosoftBeenBroughtTo2008Yet){
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
HasMicrosoftBeenBroughtTo2008Yet = true;
}
}
public static WebClient NewClient(string userAgent){
EnsureTLS12();
WebClient client = new WebClient{ Proxy = null };
client.Headers[HttpRequestHeader.UserAgent] = userAgent;
return client;
}
public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){
return (sender, args) => {
if (args.Cancelled){
try{
File.Delete(file);
}catch{
// didn't want it deleted anyways
}
}
else if (args.Error != null){
onFailure?.Invoke(args.Error);
}
else{
onSuccess?.Invoke();
}
};
}
}
}

@@ -1,9 +1,4 @@
using System; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics.Contracts;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
namespace TweetTest.Configuration{ namespace TweetTest.Configuration{
[TestClass] [TestClass]

@@ -2,7 +2,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Data; using TweetLib.Core.Data;
namespace TweetTest.Data{ namespace TweetTest.Data{
[TestClass] [TestClass]

@@ -1,11 +1,13 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Data.Serialization; using TweetLib.Core.Serialization;
namespace TweetTest.Data{ namespace TweetTest.Data{
[TestClass] [TestClass]
public class TestFileSerializer : TestIO{ public class TestFileSerializer : TestIO{
[SuppressMessage("ReSharper", "UnusedMember.Local")]
private enum TestEnum{ private enum TestEnum{
A, B, C, D, E A, B, C, D, E
} }

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" /> <Import Project="..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform> <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
@@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetTest</RootNamespace> <RootNamespace>TweetTest</RootNamespace>
<AssemblyName>TweetTest.System</AssemblyName> <AssemblyName>TweetTest.System</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
@@ -57,6 +57,10 @@
<Project>{2389a7cd-e0d3-4706-8294-092929a33a2d}</Project> <Project>{2389a7cd-e0d3-4706-8294-092929a33a2d}</Project>
<Name>TweetDuck</Name> <Name>TweetDuck</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\TweetLib.Core\TweetLib.Core.csproj">
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
<Name>TweetLib.Core</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
@@ -85,7 +89,7 @@
<PropertyGroup> <PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup> </PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" /> <Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
</Target> </Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" /> <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
</packages> </packages>

@@ -1,79 +0,0 @@
namespace TweetTest.Core.BrowserUtils
open Xunit
open TweetDuck.Core.Utils
module CheckUrl =
type Result = BrowserUtils.UrlCheckResult
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, BrowserUtils.CheckUrl("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", BrowserUtils.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", BrowserUtils.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com/path/"))

@@ -1,7 +1,7 @@
namespace TweetTest.Core.StringUtils namespace TweetTest.Core.StringUtils
open Xunit open Xunit
open TweetDuck.Core.Utils open TweetLib.Core.Utils
module ExtractBefore = module ExtractBefore =

@@ -0,0 +1,79 @@
namespace TweetTest.Core.UrlUtils
open Xunit
open TweetLib.Core.Utils
module Check =
type Result = UrlUtils.CheckResult
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, UrlUtils.Check("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", UrlUtils.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", UrlUtils.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com/path/"))

@@ -1,7 +1,7 @@
namespace TweetTest.Data.CommandLineArgs namespace TweetTest.Data.CommandLineArgs
open Xunit open Xunit
open TweetDuck.Data open TweetLib.Core.Collections
type _TestData = type _TestData =
@@ -139,52 +139,33 @@ module Flags =
module Values = module Values =
[<Theory>] [<Fact>]
[<InlineData("val1")>] let ``GetValue returns null if key is missing`` () =
[<InlineData("val2")>] Assert.Null(_TestData.values.GetValue("missing"))
let ``HasValue returns false if key is missing`` (key: string) =
Assert.False(_TestData.empty.HasValue(key))
Assert.False(_TestData.flags.HasValue(key))
[<Theory>] [<Theory>]
[<InlineData("flag1")>] [<InlineData("flag1")>]
[<InlineData("flag2")>] [<InlineData("flag2")>]
[<InlineData("flag3")>] [<InlineData("flag3")>]
let ``HasValue returns false if the name specifies a flag`` (key: string) = let ``GetValue returns null if the name specifies a flag`` (key: string) =
Assert.False(_TestData.flags.HasValue(key)) Assert.Null(_TestData.flags.GetValue(key))
[<Fact>] [<Fact>]
let ``HasValue returns true if the name specifies both a flag and a value key`` () = let ``GetValue returns correct value if the name specifies both a flag and a value key`` () =
Assert.True(_TestData.duplicate.HasValue("duplicate")) Assert.NotNull(_TestData.duplicate.GetValue("duplicate"))
[<Theory>]
[<InlineData("val1")>]
[<InlineData("val2")>]
let ``HasValue returns true if key is present`` (key: string) =
Assert.True(_TestData.values.HasValue(key))
[<Theory>]
[<InlineData("VAL1")>]
[<InlineData("VaL1")>]
let ``HasValue is case-insensitive`` (key: string) =
Assert.True(_TestData.values.HasValue(key))
[<Theory>] [<Theory>]
[<InlineData("val1", "hello")>] [<InlineData("val1", "hello")>]
[<InlineData("val2", "world")>] [<InlineData("val2", "world")>]
let ``GetValue returns correct value if key is present`` (key: string, expectedValue: string) = let ``GetValue returns correct value if key is present`` (key: string, expectedValue: string) =
Assert.Equal(expectedValue, _TestData.values.GetValue(key, "")) Assert.Equal(expectedValue, _TestData.values.GetValue(key))
[<Theory>] [<Theory>]
[<InlineData("VAL1", "hello")>] [<InlineData("VAL1", "hello")>]
[<InlineData("VaL1", "hello")>] [<InlineData("VaL1", "hello")>]
let ``GetValue is case-insensitive`` (key: string, expectedValue: string) = let ``GetValue is case-insensitive`` (key: string, expectedValue: string) =
Assert.Equal(expectedValue, _TestData.values.GetValue(key, "")) Assert.Equal(expectedValue, _TestData.values.GetValue(key))
[<Fact>]
let ``GetValue returns default value if key is missing`` () =
Assert.Equal("oh no", _TestData.values.GetValue("missing", "oh no"))
[<Fact>] [<Fact>]
let ``SetValue adds new value`` () = let ``SetValue adds new value`` () =
@@ -192,7 +173,7 @@ module Values =
args.SetValue("val3", "this is nice") args.SetValue("val3", "this is nice")
Assert.Equal(3, args.Count) Assert.Equal(3, args.Count)
Assert.Equal("this is nice", args.GetValue("val3", "")) Assert.Equal("this is nice", args.GetValue("val3"))
[<Fact>] [<Fact>]
let ``SetValue replaces existing value`` () = let ``SetValue replaces existing value`` () =
@@ -200,7 +181,7 @@ module Values =
args.SetValue("val2", "mom") args.SetValue("val2", "mom")
Assert.Equal(2, args.Count) Assert.Equal(2, args.Count)
Assert.Equal("mom", args.GetValue("val2", "")) Assert.Equal("mom", args.GetValue("val2"))
[<Theory>] [<Theory>]
[<InlineData("val1")>] [<InlineData("val1")>]
@@ -210,7 +191,7 @@ module Values =
args.RemoveValue(key) args.RemoveValue(key)
Assert.Equal(1, args.Count) Assert.Equal(1, args.Count)
Assert.False(args.HasValue(key)) Assert.Null(args.GetValue(key))
[<Theory>] [<Theory>]
[<InlineData("VAL1")>] [<InlineData("VAL1")>]
@@ -220,7 +201,7 @@ module Values =
args.RemoveValue(key) args.RemoveValue(key)
Assert.Equal(1, args.Count) Assert.Equal(1, args.Count)
Assert.False(args.HasValue(key)) Assert.Null(args.GetValue(key))
[<Fact>] [<Fact>]
let ``RemoveValue does nothing if key is missing`` () = let ``RemoveValue does nothing if key is missing`` () =
@@ -239,8 +220,8 @@ module Clone =
Assert.True(clone.HasFlag("flag1")) Assert.True(clone.HasFlag("flag1"))
Assert.True(clone.HasFlag("flag2")) Assert.True(clone.HasFlag("flag2"))
Assert.True(clone.HasFlag("flag3")) Assert.True(clone.HasFlag("flag3"))
Assert.Equal("hello", clone.GetValue("val1", "")) Assert.Equal("hello", clone.GetValue("val1"))
Assert.Equal("world", clone.GetValue("val2", "")) Assert.Equal("world", clone.GetValue("val2"))
[<Fact>] [<Fact>]
let ``cloning creates a new object`` () = let ``cloning creates a new object`` () =
@@ -259,7 +240,7 @@ module Clone =
Assert.True(original.HasFlag("flag1")) Assert.True(original.HasFlag("flag1"))
Assert.False(original.HasFlag("flag4")) Assert.False(original.HasFlag("flag4"))
Assert.Equal("hello", original.GetValue("val1", "")) Assert.Equal("hello", original.GetValue("val1"))
module ToDictionary = module ToDictionary =
@@ -327,8 +308,8 @@ module FromStringArray =
Assert.True(args.HasFlag("-flag1")) Assert.True(args.HasFlag("-flag1"))
Assert.True(args.HasFlag("-flag2")) Assert.True(args.HasFlag("-flag2"))
Assert.True(args.HasFlag("-flag3")) Assert.True(args.HasFlag("-flag3"))
Assert.Equal("first value", args.GetValue("-val1", "")) Assert.Equal("first value", args.GetValue("-val1"))
Assert.Equal("second value", args.GetValue("-val2", "")) Assert.Equal("second value", args.GetValue("-val2"))
module ReadCefArguments = module ReadCefArguments =
@@ -346,23 +327,23 @@ module ReadCefArguments =
let args = CommandLineArgs.ReadCefArguments("--first-value=10 --second-value=\"long string with spaces\"") let args = CommandLineArgs.ReadCefArguments("--first-value=10 --second-value=\"long string with spaces\"")
Assert.Equal(2, args.Count) Assert.Equal(2, args.Count)
Assert.Equal("10", args.GetValue("first-value", "")) Assert.Equal("10", args.GetValue("first-value"))
Assert.Equal("long string with spaces", args.GetValue("second-value", "")) Assert.Equal("long string with spaces", args.GetValue("second-value"))
[<Fact>] [<Fact>]
let ``reads flags as value keys with values of 1`` () = let ``reads flags as value keys with values of 1`` () =
let args = CommandLineArgs.ReadCefArguments("--first-flag-as-value --second-flag-as-value") let args = CommandLineArgs.ReadCefArguments("--first-flag-as-value --second-flag-as-value")
Assert.Equal(2, args.Count) Assert.Equal(2, args.Count)
Assert.Equal("1", args.GetValue("first-flag-as-value", "")) Assert.Equal("1", args.GetValue("first-flag-as-value"))
Assert.Equal("1", args.GetValue("second-flag-as-value", "")) Assert.Equal("1", args.GetValue("second-flag-as-value"))
[<Fact>] [<Fact>]
let ``reads complex string with whitespace correctly`` () = let ``reads complex string with whitespace correctly`` () =
let args = CommandLineArgs.ReadCefArguments("\t--first-value=55.5\r\n--first-flag-as-value\r\n --second-value=\"long string\"\t--second-flag-as-value ") let args = CommandLineArgs.ReadCefArguments("\t--first-value=55.5\r\n--first-flag-as-value\r\n --second-value=\"long string\"\t--second-flag-as-value ")
Assert.Equal(4, args.Count) Assert.Equal(4, args.Count)
Assert.Equal("55.5", args.GetValue("first-value", "")) Assert.Equal("55.5", args.GetValue("first-value"))
Assert.Equal("long string", args.GetValue("second-value", "")) Assert.Equal("long string", args.GetValue("second-value"))
Assert.Equal("1", args.GetValue("first-flag-as-value", "")) Assert.Equal("1", args.GetValue("first-flag-as-value"))
Assert.Equal("1", args.GetValue("second-flag-as-value", "")) Assert.Equal("1", args.GetValue("second-flag-as-value"))

@@ -1,7 +1,7 @@
namespace TweetTest.Data.InjectedHTML namespace TweetTest.Data.InjectedHTML
open Xunit open Xunit
open TweetDuck.Data open TweetLib.Core.Data
module Inject = module Inject =

@@ -1,7 +1,7 @@
namespace TweetTest.Data.Result namespace TweetTest.Data.Result
open Xunit open Xunit
open TweetDuck.Data open TweetLib.Core.Data
open System open System

@@ -1,7 +1,7 @@
namespace TweetTest.Data.TwoKeyDictionary namespace TweetTest.Data.TwoKeyDictionary
open Xunit open Xunit
open TweetDuck.Data open TweetLib.Core.Collections
open System.Collections.Generic open System.Collections.Generic

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props')" /> <Import Project="..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props')" />
<Import Project="..\..\packages\xunit.core.2.4.0\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.props')" /> <Import Project="..\..\packages\xunit.core.2.4.1\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.props')" />
<Import Project="..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props')" /> <Import Project="..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props')" />
<Import Project="..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" /> <Import Project="..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -14,7 +14,7 @@
<RootNamespace>TweetTest.Unit</RootNamespace> <RootNamespace>TweetTest.Unit</RootNamespace>
<AssemblyName>TweetTest.Unit</AssemblyName> <AssemblyName>TweetTest.Unit</AssemblyName>
<UseStandardResourceNames>True</UseStandardResourceNames> <UseStandardResourceNames>True</UseStandardResourceNames>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<TargetFSharpCoreVersion>4.4.3.0</TargetFSharpCoreVersion> <TargetFSharpCoreVersion>4.4.3.0</TargetFSharpCoreVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Name>TweetTest.Unit</Name> <Name>TweetTest.Unit</Name>
@@ -49,23 +49,10 @@
</Otherwise> </Otherwise>
</Choose> </Choose>
<Import Project="$(FSharpTargetsPath)" /> <Import Project="$(FSharpTargetsPath)" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets'))" />
<Error Condition="!Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.0\build\xunit.core.props'))" />
<Error Condition="!Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.0\build\xunit.core.targets'))" />
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props'))" />
</Target>
<Import Project="..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" />
<ItemGroup> <ItemGroup>
<Compile Include="Core\TestBrowserUtils.fs" />
<Compile Include="Core\TestStringUtils.fs" /> <Compile Include="Core\TestStringUtils.fs" />
<Compile Include="Core\TestTwitterUtils.fs" /> <Compile Include="Core\TestTwitterUtils.fs" />
<Compile Include="Core\TestUrlUtils.fs" />
<Compile Include="Data\TestCommandLineArgs.fs" /> <Compile Include="Data\TestCommandLineArgs.fs" />
<Compile Include="Data\TestInjectedHTML.fs" /> <Compile Include="Data\TestInjectedHTML.fs" />
<Compile Include="Data\TestResult.fs" /> <Compile Include="Data\TestResult.fs" />
@@ -73,8 +60,13 @@
<Content Include="packages.config" /> <Content Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\TweetLib.Core\TweetLib.Core.csproj">
<Name>TweetLib.Core</Name>
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
<Private>True</Private>
</ProjectReference>
<Reference Include="Microsoft.VisualStudio.CodeCoverage.Shim"> <Reference Include="Microsoft.VisualStudio.CodeCoverage.Shim">
<HintPath>..\..\packages\Microsoft.CodeCoverage.15.8.0\lib\net45\Microsoft.VisualStudio.CodeCoverage.Shim.dll</HintPath> <HintPath>..\..\packages\Microsoft.CodeCoverage.16.1.0\lib\net45\Microsoft.VisualStudio.CodeCoverage.Shim.dll</HintPath>
</Reference> </Reference>
<Reference Include="mscorlib" /> <Reference Include="mscorlib" />
<Reference Include="FSharp.Core"> <Reference Include="FSharp.Core">
@@ -91,20 +83,33 @@
<Private>True</Private> <Private>True</Private>
</ProjectReference> </ProjectReference>
<Reference Include="xunit.abstractions"> <Reference Include="xunit.abstractions">
<HintPath>..\..\packages\xunit.abstractions.2.0.2\lib\net35\xunit.abstractions.dll</HintPath> <HintPath>..\..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll</HintPath>
</Reference> </Reference>
<Reference Include="xunit.assert"> <Reference Include="xunit.assert">
<HintPath>..\..\packages\xunit.assert.2.4.0\lib\netstandard1.1\xunit.assert.dll</HintPath> <HintPath>..\..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll</HintPath>
</Reference> </Reference>
<Reference Include="xunit.core"> <Reference Include="xunit.core">
<HintPath>..\..\packages\xunit.extensibility.core.2.4.0\lib\net452\xunit.core.dll</HintPath> <HintPath>..\..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll</HintPath>
</Reference> </Reference>
<Reference Include="xunit.execution.desktop"> <Reference Include="xunit.execution.desktop">
<HintPath>..\..\packages\xunit.extensibility.execution.2.4.0\lib\net452\xunit.execution.desktop.dll</HintPath> <HintPath>..\..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<Import Project="..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets')" /> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<Import Project="..\..\packages\xunit.core.2.4.0\build\xunit.core.targets" Condition="Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.targets')" /> <PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets'))" />
<Error Condition="!Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.1\build\xunit.core.props'))" />
<Error Condition="!Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.1\build\xunit.core.targets'))" />
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props'))" />
</Target>
<Import Project="..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" />
<Import Project="..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets')" />
<Import Project="..\..\packages\xunit.core.2.4.1\build\xunit.core.targets" Condition="Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Microsoft.CodeCoverage" version="15.8.0" targetFramework="net452" /> <package id="Microsoft.CodeCoverage" version="16.1.0" targetFramework="net472" />
<package id="Microsoft.NET.Test.Sdk" version="15.8.0" targetFramework="net452" /> <package id="Microsoft.NET.Test.Sdk" version="16.1.0" targetFramework="net472" />
<package id="xunit" version="2.4.0" targetFramework="net452" /> <package id="xunit" version="2.4.1" targetFramework="net472" />
<package id="xunit.abstractions" version="2.0.2" targetFramework="net452" /> <package id="xunit.abstractions" version="2.0.3" targetFramework="net472" />
<package id="xunit.analyzers" version="0.10.0" targetFramework="net452" /> <package id="xunit.analyzers" version="0.10.0" targetFramework="net452" />
<package id="xunit.assert" version="2.4.0" targetFramework="net452" /> <package id="xunit.assert" version="2.4.1" targetFramework="net472" />
<package id="xunit.core" version="2.4.0" targetFramework="net452" /> <package id="xunit.core" version="2.4.1" targetFramework="net472" />
<package id="xunit.extensibility.core" version="2.4.0" targetFramework="net452" /> <package id="xunit.extensibility.core" version="2.4.1" targetFramework="net472" />
<package id="xunit.extensibility.execution" version="2.4.0" targetFramework="net452" /> <package id="xunit.extensibility.execution" version="2.4.1" targetFramework="net472" />
<package id="xunit.runner.visualstudio" version="2.4.0" targetFramework="net452" developmentDependency="true" /> <package id="xunit.runner.visualstudio" version="2.4.1" targetFramework="net472" developmentDependency="true" />
</packages> </packages>

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