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 (#264)
* 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:
parent
aca438b837
commit
1ccefe853a
Configuration
Core
Bridge
FormBrowser.csFormManager.csHandling
Management
Notification
Other
TweetDeckBrowser.csUtils
Data
Plugins
Program.csProperties
Reporter.csTweetDuck.csprojTweetDuck.slnUpdates
bld
lib
TweetLib.Communication
TweetLib.Core
App.cs
Application
Collections
Data
Features
Lib.csSerialization
TweetLib.Core.csprojUtils
TweetTest.System
TweetTest.Unit
@ -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(){
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace TweetDuck.Configuration.Instance{
|
||||
class PluginConfigInstance : IConfigInstance<PluginConfig>{
|
||||
public PluginConfig Instance { get; }
|
||||
|
||||
private readonly string filename;
|
||||
|
||||
public PluginConfigInstance(string filename, PluginConfig instance){
|
||||
this.filename = filename;
|
||||
this.Instance = instance;
|
||||
}
|
||||
|
||||
public void Load(){
|
||||
try{
|
||||
using(StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8)){
|
||||
string line = reader.ReadLine();
|
||||
|
||||
if (line == "#Disabled"){
|
||||
HashSet<string> newDisabled = new HashSet<string>();
|
||||
|
||||
while((line = reader.ReadLine()) != null){
|
||||
newDisabled.Add(line);
|
||||
}
|
||||
|
||||
Instance.ReloadSilently(newDisabled);
|
||||
}
|
||||
}
|
||||
}catch(FileNotFoundException){
|
||||
}catch(DirectoryNotFoundException){
|
||||
}catch(Exception e){
|
||||
Program.Reporter.HandleException("Plugin Configuration Error", "Could not read the plugin configuration file. If you continue, the list of disabled plugins will be reset to default.", true, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(){
|
||||
try{
|
||||
using(StreamWriter writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8)){
|
||||
writer.WriteLine("#Disabled");
|
||||
|
||||
foreach(string identifier in Instance.DisabledPlugins){
|
||||
writer.WriteLine(identifier);
|
||||
}
|
||||
}
|
||||
}catch(Exception e){
|
||||
Program.Reporter.HandleException("Plugin Configuration Error", "Could not save the plugin configuration file.", true, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reload(){
|
||||
Load();
|
||||
}
|
||||
|
||||
public void Reset(){
|
||||
try{
|
||||
File.Delete(filename);
|
||||
Instance.ReloadSilently(Instance.ConstructWithDefaults<PluginConfig>().DisabledPlugins);
|
||||
}catch(Exception e){
|
||||
Program.Reporter.HandleException("Plugin Configuration Error", "Could not delete the plugin configuration file.", true, e);
|
||||
return;
|
||||
}
|
||||
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,41 @@
|
||||
using System;
|
||||
using System.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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){}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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{
|
||||
|
@ -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;
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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);
|
||||
|
@ -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 },
|
||||
|
@ -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{
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -1,8 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace TweetDuck.Data.Serialization{
|
||||
interface ITypeConverter{
|
||||
bool TryWriteType(Type type, object value, out string converted);
|
||||
bool TryReadType(Type type, string value, out object converted);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System.Drawing;
|
||||
using System.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{
|
||||
|
@ -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{
|
||||
|
@ -1,5 +0,0 @@
|
||||
namespace TweetDuck.Plugins.Enums{
|
||||
enum PluginFolder{
|
||||
Root, Data
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
35
Program.cs
35
Program.cs
@ -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)){
|
||||
|
2
Properties/Resources.Designer.cs
generated
2
Properties/Resources.Designer.cs
generated
@ -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 {
|
||||
|
14
Reporter.cs
14
Reporter.cs
@ -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);
|
||||
|
@ -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')" />
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using TweetLib.Core.Features.Updates;
|
||||
|
||||
namespace TweetDuck.Updates{
|
||||
sealed partial class FormUpdateDownload : Form{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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
28
lib/TweetLib.Core/App.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using TweetLib.Core.Application;
|
||||
|
||||
namespace TweetLib.Core{
|
||||
public sealed class App{
|
||||
public static IAppErrorHandler ErrorHandler { get; private set; }
|
||||
|
||||
// Builder
|
||||
|
||||
public sealed class Builder{
|
||||
public IAppErrorHandler? ErrorHandler { get; set; }
|
||||
|
||||
// Validation
|
||||
|
||||
internal void Initialize(){
|
||||
App.ErrorHandler = Validate(ErrorHandler, nameof(ErrorHandler))!;
|
||||
}
|
||||
|
||||
private T Validate<T>(T obj, string name){
|
||||
if (obj == null){
|
||||
throw new InvalidOperationException("Missing property " + name + " on the provided App.");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
lib/TweetLib.Core/Application/IAppErrorHandler.cs
Normal file
8
lib/TweetLib.Core/Application/IAppErrorHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
@ -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;
|
||||
}
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
}
|
50
lib/TweetLib.Core/Features/Configuration/BaseConfig.cs
Normal file
50
lib/TweetLib.Core/Features/Configuration/BaseConfig.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
@ -0,0 +1,6 @@
|
||||
namespace TweetLib.Core.Features.Configuration{
|
||||
public interface IConfigManager{
|
||||
void TriggerProgramRestartRequested();
|
||||
IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using System.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);
|
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using TweetLib.Core.Features.Configuration;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins.Config{
|
||||
public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig{
|
||||
public T Instance { get; }
|
||||
|
||||
private readonly string filename;
|
||||
|
||||
public PluginConfigInstance(string filename, T instance){
|
||||
this.filename = filename;
|
||||
this.Instance = instance;
|
||||
}
|
||||
|
||||
public void Load(){
|
||||
try{
|
||||
using var reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8);
|
||||
string line = reader.ReadLine();
|
||||
|
||||
if (line == "#Disabled"){
|
||||
HashSet<string> newDisabled = new HashSet<string>();
|
||||
|
||||
while((line = reader.ReadLine()) != null){
|
||||
newDisabled.Add(line);
|
||||
}
|
||||
|
||||
Instance.Reset(newDisabled);
|
||||
}
|
||||
}catch(FileNotFoundException){
|
||||
}catch(DirectoryNotFoundException){
|
||||
}catch(Exception e){
|
||||
OnException("Could not read the plugin configuration file. If you continue, the list of disabled plugins will be reset to default.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(){
|
||||
try{
|
||||
using var writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8);
|
||||
writer.WriteLine("#Disabled");
|
||||
|
||||
foreach(string identifier in Instance.DisabledPlugins){
|
||||
writer.WriteLine(identifier);
|
||||
}
|
||||
}catch(Exception e){
|
||||
OnException("Could not save the plugin configuration file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reload(){
|
||||
Load();
|
||||
}
|
||||
|
||||
public void Reset(){
|
||||
try{
|
||||
File.Delete(filename);
|
||||
Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins);
|
||||
}catch(Exception e){
|
||||
OnException("Could not delete the plugin configuration file.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
private static void OnException(string message, Exception e){
|
||||
App.ErrorHandler.HandleException("Plugin Configuration Error", message, true, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,15 +4,15 @@
|
||||
using System.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;
|
||||
}
|
||||
}
|
5
lib/TweetLib.Core/Features/Plugins/Enums/PluginFolder.cs
Normal file
5
lib/TweetLib.Core/Features/Plugins/Enums/PluginFolder.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace TweetLib.Core.Features.Plugins.Enums{
|
||||
public enum PluginFolder{
|
||||
Root, Data
|
||||
}
|
||||
}
|
@ -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/";
|
@ -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; }
|
||||
|
@ -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; }
|
@ -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){
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
@ -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){
|
8
lib/TweetLib.Core/Features/Updates/IUpdateCheckClient.cs
Normal file
8
lib/TweetLib.Core/Features/Updates/IUpdateCheckClient.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TweetLib.Core.Features.Updates{
|
||||
public interface IUpdateCheckClient{
|
||||
bool CanCheck { get; }
|
||||
Task<UpdateInfo> Check();
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using 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; }
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace TweetDuck.Updates{
|
||||
namespace TweetLib.Core.Features.Updates{
|
||||
public enum UpdateDownloadStatus{
|
||||
None = 0,
|
||||
InProgress,
|
@ -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();
|
||||
|
@ -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
24
lib/TweetLib.Core/Lib.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace TweetLib.Core{
|
||||
public static class Lib{
|
||||
public const string BrandName = "TweetDuck";
|
||||
public const string VersionTag = "1.17.4";
|
||||
|
||||
public static CultureInfo Culture { get; private set; }
|
||||
|
||||
public static void Initialize(App.Builder app){
|
||||
Culture = CultureInfo.CurrentCulture;
|
||||
|
||||
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
|
||||
#if DEBUG
|
||||
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
|
||||
#endif
|
||||
|
||||
app.Initialize();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace TweetLib.Core.Serialization.Converters{
|
||||
internal sealed class ClrTypeConverter : ITypeConverter{
|
||||
public static ITypeConverter Instance { get; } = new ClrTypeConverter();
|
||||
|
||||
private ClrTypeConverter(){}
|
||||
|
||||
bool ITypeConverter.TryWriteType(Type type, object value, out string? converted){
|
||||
switch(Type.GetTypeCode(type)){
|
||||
case TypeCode.Boolean:
|
||||
converted = value.ToString();
|
||||
return true;
|
||||
|
||||
case TypeCode.Int32:
|
||||
converted = ((int)value).ToString(); // cast required for enums
|
||||
return true;
|
||||
|
||||
case TypeCode.String:
|
||||
converted = value?.ToString();
|
||||
return true;
|
||||
|
||||
default:
|
||||
converted = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ITypeConverter.TryReadType(Type type, string value, out object? converted){
|
||||
switch(Type.GetTypeCode(type)){
|
||||
case TypeCode.Boolean:
|
||||
if (bool.TryParse(value, out bool b)){
|
||||
converted = b;
|
||||
return true;
|
||||
}
|
||||
else goto default;
|
||||
|
||||
case TypeCode.Int32:
|
||||
if (int.TryParse(value, out int i)){
|
||||
converted = i;
|
||||
return true;
|
||||
}
|
||||
else goto default;
|
||||
|
||||
case TypeCode.String:
|
||||
converted = value;
|
||||
return true;
|
||||
|
||||
default:
|
||||
converted = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
using System;
|
||||
|
||||
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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
lib/TweetLib.Core/Serialization/ITypeConverter.cs
Normal file
8
lib/TweetLib.Core/Serialization/ITypeConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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)){
|
10
lib/TweetLib.Core/TweetLib.Core.csproj
Normal file
10
lib/TweetLib.Core/TweetLib.Core.csproj
Normal 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>
|
39
lib/TweetLib.Core/Utils/FileUtils.cs
Normal file
39
lib/TweetLib.Core/Utils/FileUtils.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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{
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
29
lib/TweetLib.Core/Utils/UrlUtils.cs
Normal file
29
lib/TweetLib.Core/Utils/UrlUtils.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
44
lib/TweetLib.Core/Utils/WebUtils.cs
Normal file
44
lib/TweetLib.Core/Utils/WebUtils.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace TweetLib.Core.Utils{
|
||||
public static class WebUtils{
|
||||
private static bool HasMicrosoftBeenBroughtTo2008Yet;
|
||||
|
||||
private static void EnsureTLS12(){
|
||||
if (!HasMicrosoftBeenBroughtTo2008Yet){
|
||||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||||
ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
|
||||
HasMicrosoftBeenBroughtTo2008Yet = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static WebClient NewClient(string userAgent){
|
||||
EnsureTLS12();
|
||||
|
||||
WebClient client = new WebClient{ Proxy = null };
|
||||
client.Headers[HttpRequestHeader.UserAgent] = userAgent;
|
||||
return client;
|
||||
}
|
||||
|
||||
public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){
|
||||
return (sender, args) => {
|
||||
if (args.Cancelled){
|
||||
try{
|
||||
File.Delete(file);
|
||||
}catch{
|
||||
// didn't want it deleted anyways
|
||||
}
|
||||
}
|
||||
else if (args.Error != null){
|
||||
onFailure?.Invoke(args.Error);
|
||||
}
|
||||
else{
|
||||
onSuccess?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using 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]
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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>
|
@ -1,79 +0,0 @@
|
||||
namespace TweetTest.Core.BrowserUtils
|
||||
|
||||
open Xunit
|
||||
open TweetDuck.Core.Utils
|
||||
|
||||
|
||||
module CheckUrl =
|
||||
type Result = BrowserUtils.UrlCheckResult
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts HTTP protocol`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts HTTPS protocol`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("https://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts FTP protocol`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("ftp://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts MAILTO protocol`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("mailto://someone@example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts URL with port, path, query, and hash`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts IPv4 address`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://127.0.0.1"))
|
||||
|
||||
[<Fact>]
|
||||
let ``accepts IPv6 address`` () =
|
||||
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://[2001:db8:0:0:0:ff00:42:8329]"))
|
||||
|
||||
[<Fact>]
|
||||
let ``recognizes t.co as tracking URL`` () =
|
||||
Assert.Equal(Result.Tracking, BrowserUtils.CheckUrl("http://t.co/12345"))
|
||||
|
||||
[<Fact>]
|
||||
let ``rejects empty URL`` () =
|
||||
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl(""))
|
||||
|
||||
[<Fact>]
|
||||
let ``rejects missing protocol`` () =
|
||||
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("www.example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``rejects banned protocol`` () =
|
||||
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("file://example.com"))
|
||||
|
||||
|
||||
module GetFileNameFromUrl =
|
||||
|
||||
[<Fact>]
|
||||
let ``simple file URL returns file name`` () =
|
||||
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html"))
|
||||
|
||||
[<Fact>]
|
||||
let ``file URL with query returns file name`` () =
|
||||
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
|
||||
|
||||
[<Fact>]
|
||||
let ``file URL w/o extension returns file name`` () =
|
||||
Assert.Equal("index", BrowserUtils.GetFileNameFromUrl("http://example.com/index"))
|
||||
|
||||
[<Fact>]
|
||||
let ``file URL with trailing dot returns file name with dot`` () =
|
||||
Assert.Equal("index.", BrowserUtils.GetFileNameFromUrl("http://example.com/index."))
|
||||
|
||||
[<Fact>]
|
||||
let ``root URL returns null`` () =
|
||||
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com"))
|
||||
|
||||
[<Fact>]
|
||||
let ``path URL returns null`` () =
|
||||
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com/path/"))
|
@ -1,7 +1,7 @@
|
||||
namespace TweetTest.Core.StringUtils
|
||||
|
||||
open Xunit
|
||||
open TweetDuck.Core.Utils
|
||||
open TweetLib.Core.Utils
|
||||
|
||||
|
||||
module ExtractBefore =
|
||||
|
79
lib/TweetTest.Unit/Core/TestUrlUtils.fs
Normal file
79
lib/TweetTest.Unit/Core/TestUrlUtils.fs
Normal 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/"))
|
@ -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"))
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace TweetTest.Data.InjectedHTML
|
||||
|
||||
open Xunit
|
||||
open TweetDuck.Data
|
||||
open TweetLib.Core.Data
|
||||
|
||||
|
||||
module Inject =
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace TweetTest.Data.Result
|
||||
|
||||
open Xunit
|
||||
open TweetDuck.Data
|
||||
open TweetLib.Core.Data
|
||||
open System
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace TweetTest.Data.TwoKeyDictionary
|
||||
|
||||
open Xunit
|
||||
open TweetDuck.Data
|
||||
open TweetLib.Core.Collections
|
||||
open System.Collections.Generic
|
||||
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user