1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-23 12:15:48 +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:
Daniel Chýlek 2019-05-26 14:55:12 +02:00 committed by GitHub
parent aca438b837
commit 1ccefe853a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 926 additions and 794 deletions
Configuration
Core
Data
Plugins
Program.cs
Properties
Reporter.csTweetDuck.csprojTweetDuck.sln
Updates
bld
lib

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
namespace TweetDuck.Configuration{
sealed class SystemConfig : ConfigManager.BaseConfig{
using TweetLib.Core.Features.Configuration;
namespace TweetDuck.Configuration{
sealed class SystemConfig : BaseConfig{
// CONFIGURATION DATA
@ -17,9 +19,9 @@ public bool HardwareAcceleration{
// 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);
}
}

View File

@ -5,9 +5,10 @@
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core.Features.Configuration;
namespace TweetDuck.Configuration{
sealed class UserConfig : ConfigManager.BaseConfig{
sealed class UserConfig : BaseConfig{
// CONFIGURATION DATA
@ -135,9 +136,9 @@ public string SpellCheckLanguage{
// 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);
}
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using System;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
@ -14,9 +15,10 @@
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
using TweetDuck.Updates;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Updates;
namespace TweetDuck.Core{
sealed partial class FormBrowser : Form, AnalyticsFile.IProvider{
@ -71,7 +73,7 @@ public FormBrowser(){
this.notification = new FormNotificationTweet(this, plugins);
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.updateBridge = new UpdateBridge(updates, this);

View File

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

View File

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

View File

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

View File

@ -3,9 +3,10 @@
using System.IO;
using System.Linq;
using TweetDuck.Core.Other;
using TweetDuck.Data;
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{
sealed class ProfileManager{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,10 @@
using TweetDuck.Core.Notification;
using TweetDuck.Core.Utils;
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{
static class AnalyticsReportGenerator{
@ -27,7 +30,7 @@ public static AnalyticsReport Create(AnalyticsFile file, ExternalInfo info, Plug
{ "System Edition" , SystemEdition },
{ "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" },
{ "System Build" , SystemBuild },
{ "System Locale" , Program.Culture.Name.ToLower() },
{ "System Locale" , Lib.Culture.Name.ToLower() },
0,
{ "RAM" , Exact(RamSize) },
{ "GPU" , GpuVendor },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,16 +3,16 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Windows.Forms;
using CefSharp.WinForms;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Utils{
static class BrowserUtils{
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 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 readonly bool HasDevTools = File.Exists(Path.Combine(Program.ProgramPath, "devtools_resources.pak"));
@ -74,29 +74,11 @@ void UpdateZoomLevel(object sender, EventArgs args){
};
}
private const string TwitterTrackingUrl = "t.co";
public enum UrlCheckResult{
Invalid, Tracking, Fine
}
public static UrlCheckResult CheckUrl(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? UrlCheckResult.Tracking : UrlCheckResult.Fine;
}
}
return UrlCheckResult.Invalid;
}
public static void OpenExternalBrowser(string url){
if (string.IsNullOrWhiteSpace(url))return;
switch(CheckUrl(url)){
case UrlCheckResult.Fine:
switch(UrlUtils.Check(url)){
case UrlUtils.CheckResult.Fine:
if (FormGuide.CheckGuideUrl(url, out string hash)){
FormGuide.Show(hash);
}
@ -117,9 +99,9 @@ public static void OpenExternalBrowser(string url){
break;
case UrlCheckResult.Tracking:
case UrlUtils.CheckResult.Tracking:
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)){
@ -135,13 +117,13 @@ public static void OpenExternalBrowser(string url){
}
if (result == DialogResult.Ignore || result == DialogResult.Yes){
goto case UrlCheckResult.Fine;
goto case UrlUtils.CheckResult.Fine;
}
}
break;
case UrlCheckResult.Invalid:
case UrlUtils.CheckResult.Invalid:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
break;
}
@ -174,50 +156,10 @@ public static void OpenExternalSearch(string query){
}
}
public static string GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
public static string GetErrorName(CefErrorCode code){
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
}
public static WebClient CreateWebClient(){
WindowsUtils.EnsureTLS12();
WebClient client = new WebClient{ Proxy = null };
client.Headers[HttpRequestHeader.UserAgent] = UserAgentVanilla;
return client;
}
public static WebClient DownloadFileAsync(string url, string target, string cookie, Action onSuccess, Action<Exception> onFailure){
WebClient client = CreateWebClient();
if (cookie != null){
client.Headers[HttpRequestHeader.Cookie] = cookie;
}
client.DownloadFileCompleted += (sender, args) => {
if (args.Cancelled){
try{
File.Delete(target);
}catch{
// didn't want it deleted anyways
}
}
else if (args.Error != null){
onFailure?.Invoke(args.Error);
}
else{
onSuccess?.Invoke();
}
};
client.DownloadFileAsync(new Uri(url), target);
return client;
}
public static int Scale(int baseValue, double scaleFactor){
return (int)Math.Round(baseValue*scaleFactor);
}

View File

@ -8,7 +8,9 @@
using TweetDuck.Core.Other;
using TweetDuck.Data;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using TweetLib.Core.Utils;
using Cookie = CefSharp.Cookie;
namespace TweetDuck.Core.Utils{
@ -71,7 +73,7 @@ public static string GetMediaLink(string url, ImageQuality quality){
}
public static string GetImageFileName(string url){
return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
return UrlUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
}
public static void ViewImage(string url, ImageQuality quality){
@ -88,7 +90,7 @@ void ViewImageInternal(string path){
string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
if (WindowsUtils.FileExistsAndNotEmpty(file)){
if (FileUtils.FileExistsAndNotEmpty(file)){
ViewImageInternal(file);
}
else{
@ -143,7 +145,7 @@ void OnFailure(Exception ex){
}
public static void DownloadVideo(string url, string username){
string filename = BrowserUtils.GetFileNameFromUrl(url);
string filename = UrlUtils.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename);
using(SaveFileDialog dialog = new SaveFileDialog{
@ -178,7 +180,10 @@ private static void DownloadFileAuth(string url, string target, Action onSuccess
}
}
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);
}
}

View File

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

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,12 @@
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
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{
sealed class PluginManager{
@ -126,12 +128,19 @@ IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
}
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;
try{
plugin = PluginLoader.FromFolder(fullDir, group);
plugin = PluginLoader.FromFolder(name, fullDir, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name), group);
}catch(Exception e){
loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+e.Message);
loadErrors.Add($"{group.GetIdentifierPrefix()}{name}: {e.Message}");
continue;
}

View File

@ -2,10 +2,8 @@
using CefSharp.WinForms;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core;
@ -14,14 +12,16 @@
using TweetDuck.Core.Other;
using TweetDuck.Core.Management;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core;
using TweetLib.Core.Collections;
using TweetLib.Core.Utils;
namespace TweetDuck{
static class Program{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string BrandName = Lib.BrandName;
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 bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
@ -48,23 +48,18 @@ static class Program{
private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
private static bool HasCleanedUp;
public static CultureInfo Culture { get; }
public static Reporter Reporter { get; }
public static ConfigManager Config { get; }
static Program(){
Culture = CultureInfo.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
#if DEBUG
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
#endif
Reporter = new Reporter(ErrorLogFilePath);
Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :(");
Config = new ConfigManager();
Lib.Initialize(new App.Builder{
ErrorHandler = Reporter
});
}
[STAThread]
@ -75,7 +70,7 @@ private static void Main(){
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);
return;
}
@ -131,7 +126,7 @@ private static void Main(){
}
try{
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze, null));
RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
}catch(Exception e){
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: "+e.Message, FormMessage.OK);
return;
@ -168,7 +163,7 @@ private static void Main(){
// ProgramPath has a trailing backslash
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)){
Application.Exit();
@ -180,7 +175,7 @@ private static void Main(){
}
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 (Path.GetInvalidPathChars().Any(custom.Contains)){

View File

@ -19,7 +19,7 @@ namespace TweetDuck.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// 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.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {

View File

@ -6,9 +6,11 @@
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using TweetLib.Core;
using TweetLib.Core.Application;
namespace TweetDuck{
sealed class Reporter{
sealed class Reporter : IAppErrorHandler{
private readonly string logFile;
public Reporter(string logFile){
@ -28,8 +30,12 @@ public bool LogVerbose(string data){
}
public bool LogImportant(string data){
return ((IAppErrorHandler)this).Log(data);
}
bool IAppErrorHandler.Log(string text){
#if DEBUG
Debug.WriteLine(data);
Debug.WriteLine(text);
#endif
StringBuilder build = new StringBuilder();
@ -38,8 +44,8 @@ public bool LogImportant(string data){
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(data).Append("\r\n\r\n");
build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("]\r\n");
build.Append(text).Append("\r\n\r\n");
try{
File.AppendAllText(logFile, build.ToString(), Encoding.UTF8);

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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.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')" />
@ -14,7 +14,8 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetDuck</RootNamespace>
<AssemblyName>TweetDuck</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ApplicationIcon>Resources\Images\icon.ico</ApplicationIcon>
@ -33,7 +34,6 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>7</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<OutputPath>bin\x86\Release\</OutputPath>
@ -43,7 +43,6 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>7</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
@ -55,10 +54,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\Instance\FileConfigInstance.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\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" />
@ -198,10 +194,7 @@
<DependentUpon>TabSettingsFeedback.cs</DependentUpon>
</Compile>
<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="Data\CombinedFileStream.cs" />
<Compile Include="Core\Management\ProfileManager.cs" />
<Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs">
<SubType>UserControl</SubType>
@ -231,19 +224,11 @@
<DependentUpon>TabSettingsNotifications.cs</DependentUpon>
</Compile>
<Compile Include="Core\Notification\Screenshot\ScreenshotBridge.cs" />
<Compile Include="Data\CommandLineArgs.cs" />
<Compile Include="Core\Notification\Screenshot\FormNotificationScreenshotable.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.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="Core\Utils\WindowsUtils.cs" />
<Compile Include="Core\Bridge\TweetDeckBridge.cs" />
@ -262,25 +247,15 @@
<Compile Include="Plugins\Controls\PluginListFlowLayout.cs">
<SubType>Component</SubType>
</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="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\PluginScriptGenerator.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Reporter.cs" />
<Compile Include="Updates\UpdateCheckEventArgs.cs" />
<Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType>
</Compile>
@ -297,9 +272,6 @@
<Compile Include="Core\Utils\BrowserUtils.cs" />
<Compile Include="Core\Utils\NativeMethods.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="Properties\AssemblyInfo.cs" />
<Compile Include="Resources\ScriptLoader.cs" />
@ -380,6 +352,10 @@
<None Include="Resources\Scripts\update.js" />
</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">
<Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
<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.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\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>
<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')" />

View File

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

View File

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

View File

@ -6,10 +6,12 @@
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using TweetDuck.Core.Utils;
using TweetLib.Core.Features.Updates;
using TweetLib.Core.Utils;
using JsonObject = System.Collections.Generic.IDictionary<string, object>;
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 UpdaterAssetName = "TweetDuck.Update.exe";
@ -19,10 +21,12 @@ public UpdateCheckClient(string 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>();
WebClient client = BrowserUtils.CreateWebClient();
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json";
client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {
@ -65,10 +69,9 @@ string AssetDownloadUrl(JsonObject obj){
private static Exception ExpandWebException(Exception e){
if (e is WebException we && we.Response is HttpWebResponse response){
try{
using(Stream stream = response.GetResponseStream())
using(StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"))){
return new Reporter.ExpandedLogException(e, reader.ReadToEnd());
}
using var stream = response.GetResponseStream();
using var reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"));
return new Reporter.ExpandedLogException(e, reader.ReadToEnd());
}catch{
// whatever
}

View File

@ -76,14 +76,14 @@ function TDGetNetFrameworkVersion: Cardinal; forward;
function TDIsVCMissing: Boolean; 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;
begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}')
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
Result := False
Exit

View File

@ -64,14 +64,14 @@ function TDGetNetFrameworkVersion: Cardinal; forward;
function TDIsVCMissing: Boolean; 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;
begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}')
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
Result := False
Exit

View File

@ -81,7 +81,7 @@ procedure TDExecuteFullDownload; forward;
var IsPortable: Boolean;
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;
begin
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'))
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
Result := False
Exit

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -10,7 +10,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetLib.Communication</RootNamespace>
<AssemblyName>TweetLib.Communication</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
@ -23,7 +23,7 @@
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>7</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
@ -51,6 +51,6 @@
<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.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>
</Project>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

28
lib/TweetLib.Core/App.cs Normal file
View 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;
}
}
}
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
using System;
namespace TweetDuck.Data{
sealed class InjectedHTML{
namespace TweetLib.Core.Data{
public sealed class InjectedHTML{
public enum Position{
Before, After
}
@ -27,7 +27,7 @@ public string InjectInto(string targetHTML){
switch(position){
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;
}

View File

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

View File

@ -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();
}
}
}
}

View File

@ -1,22 +1,20 @@
using System;
using System.IO;
using TweetDuck.Data.Serialization;
namespace TweetDuck.Configuration.Instance{
sealed class FileConfigInstance<T> : IConfigInstance<T> where T : ConfigManager.BaseConfig{
private const string ErrorTitle = "Configuration Error";
using TweetLib.Core.Serialization;
namespace TweetLib.Core.Features.Configuration{
public sealed class FileConfigInstance<T> : IConfigInstance<T> where T : BaseConfig{
public T Instance { get; }
public FileSerializer<T> Serializer { get; }
private readonly string filenameMain;
private readonly string filenameBackup;
private readonly string 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.filenameBackup = filename+".bak";
this.errorIdentifier = errorIdentifier;
this.filenameBackup = filename + ".bak";
this.identifier = identifier;
this.Instance = instance;
this.Serializer = new FileSerializer<T>();
@ -27,14 +25,14 @@ private void LoadInternal(bool backup){
}
public void Load(){
Exception firstException = null;
Exception? firstException = null;
for(int attempt = 0; attempt < 2; attempt++){
try{
LoadInternal(attempt > 0);
if (firstException != null){ // silently log exception that caused a backup restore
Program.Reporter.LogImportant(firstException.ToString());
App.ErrorHandler.Log(firstException.ToString());
}
return;
@ -49,13 +47,13 @@ public void Load(){
}
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){
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){
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 @@ public void Save(){
Serializer.Write(filenameMain, Instance);
}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){
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 @@ public void Reload(){
Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>());
LoadInternal(false);
}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){
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 @@ public void Reset(){
File.Delete(filenameMain);
File.Delete(filenameBackup);
}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;
}
Reload();
}
private static void OnException(string message, Exception e){
App.ErrorHandler.HandleException("Configuration Error", message, true, e);
}
}
}

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

24
lib/TweetLib.Core/Lib.cs Normal file
View 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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
}
};
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetTest</RootNamespace>
<AssemblyName>TweetTest.System</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
@ -57,6 +57,10 @@
<Project>{2389a7cd-e0d3-4706-8294-092929a33a2d}</Project>
<Name>TweetDuck</Name>
</ProjectReference>
<ProjectReference Include="..\TweetLib.Core\TweetLib.Core.csproj">
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
<Name>TweetLib.Core</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@ -85,7 +89,7 @@
<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.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>
<!-- 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.

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View File

@ -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/"))

View File

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

View File

@ -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/"))

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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.core.2.4.0\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.4.0\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.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\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.1\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.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.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')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -14,7 +14,7 @@
<RootNamespace>TweetTest.Unit</RootNamespace>
<AssemblyName>TweetTest.Unit</AssemblyName>
<UseStandardResourceNames>True</UseStandardResourceNames>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<TargetFSharpCoreVersion>4.4.3.0</TargetFSharpCoreVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Name>TweetTest.Unit</Name>
@ -49,23 +49,10 @@
</Otherwise>
</Choose>
<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>
<Compile Include="Core\TestBrowserUtils.fs" />
<Compile Include="Core\TestStringUtils.fs" />
<Compile Include="Core\TestTwitterUtils.fs" />
<Compile Include="Core\TestUrlUtils.fs" />
<Compile Include="Data\TestCommandLineArgs.fs" />
<Compile Include="Data\TestInjectedHTML.fs" />
<Compile Include="Data\TestResult.fs" />
@ -73,8 +60,13 @@
<Content Include="packages.config" />
</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">
<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 Include="mscorlib" />
<Reference Include="FSharp.Core">
@ -91,20 +83,33 @@
<Private>True</Private>
</ProjectReference>
<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 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 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 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>
</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')" />
<Import Project="..\..\packages\xunit.core.2.4.0\build\xunit.core.targets" Condition="Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</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.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

View File

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

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