From 2820fc8acf4f933ff9cd734d0886e294e29fe78e Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Tue, 4 Jul 2017 22:01:33 +0200
Subject: [PATCH 01/17] Fix some modals not closing when pressing the back
 button

---
 Resources/Scripts/code.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Resources/Scripts/code.js b/Resources/Scripts/code.js
index 12d2a6f9..26c324d2 100644
--- a/Resources/Scripts/code.js
+++ b/Resources/Scripts/code.js
@@ -498,7 +498,7 @@
     
     var tryCloseModal1 = function(){
       var modal = $("#open-modal");
-      return modal.is(":visible") && tryClickSelector("a[rel=dismiss]", modal);
+      return modal.is(":visible") && tryClickSelector("a.mdl-dismiss", modal);
     };
     
     var tryCloseModal2 = function(){

From 9afb58e4a75d4cd53ffe7a99018b1b9cce6b7dd3 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Thu, 6 Jul 2017 03:30:15 +0200
Subject: [PATCH 02/17] Remove unused 'using' statement

---
 Core/FormBrowser.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index 7615f00a..3843debf 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -1,7 +1,6 @@
 using CefSharp;
 using CefSharp.WinForms;
 using System;
-using System.Diagnostics;
 using System.Drawing;
 using System.Linq;
 using System.Windows.Forms;

From 1645079bc0c577849eb15358025ef98bba5c6e71 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Thu, 6 Jul 2017 03:47:59 +0200
Subject: [PATCH 03/17] Allow plugins to modify screenshot css and include a
 'td-screenshot' body class

---
 Core/FormBrowser.cs                             |  2 +-
 .../FormNotificationScreenshotable.cs           | 17 ++++++++++++++---
 .../Screenshot/TweetScreenshotManager.cs        |  7 +++++--
 3 files changed, 20 insertions(+), 6 deletions(-)

diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index 3843debf..8686438d 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -456,7 +456,7 @@ public void PlayNotificationSound(){
 
         public void OnTweetScreenshotReady(string html, int width, int height){
             if (notificationScreenshotManager == null){
-                notificationScreenshotManager = new TweetScreenshotManager(this);
+                notificationScreenshotManager = new TweetScreenshotManager(this, plugins);
             }
 
             notificationScreenshotManager.Trigger(html, width, height);
diff --git a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
index e1e427b8..a56b55d2 100644
--- a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
+++ b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
@@ -4,11 +4,16 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Bridge;
 using TweetDuck.Core.Utils;
+using TweetDuck.Plugins;
 using TweetDuck.Resources;
 
 namespace TweetDuck.Core.Notification.Screenshot{
     sealed class FormNotificationScreenshotable : FormNotificationBase{
-        public FormNotificationScreenshotable(Action callback, Form owner) : base(owner, false){
+        private readonly PluginManager plugins;
+
+        public FormNotificationScreenshotable(Action callback, Form owner, PluginManager pluginManager) : base(owner, false){
+            this.plugins = pluginManager;
+
             browser.RegisterAsyncJsObject("$TD_NotificationScreenshot", new CallbackBridge(this, callback));
 
             browser.FrameLoadEnd += (sender, args) => {
@@ -17,9 +22,15 @@ public FormNotificationScreenshotable(Action callback, Form owner) : base(owner,
                 }
             };
         }
-
+        
         protected override string GetTweetHTML(TweetNotification tweet){
-            return tweet.GenerateHtml(enableCustomCSS: false);
+            string html = tweet.GenerateHtml("td-screenshot", false);
+
+            foreach(InjectedHTML injection in plugins.Bridge.NotificationInjections){
+                html = injection.Inject(html);
+            }
+
+            return html;
         }
 
         public void LoadNotificationForScreenshot(TweetNotification tweet, int width, int height){
diff --git a/Core/Notification/Screenshot/TweetScreenshotManager.cs b/Core/Notification/Screenshot/TweetScreenshotManager.cs
index 27e4885f..ddae302a 100644
--- a/Core/Notification/Screenshot/TweetScreenshotManager.cs
+++ b/Core/Notification/Screenshot/TweetScreenshotManager.cs
@@ -4,17 +4,20 @@
 using System;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
+using TweetDuck.Plugins;
 
 namespace TweetDuck.Core.Notification.Screenshot{
     sealed class TweetScreenshotManager : IDisposable{
         private readonly Form owner;
+        private readonly PluginManager plugins;
         private readonly Timer timeout;
         private readonly Timer disposer;
         
         private FormNotificationScreenshotable screenshot;
 
-        public TweetScreenshotManager(Form owner){
+        public TweetScreenshotManager(Form owner, PluginManager pluginManager){
             this.owner = owner;
+            this.plugins = pluginManager;
 
             this.timeout = new Timer{ Interval = 8000 };
             this.timeout.Tick += timeout_Tick;
@@ -40,7 +43,7 @@ public void Trigger(string html, int width, int height){
                 return;
             }
 
-            screenshot = new FormNotificationScreenshotable(Callback, owner){
+            screenshot = new FormNotificationScreenshotable(Callback, owner, plugins){
                 CanMoveWindow = () => false
             };
 

From 4bff006743dac74e4181b59eb692088a496f63d5 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Thu, 6 Jul 2017 20:58:06 +0200
Subject: [PATCH 04/17] Refactor (move files into different namespaces)

---
 Configuration/Arguments.cs                             |  2 +-
 Configuration/UserConfig.cs                            |  2 +-
 Core/FormBrowser.cs                                    |  2 +-
 Core/Notification/FormNotificationMain.cs              |  1 +
 .../Screenshot/FormNotificationScreenshotable.cs       |  1 +
 Core/Notification/SoundNotification.cs                 |  1 -
 Core/Other/Settings/Dialogs/DialogSettingsRestart.cs   |  2 +-
 Core/Other/Settings/Export/ExportManager.cs            |  1 +
 Core/Other/Settings/TabSettingsSounds.cs               |  2 +-
 Core/Utils/CommandLineArgsParser.cs                    |  1 +
 .../Settings/Export => Data}/CombinedFileStream.cs     |  2 +-
 {Core/Utils => Data}/CommandLineArgs.cs                |  2 +-
 {Core/Utils => Data}/InjectedHTML.cs                   |  2 +-
 {Core/Utils => Data}/TwoKeyDictionary.cs               |  2 +-
 {Core/Utils => Data}/WindowState.cs                    |  2 +-
 Plugins/PluginBridge.cs                                |  2 +-
 Program.cs                                             |  1 +
 TweetDuck.csproj                                       | 10 +++++-----
 lib/TweetLib.Audio/AudioPlayer.cs                      |  1 -
 lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs     |  1 -
 .../{Utils => }/PlaybackErrorEventArgs.cs              |  2 +-
 lib/TweetLib.Audio/TweetLib.Audio.csproj               |  4 ++--
 tests/Core/{Utils => }/TestBrowserUtils.cs             |  2 +-
 tests/Core/{Utils => }/TestCommandLineArgsParser.cs    |  3 ++-
 .../{Core/Settings => Data}/TestCombinedFileStream.cs  |  8 ++++----
 tests/{Core/Utils => Data}/TestCommandLineArgs.cs      |  8 ++++----
 tests/{Core/Utils => Data}/TestInjectedHTML.cs         |  4 ++--
 tests/{Core/Utils => Data}/TestTwoKeyDictionary.cs     |  6 +++---
 28 files changed, 40 insertions(+), 37 deletions(-)
 rename {Core/Other/Settings/Export => Data}/CombinedFileStream.cs (98%)
 rename {Core/Utils => Data}/CommandLineArgs.cs (99%)
 rename {Core/Utils => Data}/InjectedHTML.cs (96%)
 rename {Core/Utils => Data}/TwoKeyDictionary.cs (98%)
 rename {Core/Utils => Data}/WindowState.cs (96%)
 rename lib/TweetLib.Audio/{Utils => }/PlaybackErrorEventArgs.cs (90%)
 rename tests/Core/{Utils => }/TestBrowserUtils.cs (99%)
 rename tests/Core/{Utils => }/TestCommandLineArgsParser.cs (96%)
 rename tests/{Core/Settings => Data}/TestCombinedFileStream.cs (97%)
 rename tests/{Core/Utils => Data}/TestCommandLineArgs.cs (97%)
 rename tests/{Core/Utils => Data}/TestInjectedHTML.cs (98%)
 rename tests/{Core/Utils => Data}/TestTwoKeyDictionary.cs (99%)

diff --git a/Configuration/Arguments.cs b/Configuration/Arguments.cs
index 34c52491..85ed6beb 100644
--- a/Configuration/Arguments.cs
+++ b/Configuration/Arguments.cs
@@ -1,5 +1,5 @@
 using System;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 
 namespace TweetDuck.Configuration{
     static class Arguments{
diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 0c386971..7a47c1c8 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -6,7 +6,7 @@
 using TweetDuck.Core;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Notification;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 
 namespace TweetDuck.Configuration{
     [Serializable]
diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index 8686438d..59e55395 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -19,7 +19,7 @@
 using TweetDuck.Resources;
 using TweetDuck.Updates;
 using TweetDuck.Updates.Events;
-using TweetLib.Audio.Utils;
+using TweetLib.Audio;
 
 namespace TweetDuck.Core{
     sealed partial class FormBrowser : Form{
diff --git a/Core/Notification/FormNotificationMain.cs b/Core/Notification/FormNotificationMain.cs
index a230c6b2..eec4cc0d 100644
--- a/Core/Notification/FormNotificationMain.cs
+++ b/Core/Notification/FormNotificationMain.cs
@@ -5,6 +5,7 @@
 using TweetDuck.Core.Bridge;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 using TweetDuck.Plugins;
 using TweetDuck.Plugins.Enums;
 using TweetDuck.Resources;
diff --git a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
index a56b55d2..e308aa4f 100644
--- a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
+++ b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
@@ -4,6 +4,7 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Bridge;
 using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 using TweetDuck.Plugins;
 using TweetDuck.Resources;
 
diff --git a/Core/Notification/SoundNotification.cs b/Core/Notification/SoundNotification.cs
index c0f05f42..645e0262 100644
--- a/Core/Notification/SoundNotification.cs
+++ b/Core/Notification/SoundNotification.cs
@@ -1,6 +1,5 @@
 using System;
 using TweetLib.Audio;
-using TweetLib.Audio.Utils;
 
 namespace TweetDuck.Core.Notification{
     sealed class SoundNotification : IDisposable{
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs b/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs
index a719d84c..e362b6b3 100644
--- a/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs
+++ b/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs
@@ -3,7 +3,7 @@
 using System.Linq;
 using System.Windows.Forms;
 using TweetDuck.Configuration;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 
 namespace TweetDuck.Core.Other.Settings.Dialogs{
     sealed partial class DialogSettingsRestart : Form{
diff --git a/Core/Other/Settings/Export/ExportManager.cs b/Core/Other/Settings/Export/ExportManager.cs
index e3f2c3a2..cf42ab4b 100644
--- a/Core/Other/Settings/Export/ExportManager.cs
+++ b/Core/Other/Settings/Export/ExportManager.cs
@@ -4,6 +4,7 @@
 using System.Linq;
 using System.Windows.Forms;
 using TweetDuck.Configuration;
+using TweetDuck.Data;
 using TweetDuck.Plugins;
 using TweetDuck.Plugins.Enums;
 
diff --git a/Core/Other/Settings/TabSettingsSounds.cs b/Core/Other/Settings/TabSettingsSounds.cs
index a2947eef..fe320588 100644
--- a/Core/Other/Settings/TabSettingsSounds.cs
+++ b/Core/Other/Settings/TabSettingsSounds.cs
@@ -3,7 +3,7 @@
 using System.IO;
 using System.Windows.Forms;
 using TweetDuck.Core.Notification;
-using TweetLib.Audio.Utils;
+using TweetLib.Audio;
 
 namespace TweetDuck.Core.Other.Settings{
     partial class TabSettingsSounds : BaseTabSettings{
diff --git a/Core/Utils/CommandLineArgsParser.cs b/Core/Utils/CommandLineArgsParser.cs
index 7c3397bf..538b8a73 100644
--- a/Core/Utils/CommandLineArgsParser.cs
+++ b/Core/Utils/CommandLineArgsParser.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Text.RegularExpressions;
+using TweetDuck.Data;
 
 namespace TweetDuck.Core.Utils{
     static class CommandLineArgsParser{
diff --git a/Core/Other/Settings/Export/CombinedFileStream.cs b/Data/CombinedFileStream.cs
similarity index 98%
rename from Core/Other/Settings/Export/CombinedFileStream.cs
rename to Data/CombinedFileStream.cs
index 792a28f0..48fab228 100644
--- a/Core/Other/Settings/Export/CombinedFileStream.cs
+++ b/Data/CombinedFileStream.cs
@@ -2,7 +2,7 @@
 using System.IO;
 using System.Text;
 
-namespace TweetDuck.Core.Other.Settings.Export{
+namespace TweetDuck.Data{
     class CombinedFileStream : IDisposable{
         public const char KeySeparator = '|';
 
diff --git a/Core/Utils/CommandLineArgs.cs b/Data/CommandLineArgs.cs
similarity index 99%
rename from Core/Utils/CommandLineArgs.cs
rename to Data/CommandLineArgs.cs
index 295233ec..e81a886b 100644
--- a/Core/Utils/CommandLineArgs.cs
+++ b/Data/CommandLineArgs.cs
@@ -1,7 +1,7 @@
 using System.Collections.Generic;
 using System.Text;
 
-namespace TweetDuck.Core.Utils{
+namespace TweetDuck.Data{
     class CommandLineArgs{
         public static CommandLineArgs FromStringArray(char entryChar, string[] array){
             CommandLineArgs args = new CommandLineArgs();
diff --git a/Core/Utils/InjectedHTML.cs b/Data/InjectedHTML.cs
similarity index 96%
rename from Core/Utils/InjectedHTML.cs
rename to Data/InjectedHTML.cs
index 7982ff36..7268f987 100644
--- a/Core/Utils/InjectedHTML.cs
+++ b/Data/InjectedHTML.cs
@@ -1,6 +1,6 @@
 using System;
 
-namespace TweetDuck.Core.Utils{
+namespace TweetDuck.Data{
     class InjectedHTML{
         public enum Position{
             Before, After
diff --git a/Core/Utils/TwoKeyDictionary.cs b/Data/TwoKeyDictionary.cs
similarity index 98%
rename from Core/Utils/TwoKeyDictionary.cs
rename to Data/TwoKeyDictionary.cs
index 39db733d..5655b68c 100644
--- a/Core/Utils/TwoKeyDictionary.cs
+++ b/Data/TwoKeyDictionary.cs
@@ -1,7 +1,7 @@
 using System.Collections.Generic;
 using System.Linq;
 
-namespace TweetDuck.Core.Utils{
+namespace TweetDuck.Data{
     class TwoKeyDictionary<K1, K2, V>{
         private readonly Dictionary<K1, Dictionary<K2, V>> dict;
         private readonly int innerCapacity;
diff --git a/Core/Utils/WindowState.cs b/Data/WindowState.cs
similarity index 96%
rename from Core/Utils/WindowState.cs
rename to Data/WindowState.cs
index 2196df35..a29efac5 100644
--- a/Core/Utils/WindowState.cs
+++ b/Data/WindowState.cs
@@ -3,7 +3,7 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 
-namespace TweetDuck.Core.Utils{
+namespace TweetDuck.Data{
     [Serializable]
     class WindowState{
         private Rectangle rect;
diff --git a/Plugins/PluginBridge.cs b/Plugins/PluginBridge.cs
index 64e2c815..91dee055 100644
--- a/Plugins/PluginBridge.cs
+++ b/Plugins/PluginBridge.cs
@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 using TweetDuck.Plugins.Enums;
 using TweetDuck.Plugins.Events;
 
diff --git a/Program.cs b/Program.cs
index 1ac12c63..232ff4e4 100644
--- a/Program.cs
+++ b/Program.cs
@@ -11,6 +11,7 @@
 using TweetDuck.Core.Other;
 using TweetDuck.Core.Other.Settings.Export;
 using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 using TweetDuck.Plugins;
 using TweetDuck.Plugins.Events;
 using TweetDuck.Updates;
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 8cc74af7..bab4a95c 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -164,7 +164,7 @@
     <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsRestart.Designer.cs">
       <DependentUpon>DialogSettingsRestart.cs</DependentUpon>
     </Compile>
-    <Compile Include="Core\Other\Settings\Export\CombinedFileStream.cs" />
+    <Compile Include="Data\CombinedFileStream.cs" />
     <Compile Include="Core\Other\Settings\Export\ExportFileFlags.cs" />
     <Compile Include="Core\Other\Settings\Export\ExportManager.cs" />
     <Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs">
@@ -195,15 +195,15 @@
       <DependentUpon>TabSettingsNotifications.cs</DependentUpon>
     </Compile>
     <Compile Include="Core\Bridge\CallbackBridge.cs" />
-    <Compile Include="Core\Utils\CommandLineArgs.cs" />
+    <Compile Include="Data\CommandLineArgs.cs" />
     <Compile Include="Core\Utils\CommandLineArgsParser.cs" />
     <Compile Include="Core\Notification\Screenshot\FormNotificationScreenshotable.cs">
       <SubType>Form</SubType>
     </Compile>
     <Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
-    <Compile Include="Core\Utils\InjectedHTML.cs" />
-    <Compile Include="Core\Utils\TwoKeyDictionary.cs" />
-    <Compile Include="Core\Utils\WindowState.cs" />
+    <Compile Include="Data\InjectedHTML.cs" />
+    <Compile Include="Data\TwoKeyDictionary.cs" />
+    <Compile Include="Data\WindowState.cs" />
     <Compile Include="Core\Utils\WindowsUtils.cs" />
     <Compile Include="Core\Bridge\TweetDeckBridge.cs" />
     <Compile Include="Core\Other\FormSettings.cs">
diff --git a/lib/TweetLib.Audio/AudioPlayer.cs b/lib/TweetLib.Audio/AudioPlayer.cs
index 787d07f9..70730f45 100644
--- a/lib/TweetLib.Audio/AudioPlayer.cs
+++ b/lib/TweetLib.Audio/AudioPlayer.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Runtime.InteropServices;
 using TweetLib.Audio.Impl;
-using TweetLib.Audio.Utils;
 
 namespace TweetLib.Audio{
     public abstract class AudioPlayer : IDisposable{
diff --git a/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs b/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs
index d10219fe..9bc73f93 100644
--- a/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs
+++ b/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs
@@ -1,7 +1,6 @@
 using System;
 using System.IO;
 using System.Media;
-using TweetLib.Audio.Utils;
 
 namespace TweetLib.Audio.Impl{
     sealed class SoundPlayerImplFallback : AudioPlayer{
diff --git a/lib/TweetLib.Audio/Utils/PlaybackErrorEventArgs.cs b/lib/TweetLib.Audio/PlaybackErrorEventArgs.cs
similarity index 90%
rename from lib/TweetLib.Audio/Utils/PlaybackErrorEventArgs.cs
rename to lib/TweetLib.Audio/PlaybackErrorEventArgs.cs
index 199138fc..f8f07325 100644
--- a/lib/TweetLib.Audio/Utils/PlaybackErrorEventArgs.cs
+++ b/lib/TweetLib.Audio/PlaybackErrorEventArgs.cs
@@ -1,6 +1,6 @@
 using System;
 
-namespace TweetLib.Audio.Utils{
+namespace TweetLib.Audio{
     public sealed class PlaybackErrorEventArgs : EventArgs{
         public string Message { get; }
         public bool Ignore { get; set; }
diff --git a/lib/TweetLib.Audio/TweetLib.Audio.csproj b/lib/TweetLib.Audio/TweetLib.Audio.csproj
index 053c1efb..b4d9968b 100644
--- a/lib/TweetLib.Audio/TweetLib.Audio.csproj
+++ b/lib/TweetLib.Audio/TweetLib.Audio.csproj
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
@@ -30,7 +30,7 @@
   <ItemGroup>
     <Compile Include="AudioPlayer.cs" />
     <Compile Include="Utils\NativeCoreAudio.cs" />
-    <Compile Include="Utils\PlaybackErrorEventArgs.cs" />
+    <Compile Include="PlaybackErrorEventArgs.cs" />
     <Compile Include="Impl\SoundPlayerImplFallback.cs" />
     <Compile Include="Impl\SoundPlayerImplWMP.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
diff --git a/tests/Core/Utils/TestBrowserUtils.cs b/tests/Core/TestBrowserUtils.cs
similarity index 99%
rename from tests/Core/Utils/TestBrowserUtils.cs
rename to tests/Core/TestBrowserUtils.cs
index fa0f72f4..c71e0f65 100644
--- a/tests/Core/Utils/TestBrowserUtils.cs
+++ b/tests/Core/TestBrowserUtils.cs
@@ -1,7 +1,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using TweetDuck.Core.Utils;
 
-namespace UnitTests.Core.Utils{
+namespace UnitTests.Core{
     [TestClass]
     public class TestBrowserUtils{
         [TestMethod]
diff --git a/tests/Core/Utils/TestCommandLineArgsParser.cs b/tests/Core/TestCommandLineArgsParser.cs
similarity index 96%
rename from tests/Core/Utils/TestCommandLineArgsParser.cs
rename to tests/Core/TestCommandLineArgsParser.cs
index ececcfd4..ef7cf88a 100644
--- a/tests/Core/Utils/TestCommandLineArgsParser.cs
+++ b/tests/Core/TestCommandLineArgsParser.cs
@@ -1,7 +1,8 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 
-namespace UnitTests.Core.Utils{
+namespace UnitTests.Core{
     [TestClass]
     public class TestCommandLineArgsParser{
         [TestMethod]
diff --git a/tests/Core/Settings/TestCombinedFileStream.cs b/tests/Data/TestCombinedFileStream.cs
similarity index 97%
rename from tests/Core/Settings/TestCombinedFileStream.cs
rename to tests/Data/TestCombinedFileStream.cs
index 9299f89d..e49f5365 100644
--- a/tests/Core/Settings/TestCombinedFileStream.cs
+++ b/tests/Data/TestCombinedFileStream.cs
@@ -1,9 +1,9 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
+using System;
 using System.IO;
-using TweetDuck.Core.Other.Settings.Export;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TweetDuck.Data;
 
-namespace UnitTests.Core.Settings{
+namespace UnitTests.Data{
     [TestClass]
     public class TestCombinedFileStream{
         [TestMethod]
diff --git a/tests/Core/Utils/TestCommandLineArgs.cs b/tests/Data/TestCommandLineArgs.cs
similarity index 97%
rename from tests/Core/Utils/TestCommandLineArgs.cs
rename to tests/Data/TestCommandLineArgs.cs
index 7d13acb4..70da4965 100644
--- a/tests/Core/Utils/TestCommandLineArgs.cs
+++ b/tests/Data/TestCommandLineArgs.cs
@@ -1,8 +1,8 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Collections.Generic;
-using TweetDuck.Core.Utils;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TweetDuck.Data;
 
-namespace UnitTests.Core.Utils{
+namespace UnitTests.Data{
     [TestClass]
     public class TestCommandLineArgs{
         [TestMethod]
diff --git a/tests/Core/Utils/TestInjectedHTML.cs b/tests/Data/TestInjectedHTML.cs
similarity index 98%
rename from tests/Core/Utils/TestInjectedHTML.cs
rename to tests/Data/TestInjectedHTML.cs
index 005fc38a..7add28df 100644
--- a/tests/Core/Utils/TestInjectedHTML.cs
+++ b/tests/Data/TestInjectedHTML.cs
@@ -2,9 +2,9 @@
 using System.Collections.Generic;
 using System.Linq;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 
-namespace UnitTests.Core.Utils{
+namespace UnitTests.Data{
     [TestClass]
     public class TestInjectedHTML{
         private static IEnumerable<InjectedHTML.Position> Positions => Enum.GetValues(typeof(InjectedHTML.Position)).Cast<InjectedHTML.Position>();
diff --git a/tests/Core/Utils/TestTwoKeyDictionary.cs b/tests/Data/TestTwoKeyDictionary.cs
similarity index 99%
rename from tests/Core/Utils/TestTwoKeyDictionary.cs
rename to tests/Data/TestTwoKeyDictionary.cs
index 142f8fb4..4a984ae1 100644
--- a/tests/Core/Utils/TestTwoKeyDictionary.cs
+++ b/tests/Data/TestTwoKeyDictionary.cs
@@ -1,10 +1,10 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
-using TweetDuck.Core.Utils;
-using System.Collections.Generic;
+using TweetDuck.Data;
 
-namespace UnitTests.Core.Utils{
+namespace UnitTests.Data{
     [TestClass]
     public class TestTwoKeyDictionary{
         private static TwoKeyDictionary<string, int, string> CreateDict(){

From 4c610ea32d529ff3fe4c101e4dbbc6e9ffa9179a Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Thu, 6 Jul 2017 20:58:40 +0200
Subject: [PATCH 05/17] Move TweetDeck URL into a constant

---
 Core/FormBrowser.cs                       | 2 +-
 Core/Notification/FormNotificationBase.cs | 4 ++--
 Core/Utils/BrowserUtils.cs                | 2 ++
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index 59e55395..b920d814 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -384,7 +384,7 @@ public void UpdateProperties(PropertyBridge.Properties properties){
         }
 
         public void ReloadToTweetDeck(){
-            browser.ExecuteScriptAsync("window.location.href = 'https://tweetdeck.twitter.com'");
+            browser.ExecuteScriptAsync($"window.location.href = '{BrowserUtils.TweetDeckURL}'");
         }
 
         // callback handlers
diff --git a/Core/Notification/FormNotificationBase.cs b/Core/Notification/FormNotificationBase.cs
index 81a3bdb5..92ef43d3 100644
--- a/Core/Notification/FormNotificationBase.cs
+++ b/Core/Notification/FormNotificationBase.cs
@@ -111,7 +111,7 @@ public FormNotificationBase(Form owner, bool enableContextMenu){
             this.dpiScale = this.GetDPIScale();
 
             DefaultResourceHandlerFactory handlerFactory = (DefaultResourceHandlerFactory)browser.ResourceHandlerFactory;
-            handlerFactory.RegisterHandler("https://tweetdeck.twitter.com", this.resourceHandler);
+            handlerFactory.RegisterHandler(BrowserUtils.TweetDeckURL, this.resourceHandler);
 
             Controls.Add(browser);
 
@@ -180,7 +180,7 @@ protected virtual void LoadTweet(TweetNotification tweet){
             currentColumn = tweet.Column;
 
             resourceHandler.SetHTML(GetTweetHTML(tweet));
-            browser.Load("https://tweetdeck.twitter.com");
+            browser.Load(BrowserUtils.TweetDeckURL);
         }
 
         protected virtual void SetNotificationSize(int width, int height){
diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index 22bbfb5a..6a85e986 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -10,6 +10,8 @@
 
 namespace TweetDuck.Core.Utils{
     static class BrowserUtils{
+        public const string TweetDeckURL = "https://tweetdeck.twitter.com";
+
         public static string HeaderAcceptLanguage{
             get{
                 string culture = CultureInfo.CurrentCulture.Name;

From 71b306d5fd4782a70aef09782171a2be7e9cce46 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Thu, 6 Jul 2017 21:26:43 +0200
Subject: [PATCH 06/17] Fix unit test project file after refactoring

---
 tests/UnitTests.csproj | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/tests/UnitTests.csproj b/tests/UnitTests.csproj
index 2e67154b..e017293a 100644
--- a/tests/UnitTests.csproj
+++ b/tests/UnitTests.csproj
@@ -47,12 +47,12 @@
     </Otherwise>
   </Choose>
   <ItemGroup>
-    <Compile Include="Core\Settings\TestCombinedFileStream.cs" />
-    <Compile Include="Core\Utils\TestBrowserUtils.cs" />
-    <Compile Include="Core\Utils\TestCommandLineArgs.cs" />
-    <Compile Include="Core\Utils\TestCommandLineArgsParser.cs" />
-    <Compile Include="Core\Utils\TestInjectedHTML.cs" />
-    <Compile Include="Core\Utils\TestTwoKeyDictionary.cs" />
+    <Compile Include="Data\TestCombinedFileStream.cs" />
+    <Compile Include="Core\TestBrowserUtils.cs" />
+    <Compile Include="Data\TestCommandLineArgs.cs" />
+    <Compile Include="Core\TestCommandLineArgsParser.cs" />
+    <Compile Include="Data\TestInjectedHTML.cs" />
+    <Compile Include="Data\TestTwoKeyDictionary.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="TestUtils.cs" />
   </ItemGroup>

From 796fb348a3b584e037f51e14dc17b7f3d366c523 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 00:48:00 +0200
Subject: [PATCH 07/17] Add classes for serializing objects to/from text files

---
 Data/Serialization/FileSerializer.cs    | 129 ++++++++++++++++++++++++
 Data/Serialization/ISerializedObject.cs |   5 +
 Data/Serialization/ITypeConverter.cs    |   8 ++
 TweetDuck.csproj                        |   3 +
 tests/Data/TestFileSerializer.cs        |  45 +++++++++
 tests/UnitTests.csproj                  |   1 +
 6 files changed, 191 insertions(+)
 create mode 100644 Data/Serialization/FileSerializer.cs
 create mode 100644 Data/Serialization/ISerializedObject.cs
 create mode 100644 Data/Serialization/ITypeConverter.cs
 create mode 100644 tests/Data/TestFileSerializer.cs

diff --git a/Data/Serialization/FileSerializer.cs b/Data/Serialization/FileSerializer.cs
new file mode 100644
index 00000000..11317da0
--- /dev/null
+++ b/Data/Serialization/FileSerializer.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+
+namespace TweetDuck.Data.Serialization{
+    class FileSerializer<T> where T : ISerializedObject{
+        private const string NewLineReal = "\r\n";
+        private const string NewLineCustom = "\r~\n";
+
+        private static readonly ITypeConverter BasicSerializerObj = new BasicSerializer();
+        
+        private readonly Dictionary<string, PropertyInfo> props;
+        private readonly Dictionary<Type, ITypeConverter> serializers;
+
+        public FileSerializer(){
+            this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.CanWrite).ToDictionary(prop => prop.Name);
+            this.serializers = new Dictionary<Type, ITypeConverter>();
+        }
+
+        public void RegisterSerializer(Type type, ITypeConverter serializer){
+            serializers[type] = serializer;
+        }
+
+        public void Write(string file, T obj){
+            using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
+                foreach(KeyValuePair<string, PropertyInfo> prop in props){
+                    Type type = prop.Value.PropertyType;
+                    object value = prop.Value.GetValue(obj);
+                    
+                    if (!serializers.TryGetValue(type, out ITypeConverter serializer)) {
+                        serializer = BasicSerializerObj;
+                    }
+
+                    if (serializer.TryWriteType(type, value, out string converted)){
+                        if (converted != null){
+                            writer.Write($"{prop.Key} {converted.Replace(Environment.NewLine, NewLineCustom)}");
+                            writer.Write(NewLineReal);
+                        }
+                    }
+                    else{
+                        throw new SerializationException($"Invalid serialization type, conversion failed for: {type}");
+                    }
+                }
+            }
+        }
+
+        public void Read(string file, T obj){
+            using(StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))){
+                foreach(string line in reader.ReadToEnd().Split(new string[]{ NewLineReal }, StringSplitOptions.RemoveEmptyEntries)){
+                    int space = line.IndexOf(' ');
+
+                    if (space == -1){
+                        throw new SerializationException($"Invalid file format, missing separator: {line}");
+                    }
+
+                    string property = line.Substring(0, space);
+                    string value = line.Substring(space+1).Replace(NewLineCustom, Environment.NewLine);
+
+                    if (props.TryGetValue(property, out PropertyInfo info)){
+                        if (!serializers.TryGetValue(info.PropertyType, out ITypeConverter serializer)) {
+                            serializer = BasicSerializerObj;
+                        }
+
+                        if (serializer.TryReadType(info.PropertyType, value, out object converted)){
+                            info.SetValue(obj, converted);
+                        }
+                        else{
+                            throw new SerializationException($"Invalid file format, cannot convert value: {value} (property: {property})");
+                        }
+                    }
+                    else if (!obj.OnReadUnknownProperty(property, value)){
+                        throw new SerializationException($"Invalid file format, unknown property: {property}+");
+                    }
+                }
+            }
+        }
+
+        private class BasicSerializer : 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;
+                }
+            }
+        }
+    }
+}
diff --git a/Data/Serialization/ISerializedObject.cs b/Data/Serialization/ISerializedObject.cs
new file mode 100644
index 00000000..3d538583
--- /dev/null
+++ b/Data/Serialization/ISerializedObject.cs
@@ -0,0 +1,5 @@
+namespace TweetDuck.Data.Serialization{
+    interface ISerializedObject{
+        bool OnReadUnknownProperty(string property, string value);
+    }
+}
diff --git a/Data/Serialization/ITypeConverter.cs b/Data/Serialization/ITypeConverter.cs
new file mode 100644
index 00000000..67b2126f
--- /dev/null
+++ b/Data/Serialization/ITypeConverter.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace TweetDuck.Data.Serialization{
+    interface ITypeConverter{
+        bool TryWriteType(Type type, object value, out string converted);
+        bool TryReadType(Type type, string value, out object converted);
+    }
+}
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index bab4a95c..9376e6bd 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -201,7 +201,10 @@
       <SubType>Form</SubType>
     </Compile>
     <Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
+    <Compile Include="Data\Serialization\FileSerializer.cs" />
     <Compile Include="Data\InjectedHTML.cs" />
+    <Compile Include="Data\Serialization\ISerializedObject.cs" />
+    <Compile Include="Data\Serialization\ITypeConverter.cs" />
     <Compile Include="Data\TwoKeyDictionary.cs" />
     <Compile Include="Data\WindowState.cs" />
     <Compile Include="Core\Utils\WindowsUtils.cs" />
diff --git a/tests/Data/TestFileSerializer.cs b/tests/Data/TestFileSerializer.cs
new file mode 100644
index 00000000..5454b2b5
--- /dev/null
+++ b/tests/Data/TestFileSerializer.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TweetDuck.Data.Serialization;
+
+namespace UnitTests.Data{
+    [TestClass]
+    public class TestFileSerializer{
+        private class SerializationTestBasic : ISerializedObject{
+            public bool TestBool { get; set; }
+            public int TestInt { get; set; }
+            public string TestString { get; set; }
+            public string TestStringNull { get; set; }
+
+            bool ISerializedObject.OnReadUnknownProperty(string property, string value){
+                return false;
+            }
+        }
+
+        [TestMethod]
+        public void TestBasicWriteRead(){
+            FileSerializer<SerializationTestBasic> serializer = new FileSerializer<SerializationTestBasic>();
+            
+            SerializationTestBasic write = new SerializationTestBasic{
+                TestBool = true,
+                TestInt = -100,
+                TestString = "abc"+Environment.NewLine+"def",
+                TestStringNull = null
+            };
+
+            serializer.Write("serialized_basic", write);
+
+            Assert.IsTrue(File.Exists("serialized_basic"));
+            TestUtils.DeleteFileOnExit("serialized_basic");
+
+            SerializationTestBasic read = new SerializationTestBasic();
+            serializer.Read("serialized_basic", read);
+
+            Assert.IsTrue(read.TestBool);
+            Assert.AreEqual(-100, read.TestInt);
+            Assert.AreEqual("abc"+Environment.NewLine+"def", read.TestString);
+            Assert.IsNull(read.TestStringNull);
+        }
+    }
+}
diff --git a/tests/UnitTests.csproj b/tests/UnitTests.csproj
index e017293a..dfb7360d 100644
--- a/tests/UnitTests.csproj
+++ b/tests/UnitTests.csproj
@@ -51,6 +51,7 @@
     <Compile Include="Core\TestBrowserUtils.cs" />
     <Compile Include="Data\TestCommandLineArgs.cs" />
     <Compile Include="Core\TestCommandLineArgsParser.cs" />
+    <Compile Include="Data\TestFileSerializer.cs" />
     <Compile Include="Data\TestInjectedHTML.cs" />
     <Compile Include="Data\TestTwoKeyDictionary.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />

From 38c2781cd38a539e9718c678c7eb102f3c1b5ace Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 00:53:19 +0200
Subject: [PATCH 08/17] Add an enum test to FileSerializer unit test

---
 tests/Data/TestFileSerializer.cs | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/tests/Data/TestFileSerializer.cs b/tests/Data/TestFileSerializer.cs
index 5454b2b5..231fc943 100644
--- a/tests/Data/TestFileSerializer.cs
+++ b/tests/Data/TestFileSerializer.cs
@@ -6,11 +6,16 @@
 namespace UnitTests.Data{
     [TestClass]
     public class TestFileSerializer{
+        private enum TestEnum{
+            A, B, C, D, E
+        }
+
         private class SerializationTestBasic : ISerializedObject{
             public bool TestBool { get; set; }
             public int TestInt { get; set; }
             public string TestString { get; set; }
             public string TestStringNull { get; set; }
+            public TestEnum TestEnum { get; set; }
 
             bool ISerializedObject.OnReadUnknownProperty(string property, string value){
                 return false;
@@ -25,7 +30,8 @@ public void TestBasicWriteRead(){
                 TestBool = true,
                 TestInt = -100,
                 TestString = "abc"+Environment.NewLine+"def",
-                TestStringNull = null
+                TestStringNull = null,
+                TestEnum = TestEnum.D
             };
 
             serializer.Write("serialized_basic", write);
@@ -40,6 +46,9 @@ public void TestBasicWriteRead(){
             Assert.AreEqual(-100, read.TestInt);
             Assert.AreEqual("abc"+Environment.NewLine+"def", read.TestString);
             Assert.IsNull(read.TestStringNull);
+            Assert.AreEqual(TestEnum.D, read.TestEnum);
         }
+
+        // TODO more complex tests
     }
 }

From d431b63c27893d143e0f99ec5169f9e98d77b1e9 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 01:47:14 +0200
Subject: [PATCH 09/17] Add SingleTypeConverter and update names in
 FileSerializer

---
 Data/Serialization/FileSerializer.cs      | 12 ++++-----
 Data/Serialization/SingleTypeConverter.cs | 31 +++++++++++++++++++++++
 TweetDuck.csproj                          |  1 +
 3 files changed, 38 insertions(+), 6 deletions(-)
 create mode 100644 Data/Serialization/SingleTypeConverter.cs

diff --git a/Data/Serialization/FileSerializer.cs b/Data/Serialization/FileSerializer.cs
index 11317da0..97bef964 100644
--- a/Data/Serialization/FileSerializer.cs
+++ b/Data/Serialization/FileSerializer.cs
@@ -13,15 +13,15 @@ class FileSerializer<T> where T : ISerializedObject{
         private static readonly ITypeConverter BasicSerializerObj = new BasicSerializer();
         
         private readonly Dictionary<string, PropertyInfo> props;
-        private readonly Dictionary<Type, ITypeConverter> serializers;
+        private readonly Dictionary<Type, ITypeConverter> converters;
 
         public FileSerializer(){
             this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.CanWrite).ToDictionary(prop => prop.Name);
-            this.serializers = new Dictionary<Type, ITypeConverter>();
+            this.converters = new Dictionary<Type, ITypeConverter>();
         }
 
-        public void RegisterSerializer(Type type, ITypeConverter serializer){
-            serializers[type] = serializer;
+        public void RegisterTypeConverter(Type type, ITypeConverter converter){
+            converters[type] = converter;
         }
 
         public void Write(string file, T obj){
@@ -30,7 +30,7 @@ public void Write(string file, T obj){
                     Type type = prop.Value.PropertyType;
                     object value = prop.Value.GetValue(obj);
                     
-                    if (!serializers.TryGetValue(type, out ITypeConverter serializer)) {
+                    if (!converters.TryGetValue(type, out ITypeConverter serializer)) {
                         serializer = BasicSerializerObj;
                     }
 
@@ -60,7 +60,7 @@ public void Read(string file, T obj){
                     string value = line.Substring(space+1).Replace(NewLineCustom, Environment.NewLine);
 
                     if (props.TryGetValue(property, out PropertyInfo info)){
-                        if (!serializers.TryGetValue(info.PropertyType, out ITypeConverter serializer)) {
+                        if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)) {
                             serializer = BasicSerializerObj;
                         }
 
diff --git a/Data/Serialization/SingleTypeConverter.cs b/Data/Serialization/SingleTypeConverter.cs
new file mode 100644
index 00000000..26a075c0
--- /dev/null
+++ b/Data/Serialization/SingleTypeConverter.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace TweetDuck.Data.Serialization{
+    class SingleTypeConverter<T> : ITypeConverter{
+        public delegate string FuncConvertToString<U>(U value);
+        public delegate U FuncConvertToObject<U>(string value);
+
+        public FuncConvertToString<T> ConvertToString { get; set; }
+        public FuncConvertToObject<T> ConvertToObject { get; set; }
+
+        bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
+            try{
+                converted = ConvertToString((T)value);
+                return true;
+            }catch{
+                converted = null;
+                return false;
+            }
+        }
+
+        bool ITypeConverter.TryReadType(Type type, string value, out object converted){
+            try{
+                converted = ConvertToObject(value);
+                return true;
+            }catch{
+                converted = null;
+                return false;
+            }
+        }
+    }
+}
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 9376e6bd..718dc6ed 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -205,6 +205,7 @@
     <Compile Include="Data\InjectedHTML.cs" />
     <Compile Include="Data\Serialization\ISerializedObject.cs" />
     <Compile Include="Data\Serialization\ITypeConverter.cs" />
+    <Compile Include="Data\Serialization\SingleTypeConverter.cs" />
     <Compile Include="Data\TwoKeyDictionary.cs" />
     <Compile Include="Data\WindowState.cs" />
     <Compile Include="Core\Utils\WindowsUtils.cs" />

From 424c0e596ca4a18de17095c6c5098abb972209b3 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 02:56:02 +0200
Subject: [PATCH 10/17] Add legacy config detection and replace UserConfig
 serialization with FileSerializer

---
 Configuration/UserConfig.cs          | 142 +++++-------------
 Configuration/UserConfigLegacy.cs    | 210 +++++++++++++++++++++++++++
 Data/Serialization/FileSerializer.cs |   4 +
 Data/WindowState.cs                  |  16 +-
 TweetDuck.csproj                     |   1 +
 5 files changed, 269 insertions(+), 104 deletions(-)
 create mode 100644 Configuration/UserConfigLegacy.cs

diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 7a47c1c8..fc54aabf 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -1,26 +1,37 @@
 using System;
 using System.Drawing;
 using System.IO;
-using System.Runtime.Serialization;
-using System.Runtime.Serialization.Formatters.Binary;
+using System.Linq;
 using TweetDuck.Core;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Notification;
 using TweetDuck.Data;
+using TweetDuck.Data.Serialization;
 
 namespace TweetDuck.Configuration{
-    [Serializable]
-    sealed class UserConfig{
-        private static readonly IFormatter Formatter = new BinaryFormatter{ Binder = new LegacyBinder() };
+    sealed class UserConfig : ISerializedObject{
+        private static readonly FileSerializer<UserConfig> Serializer = new FileSerializer<UserConfig>();
 
-        private class LegacyBinder : SerializationBinder{
-            public override Type BindToType(string assemblyName, string typeName){
-                return Type.GetType(string.Format("{0}, {1}", typeName.Replace("TweetDck", "TweetDuck"), assemblyName.Replace("TweetDck", "TweetDuck")));
-            }
+        static UserConfig(){
+            Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
+
+            Serializer.RegisterTypeConverter(typeof(Point), new SingleTypeConverter<Point>{
+                ConvertToString = value => $"{value.X} {value.Y}",
+                ConvertToObject = value => {
+                    int[] elements = value.Split(' ').Select(int.Parse).ToArray();
+                    return new Point(elements[0], elements[1]);
+                }
+            });
+            
+            Serializer.RegisterTypeConverter(typeof(Size), new SingleTypeConverter<Size>{
+                ConvertToString = value => $"{value.Width} {value.Height}",
+                ConvertToObject = value => {
+                    int[] elements = value.Split(' ').Select(int.Parse).ToArray();
+                    return new Size(elements[0], elements[1]);
+                }
+            });
         }
-
-        private const int CurrentFileVersion = 11;
-
+        
         // START OF CONFIGURATION
 
         public WindowState BrowserWindow { get; set; }
@@ -101,25 +112,18 @@ public TrayIcon.Behavior TrayBehavior{
 
         // END OF CONFIGURATION
         
-        [field:NonSerialized]
         public event EventHandler MuteToggled;
-        
-        [field:NonSerialized]
         public event EventHandler ZoomLevelChanged;
-        
-        [field:NonSerialized]
         public event EventHandler TrayBehaviorChanged;
-
-        [NonSerialized]
-        private string file;
-
-        private int fileVersion;
+        
+        private readonly string file;
+        
         private bool muteNotifications;
         private int zoomLevel;
         private string notificationSoundPath;
         private TrayIcon.Behavior trayBehavior;
 
-        private UserConfig(string file){
+        public UserConfig(string file){ // TODO make private after removing UserConfigLegacy
             this.file = file;
 
             BrowserWindow = new WindowState();
@@ -139,71 +143,8 @@ private UserConfig(string file){
             PluginsWindow = new WindowState();
         }
 
-        private void UpgradeFile(){
-            if (fileVersion == CurrentFileVersion){
-                return;
-            }
-
-            // if outdated, cycle through all versions
-            if (fileVersion == 0){
-                DisplayNotificationTimer = true;
-                EnableUpdateCheck = true;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 1){
-                ExpandLinksOnHover = true;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 2){
-                BrowserWindow = new WindowState();
-                PluginsWindow = new WindowState();
-                ++fileVersion;
-            }
-
-            if (fileVersion == 3){
-                EnableTrayHighlight = true;
-                NotificationDurationValue = 25;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 4){
-                ++fileVersion;
-            }
-
-            if (fileVersion == 5){
-                ++fileVersion;
-            }
-
-            if (fileVersion == 6){
-                NotificationNonIntrusiveMode = true;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 7){
-                ZoomLevel = 100;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 8){
-                SwitchAccountSelectors = true;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 9){
-                NotificationScrollSpeed = 100;
-                ++fileVersion;
-            }
-
-            if (fileVersion == 10){
-                NotificationSize = TweetNotification.Size.Auto;
-                ++fileVersion;
-            }
-
-            // update the version
-            fileVersion = CurrentFileVersion;
-            Save();
+        bool ISerializedObject.OnReadUnknownProperty(string property, string value){
+            return false;
         }
 
         public bool Save(){
@@ -219,10 +160,7 @@ public bool Save(){
                     File.Move(file, backupFile);
                 }
 
-                using(Stream stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None)){
-                    Formatter.Serialize(stream, this);
-                }
-
+                Serializer.Write(file, this);
                 return true;
             }catch(Exception e){
                 Program.Reporter.HandleException("Configuration Error", "Could not save the configuration file.", true, e);
@@ -231,22 +169,20 @@ public bool Save(){
         }
         
         public static UserConfig Load(string file){
-            UserConfig config = null;
             Exception firstException = null;
 
             for(int attempt = 0; attempt < 2; attempt++){
                 try{
-                    using(Stream stream = new FileStream(attempt == 0 ? file : GetBackupFile(file), FileMode.Open, FileAccess.Read, FileShare.Read)){
-                        if ((config = Formatter.Deserialize(stream) as UserConfig) != null){
-                            config.file = file;
-                        }
-                    }
-
-                    config?.UpgradeFile();
-                    break;
+                    UserConfig config = new UserConfig(file);
+                    Serializer.Read(attempt == 0 ? file : GetBackupFile(file), config);
+                    return config;
                 }catch(FileNotFoundException){
                 }catch(DirectoryNotFoundException){
                     break;
+                }catch(FormatException){
+                    UserConfig config = UserConfigLegacy.Load(file);
+                    config.Save();
+                    return config;
                 }catch(Exception e){
                     if (attempt == 0){
                         firstException = e;
@@ -258,11 +194,11 @@ public static UserConfig Load(string file){
                 }
             }
 
-            if (firstException != null && config == null){
+            if (firstException != null){
                 Program.Reporter.HandleException("Configuration Error", "Could not open the configuration file.", true, firstException);
             }
 
-            return config ?? new UserConfig(file);
+            return new UserConfig(file);
         }
 
         public static string GetBackupFile(string file){
diff --git a/Configuration/UserConfigLegacy.cs b/Configuration/UserConfigLegacy.cs
new file mode 100644
index 00000000..ec8218db
--- /dev/null
+++ b/Configuration/UserConfigLegacy.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Drawing;
+using System.IO;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters.Binary;
+using TweetDuck.Core;
+using TweetDuck.Core.Controls;
+using TweetDuck.Core.Notification;
+using TweetDuck.Data;
+
+namespace TweetDuck.Configuration{
+    [Serializable]
+    sealed class UserConfigLegacy{ // TODO remove eventually
+        private static readonly IFormatter Formatter = new BinaryFormatter{ Binder = new LegacyBinder() };
+
+        private class LegacyBinder : SerializationBinder{
+            public override Type BindToType(string assemblyName, string typeName){
+                return Type.GetType(string.Format("{0}, {1}", typeName.Replace("TweetDck", "TweetDuck").Replace(".UserConfig", ".UserConfigLegacy").Replace("Core.Utils.WindowState", "Data.WindowState"), assemblyName.Replace("TweetDck", "TweetDuck")));
+            }
+        }
+        
+        private const int CurrentFileVersion = 11;
+
+        // START OF CONFIGURATION
+
+        public WindowState BrowserWindow { get; set; }
+        public WindowState PluginsWindow { get; set; }
+
+        public bool DisplayNotificationColumn { get; set; }
+        public bool DisplayNotificationTimer { get; set; }
+        public bool NotificationTimerCountDown { get; set; }
+        public bool NotificationSkipOnLinkClick { get; set; }
+        public bool NotificationNonIntrusiveMode { get; set; }
+
+        public int NotificationIdlePauseSeconds { get; set; }
+        public int NotificationDurationValue { get; set; }
+        public int NotificationScrollSpeed { get; set; }
+
+        public TweetNotification.Position NotificationPosition { get; set; }
+        public Point CustomNotificationPosition { get; set; }
+        public int NotificationEdgeDistance { get; set; }
+        public int NotificationDisplay { get; set; }
+
+        public TweetNotification.Size NotificationSize { get; set; }
+        public Size CustomNotificationSize { get; set; }
+
+        public bool EnableSpellCheck { get; set; }
+        public bool ExpandLinksOnHover { get; set; }
+        public bool SwitchAccountSelectors { get; set; }
+        public bool EnableTrayHighlight { get; set; }
+
+        public bool EnableUpdateCheck { get; set; }
+        public string DismissedUpdate { get; set; }
+
+        public string CustomCefArgs { get; set; }
+        public string CustomBrowserCSS { get; set; }
+        public string CustomNotificationCSS { get; set; }
+        
+        public string NotificationSoundPath{
+            get => string.IsNullOrEmpty(notificationSoundPath) ? string.Empty : notificationSoundPath;
+            set => notificationSoundPath = value;
+        }
+
+        public bool MuteNotifications{
+            get => muteNotifications;
+            set => muteNotifications = value;
+        }
+
+        public int ZoomLevel{
+            get => zoomLevel;
+            set => zoomLevel = value;
+        }
+
+        public TrayIcon.Behavior TrayBehavior{
+            get => trayBehavior;
+            set => trayBehavior = value;
+        }
+
+        // END OF CONFIGURATION
+        
+        [NonSerialized]
+        private string file;
+
+        private int fileVersion;
+        private bool muteNotifications;
+        private int zoomLevel;
+        private string notificationSoundPath;
+        private TrayIcon.Behavior trayBehavior;
+
+        private UserConfigLegacy(string file){
+            this.file = file;
+
+            BrowserWindow = new WindowState();
+            ZoomLevel = 100;
+            DisplayNotificationTimer = true;
+            NotificationNonIntrusiveMode = true;
+            NotificationPosition = TweetNotification.Position.TopRight;
+            CustomNotificationPosition = ControlExtensions.InvisibleLocation;
+            NotificationSize = TweetNotification.Size.Auto;
+            NotificationEdgeDistance = 8;
+            NotificationDurationValue = 25;
+            NotificationScrollSpeed = 100;
+            EnableUpdateCheck = true;
+            ExpandLinksOnHover = true;
+            SwitchAccountSelectors = true;
+            EnableTrayHighlight = true;
+            PluginsWindow = new WindowState();
+        }
+
+        private void UpgradeFile(){
+            if (fileVersion == CurrentFileVersion){
+                return;
+            }
+
+            // if outdated, cycle through all versions
+            if (fileVersion <= 5){
+                DisplayNotificationTimer = true;
+                EnableUpdateCheck = true;
+                ExpandLinksOnHover = true;
+                BrowserWindow = new WindowState();
+                PluginsWindow = new WindowState();
+                EnableTrayHighlight = true;
+                NotificationDurationValue = 25;
+                fileVersion = 6;
+            }
+
+            if (fileVersion == 6){
+                NotificationNonIntrusiveMode = true;
+                ++fileVersion;
+            }
+
+            if (fileVersion == 7){
+                ZoomLevel = 100;
+                ++fileVersion;
+            }
+
+            if (fileVersion == 8){
+                SwitchAccountSelectors = true;
+                ++fileVersion;
+            }
+
+            if (fileVersion == 9){
+                NotificationScrollSpeed = 100;
+                ++fileVersion;
+            }
+
+            if (fileVersion == 10){
+                NotificationSize = TweetNotification.Size.Auto;
+                ++fileVersion;
+            }
+
+            // update the version
+            fileVersion = CurrentFileVersion;
+        }
+
+        public UserConfig ConvertLegacy(){
+            return new UserConfig(file){
+                BrowserWindow = BrowserWindow,
+                PluginsWindow = PluginsWindow,
+                DisplayNotificationColumn = DisplayNotificationColumn,
+                DisplayNotificationTimer = DisplayNotificationTimer,
+                NotificationTimerCountDown = NotificationTimerCountDown,
+                NotificationSkipOnLinkClick = NotificationSkipOnLinkClick,
+                NotificationNonIntrusiveMode = NotificationNonIntrusiveMode,
+                NotificationIdlePauseSeconds = NotificationIdlePauseSeconds,
+                NotificationDurationValue = NotificationDurationValue,
+                NotificationScrollSpeed = NotificationScrollSpeed,
+                NotificationPosition = NotificationPosition,
+                CustomNotificationPosition = CustomNotificationPosition,
+                NotificationEdgeDistance = NotificationEdgeDistance,
+                NotificationDisplay = NotificationDisplay,
+                NotificationSize = NotificationSize,
+                CustomNotificationSize = CustomNotificationSize,
+                EnableSpellCheck = EnableSpellCheck,
+                ExpandLinksOnHover = ExpandLinksOnHover,
+                SwitchAccountSelectors = SwitchAccountSelectors,
+                EnableTrayHighlight = EnableTrayHighlight,
+                EnableUpdateCheck = EnableUpdateCheck,
+                DismissedUpdate = DismissedUpdate,
+                CustomCefArgs = CustomCefArgs,
+                CustomBrowserCSS = CustomBrowserCSS,
+                CustomNotificationCSS = CustomNotificationCSS,
+                NotificationSoundPath = NotificationSoundPath,
+                MuteNotifications = MuteNotifications,
+                ZoomLevel = ZoomLevel,
+                TrayBehavior = TrayBehavior
+            };
+        }
+        
+        public static UserConfig Load(string file){
+            UserConfigLegacy config = null;
+            
+            try{
+                using(Stream stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)){
+                    if ((config = Formatter.Deserialize(stream) as UserConfigLegacy) != null){
+                        config.file = file;
+                    }
+                }
+
+                config?.UpgradeFile();
+            }catch(FileNotFoundException){
+            }catch(DirectoryNotFoundException){
+            }catch(Exception e){
+                Program.Reporter.HandleException("Configuration Error", "Could not open the configuration file.", true, e);
+            }
+
+            return (config ?? new UserConfigLegacy(file)).ConvertLegacy();
+        }
+    }
+}
diff --git a/Data/Serialization/FileSerializer.cs b/Data/Serialization/FileSerializer.cs
index 97bef964..9a00ce7b 100644
--- a/Data/Serialization/FileSerializer.cs
+++ b/Data/Serialization/FileSerializer.cs
@@ -49,6 +49,10 @@ public void Write(string file, T obj){
 
         public void Read(string file, T obj){
             using(StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))){
+                if (reader.Peek() == 0){
+                    throw new FormatException("Input appears to be a binary file.");
+                }
+
                 foreach(string line in reader.ReadToEnd().Split(new string[]{ NewLineReal }, StringSplitOptions.RemoveEmptyEntries)){
                     int space = line.IndexOf(' ');
 
diff --git a/Data/WindowState.cs b/Data/WindowState.cs
index a29efac5..b8ece111 100644
--- a/Data/WindowState.cs
+++ b/Data/WindowState.cs
@@ -1,10 +1,12 @@
 using System;
 using System.Drawing;
+using System.Linq;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
+using TweetDuck.Data.Serialization;
 
 namespace TweetDuck.Data{
-    [Serializable]
+    [Serializable] // TODO remove attribute with UserConfigLegacy
     class WindowState{
         private Rectangle rect;
         private bool isMaximized;
@@ -26,5 +28,17 @@ public void Restore(Form form, bool firstTimeFullscreen){
                 Save(form);
             }
         }
+
+        public static readonly SingleTypeConverter<WindowState> Converter = new SingleTypeConverter<WindowState>{
+            ConvertToString = value => $"{(value.isMaximized ? 'M' : '_')}{value.rect.X} {value.rect.Y} {value.rect.Width} {value.rect.Height}",
+            ConvertToObject = value => {
+                int[] elements = value.Substring(1).Split(' ').Select(int.Parse).ToArray();
+                
+                return new WindowState{
+                    rect = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
+                    isMaximized = value[0] == 'M'
+                };
+            }
+        };
     }
 }
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 718dc6ed..90560f96 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -73,6 +73,7 @@
     <Compile Include="Configuration\LockManager.cs" />
     <Compile Include="Configuration\SystemConfig.cs" />
     <Compile Include="Configuration\UserConfig.cs" />
+    <Compile Include="Configuration\UserConfigLegacy.cs" />
     <Compile Include="Core\Bridge\PropertyBridge.cs" />
     <Compile Include="Core\Controls\ControlExtensions.cs" />
     <Compile Include="Core\Controls\FlatButton.cs">

From 5a21d2cb104833888493b95de8b518c30dbbc7c2 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 15:52:13 +0200
Subject: [PATCH 11/17] Add StringUtils with unit tests and use it

---
 Configuration/UserConfig.cs      |  5 +++--
 Core/Handling/ContextMenuBase.cs |  6 +-----
 Core/Utils/BrowserUtils.cs       |  7 +------
 Core/Utils/StringUtils.cs        | 20 ++++++++++++++++++
 Data/CombinedFileStream.cs       |  7 +++----
 Data/WindowState.cs              |  4 ++--
 TweetDuck.csproj                 |  1 +
 tests/Core/TestBrowserUtils.cs   | 10 ---------
 tests/Core/TestStringUtils.cs    | 36 ++++++++++++++++++++++++++++++++
 tests/UnitTests.csproj           |  1 +
 10 files changed, 68 insertions(+), 29 deletions(-)
 create mode 100644 Core/Utils/StringUtils.cs
 create mode 100644 tests/Core/TestStringUtils.cs

diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index fc54aabf..53ae9a68 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -5,6 +5,7 @@
 using TweetDuck.Core;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Notification;
+using TweetDuck.Core.Utils;
 using TweetDuck.Data;
 using TweetDuck.Data.Serialization;
 
@@ -18,7 +19,7 @@ static UserConfig(){
             Serializer.RegisterTypeConverter(typeof(Point), new SingleTypeConverter<Point>{
                 ConvertToString = value => $"{value.X} {value.Y}",
                 ConvertToObject = value => {
-                    int[] elements = value.Split(' ').Select(int.Parse).ToArray();
+                    int[] elements = StringUtils.ParseInts(value, ' ');
                     return new Point(elements[0], elements[1]);
                 }
             });
@@ -26,7 +27,7 @@ static UserConfig(){
             Serializer.RegisterTypeConverter(typeof(Size), new SingleTypeConverter<Size>{
                 ConvertToString = value => $"{value.Width} {value.Height}",
                 ConvertToObject = value => {
-                    int[] elements = value.Split(' ').Select(int.Parse).ToArray();
+                    int[] elements = StringUtils.ParseInts(value, ' ');
                     return new Size(elements[0], elements[1]);
                 }
             });
diff --git a/Core/Handling/ContextMenuBase.cs b/Core/Handling/ContextMenuBase.cs
index 05325c07..beaa5248 100644
--- a/Core/Handling/ContextMenuBase.cs
+++ b/Core/Handling/ContextMenuBase.cs
@@ -134,11 +134,7 @@ private static string GetImageFileName(string url){
             int dot = url.LastIndexOf('.');
 
             if (dot != -1){
-                int colon = url.IndexOf(':', dot);
-            
-                if (colon != -1){
-                    url = url.Substring(0, colon);
-                }
+                url = StringUtils.ExtractBefore(url, ':', dot);
             }
 
             // return file name
diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index 6a85e986..9cfe5b82 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -5,7 +5,6 @@
 using System.Globalization;
 using System.IO;
 using System.Net;
-using System.Text.RegularExpressions;
 using System.Windows.Forms;
 
 namespace TweetDuck.Core.Utils{
@@ -63,12 +62,8 @@ public static string GetFileNameFromUrl(string url){
             return string.IsNullOrEmpty(file) ? null : file;
         }
 
-        public static string ConvertPascalCaseToScreamingSnakeCase(string str){
-            return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpperInvariant();
-        }
-
         public static string GetErrorName(CefErrorCode code){
-            return ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
+            return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
         }
 
         public static WebClient DownloadFileAsync(string url, string target, Action onSuccess, Action<Exception> onFailure){
diff --git a/Core/Utils/StringUtils.cs b/Core/Utils/StringUtils.cs
new file mode 100644
index 00000000..53cd89de
--- /dev/null
+++ b/Core/Utils/StringUtils.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace TweetDuck.Core.Utils{
+    static class StringUtils{
+        public static string ExtractBefore(string str, char search, int startIndex = 0){
+            int index = str.IndexOf(search, startIndex);
+            return index == -1 ? str : str.Substring(0, index);
+        }
+
+        public static int[] ParseInts(string str, char separator){
+            return str.Split(new char[]{ separator }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
+        }
+        
+        public static string ConvertPascalCaseToScreamingSnakeCase(string str){
+            return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpperInvariant();
+        }
+    }
+}
diff --git a/Data/CombinedFileStream.cs b/Data/CombinedFileStream.cs
index 48fab228..061f4e59 100644
--- a/Data/CombinedFileStream.cs
+++ b/Data/CombinedFileStream.cs
@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using System.Text;
+using TweetDuck.Core.Utils;
 
 namespace TweetDuck.Data{
     class CombinedFileStream : IDisposable{
@@ -79,8 +80,7 @@ public string SkipFile(){
             stream.Position += BitConverter.ToInt32(contentLength, 0);
 
             string keyName = Encoding.UTF8.GetString(name);
-            int separatorIndex = keyName.IndexOf(KeySeparator);
-            return separatorIndex == -1 ? keyName : keyName.Substring(0, separatorIndex);
+            return StringUtils.ExtractBefore(keyName, KeySeparator);
         }
 
         public void Flush(){
@@ -96,8 +96,7 @@ public class Entry{
 
             public string KeyName{
                 get{
-                    int index = Identifier.IndexOf(KeySeparator);
-                    return index == -1 ? Identifier : Identifier.Substring(0, index);
+                    return StringUtils.ExtractBefore(Identifier, KeySeparator);
                 }
             }
 
diff --git a/Data/WindowState.cs b/Data/WindowState.cs
index b8ece111..ce8b8efe 100644
--- a/Data/WindowState.cs
+++ b/Data/WindowState.cs
@@ -1,8 +1,8 @@
 using System;
 using System.Drawing;
-using System.Linq;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
+using TweetDuck.Core.Utils;
 using TweetDuck.Data.Serialization;
 
 namespace TweetDuck.Data{
@@ -32,7 +32,7 @@ public void Restore(Form form, bool firstTimeFullscreen){
         public static readonly SingleTypeConverter<WindowState> Converter = new SingleTypeConverter<WindowState>{
             ConvertToString = value => $"{(value.isMaximized ? 'M' : '_')}{value.rect.X} {value.rect.Y} {value.rect.Width} {value.rect.Height}",
             ConvertToObject = value => {
-                int[] elements = value.Substring(1).Split(' ').Select(int.Parse).ToArray();
+                int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
                 
                 return new WindowState{
                     rect = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 90560f96..916ce771 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -165,6 +165,7 @@
     <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsRestart.Designer.cs">
       <DependentUpon>DialogSettingsRestart.cs</DependentUpon>
     </Compile>
+    <Compile Include="Core\Utils\StringUtils.cs" />
     <Compile Include="Data\CombinedFileStream.cs" />
     <Compile Include="Core\Other\Settings\Export\ExportFileFlags.cs" />
     <Compile Include="Core\Other\Settings\Export\ExportManager.cs" />
diff --git a/tests/Core/TestBrowserUtils.cs b/tests/Core/TestBrowserUtils.cs
index c71e0f65..2c7a9fff 100644
--- a/tests/Core/TestBrowserUtils.cs
+++ b/tests/Core/TestBrowserUtils.cs
@@ -46,15 +46,5 @@ public void TestGetFileNameFromUrl(){
 
             Assert.IsNull(BrowserUtils.GetFileNameFromUrl("http://test.com/"));
         }
-
-        [TestMethod]
-        public void TestConvertPascalCaseToScreamingSnakeCase(){
-            Assert.AreEqual("HELP", BrowserUtils.ConvertPascalCaseToScreamingSnakeCase("Help"));
-            Assert.AreEqual("HELP_ME", BrowserUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMe"));
-            Assert.AreEqual("HELP_ME_PLEASE", BrowserUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMePlease"));
-
-            Assert.AreEqual("HTML_CODE", BrowserUtils.ConvertPascalCaseToScreamingSnakeCase("HTMLCode"));
-            Assert.AreEqual("CHECK_OUT_MY_HTML_CODE", BrowserUtils.ConvertPascalCaseToScreamingSnakeCase("CheckOutMyHTMLCode"));
-        }
     }
 }
diff --git a/tests/Core/TestStringUtils.cs b/tests/Core/TestStringUtils.cs
new file mode 100644
index 00000000..2b7e4405
--- /dev/null
+++ b/tests/Core/TestStringUtils.cs
@@ -0,0 +1,36 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TweetDuck.Core.Utils;
+
+namespace UnitTests.Core{
+    [TestClass]
+    public class TestStringUtils{
+        [TestMethod]
+        public void TestExtractBefore(){
+            Assert.AreEqual("missing", StringUtils.ExtractBefore("missing", '_'));
+            Assert.AreEqual("", StringUtils.ExtractBefore("_empty", '_'));
+            Assert.AreEqual("some", StringUtils.ExtractBefore("some_text", '_'));
+            Assert.AreEqual("first", StringUtils.ExtractBefore("first_separator_only", '_'));
+            Assert.AreEqual("start_index", StringUtils.ExtractBefore("start_index_test", '_', 8));
+        }
+
+        [TestMethod]
+        public void TestParseInts(){
+            CollectionAssert.AreEqual(new int[0], StringUtils.ParseInts("", ','));
+            CollectionAssert.AreEqual(new int[]{ 1 }, StringUtils.ParseInts("1", ','));
+            CollectionAssert.AreEqual(new int[]{ 1, 2, 3 }, StringUtils.ParseInts("1,2,3", ','));
+            CollectionAssert.AreEqual(new int[]{ 1, 2, 3 }, StringUtils.ParseInts("1,2,3,", ','));
+            CollectionAssert.AreEqual(new int[]{ 1, 2, 3 }, StringUtils.ParseInts(",1,2,,3,", ','));
+            CollectionAssert.AreEqual(new int[]{ -50, 50 }, StringUtils.ParseInts("-50,50", ','));
+        }
+
+        [TestMethod]
+        public void TestConvertPascalCaseToScreamingSnakeCase(){
+            Assert.AreEqual("HELP", StringUtils.ConvertPascalCaseToScreamingSnakeCase("Help"));
+            Assert.AreEqual("HELP_ME", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMe"));
+            Assert.AreEqual("HELP_ME_PLEASE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMePlease"));
+
+            Assert.AreEqual("HTML_CODE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HTMLCode"));
+            Assert.AreEqual("CHECK_OUT_MY_HTML_CODE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("CheckOutMyHTMLCode"));
+        }
+    }
+}
diff --git a/tests/UnitTests.csproj b/tests/UnitTests.csproj
index dfb7360d..a5bb36ad 100644
--- a/tests/UnitTests.csproj
+++ b/tests/UnitTests.csproj
@@ -47,6 +47,7 @@
     </Otherwise>
   </Choose>
   <ItemGroup>
+    <Compile Include="Core\TestStringUtils.cs" />
     <Compile Include="Data\TestCombinedFileStream.cs" />
     <Compile Include="Core\TestBrowserUtils.cs" />
     <Compile Include="Data\TestCommandLineArgs.cs" />

From c63e6a1e495aa7b8e3cb13bb762d5aa475c2fad2 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 16:15:10 +0200
Subject: [PATCH 12/17] More refactoring (seal classes, fix names and comments)

---
 Configuration/UserConfig.cs               | 1 -
 Data/CombinedFileStream.cs                | 2 +-
 Data/CommandLineArgs.cs                   | 2 +-
 Data/InjectedHTML.cs                      | 2 +-
 Data/Serialization/FileSerializer.cs      | 6 +++---
 Data/Serialization/SingleTypeConverter.cs | 2 +-
 Data/TwoKeyDictionary.cs                  | 2 +-
 Data/WindowState.cs                       | 2 +-
 Plugins/Plugin.cs                         | 2 +-
 Plugins/PluginBridge.cs                   | 2 +-
 Plugins/PluginScriptGenerator.cs          | 2 +-
 Reporter.cs                               | 2 +-
 Updates/UpdateInfo.cs                     | 2 +-
 Updates/UpdaterSettings.cs                | 2 +-
 14 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 53ae9a68..1aa10751 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Drawing;
 using System.IO;
-using System.Linq;
 using TweetDuck.Core;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Notification;
diff --git a/Data/CombinedFileStream.cs b/Data/CombinedFileStream.cs
index 061f4e59..223bded6 100644
--- a/Data/CombinedFileStream.cs
+++ b/Data/CombinedFileStream.cs
@@ -4,7 +4,7 @@
 using TweetDuck.Core.Utils;
 
 namespace TweetDuck.Data{
-    class CombinedFileStream : IDisposable{
+    sealed class CombinedFileStream : IDisposable{
         public const char KeySeparator = '|';
 
         private readonly Stream stream;
diff --git a/Data/CommandLineArgs.cs b/Data/CommandLineArgs.cs
index e81a886b..0f525993 100644
--- a/Data/CommandLineArgs.cs
+++ b/Data/CommandLineArgs.cs
@@ -2,7 +2,7 @@
 using System.Text;
 
 namespace TweetDuck.Data{
-    class CommandLineArgs{
+    sealed class CommandLineArgs{
         public static CommandLineArgs FromStringArray(char entryChar, string[] array){
             CommandLineArgs args = new CommandLineArgs();
             ReadStringArray(entryChar, array, args);
diff --git a/Data/InjectedHTML.cs b/Data/InjectedHTML.cs
index 7268f987..6b797b48 100644
--- a/Data/InjectedHTML.cs
+++ b/Data/InjectedHTML.cs
@@ -1,7 +1,7 @@
 using System;
 
 namespace TweetDuck.Data{
-    class InjectedHTML{
+    sealed class InjectedHTML{
         public enum Position{
             Before, After
         }
diff --git a/Data/Serialization/FileSerializer.cs b/Data/Serialization/FileSerializer.cs
index 9a00ce7b..052ad227 100644
--- a/Data/Serialization/FileSerializer.cs
+++ b/Data/Serialization/FileSerializer.cs
@@ -6,11 +6,11 @@
 using System.Runtime.Serialization;
 
 namespace TweetDuck.Data.Serialization{
-    class FileSerializer<T> where T : ISerializedObject{
+    sealed class FileSerializer<T> where T : ISerializedObject{
         private const string NewLineReal = "\r\n";
         private const string NewLineCustom = "\r~\n";
 
-        private static readonly ITypeConverter BasicSerializerObj = new BasicSerializer();
+        private static readonly ITypeConverter BasicSerializerObj = new BasicTypeConverter();
         
         private readonly Dictionary<string, PropertyInfo> props;
         private readonly Dictionary<Type, ITypeConverter> converters;
@@ -82,7 +82,7 @@ public void Read(string file, T obj){
             }
         }
 
-        private class BasicSerializer : ITypeConverter{
+        private class BasicTypeConverter : ITypeConverter{
             bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
                 switch(Type.GetTypeCode(type)){
                     case TypeCode.Boolean:
diff --git a/Data/Serialization/SingleTypeConverter.cs b/Data/Serialization/SingleTypeConverter.cs
index 26a075c0..182a949d 100644
--- a/Data/Serialization/SingleTypeConverter.cs
+++ b/Data/Serialization/SingleTypeConverter.cs
@@ -1,7 +1,7 @@
 using System;
 
 namespace TweetDuck.Data.Serialization{
-    class SingleTypeConverter<T> : ITypeConverter{
+    sealed class SingleTypeConverter<T> : ITypeConverter{
         public delegate string FuncConvertToString<U>(U value);
         public delegate U FuncConvertToObject<U>(string value);
 
diff --git a/Data/TwoKeyDictionary.cs b/Data/TwoKeyDictionary.cs
index 5655b68c..cb96265d 100644
--- a/Data/TwoKeyDictionary.cs
+++ b/Data/TwoKeyDictionary.cs
@@ -2,7 +2,7 @@
 using System.Linq;
 
 namespace TweetDuck.Data{
-    class TwoKeyDictionary<K1, K2, V>{
+    sealed class TwoKeyDictionary<K1, K2, V>{
         private readonly Dictionary<K1, Dictionary<K2, V>> dict;
         private readonly int innerCapacity;
 
diff --git a/Data/WindowState.cs b/Data/WindowState.cs
index ce8b8efe..9ec75951 100644
--- a/Data/WindowState.cs
+++ b/Data/WindowState.cs
@@ -7,7 +7,7 @@
 
 namespace TweetDuck.Data{
     [Serializable] // TODO remove attribute with UserConfigLegacy
-    class WindowState{
+    sealed class WindowState{
         private Rectangle rect;
         private bool isMaximized;
 
diff --git a/Plugins/Plugin.cs b/Plugins/Plugin.cs
index e5ccdecc..9cbc48b6 100644
--- a/Plugins/Plugin.cs
+++ b/Plugins/Plugin.cs
@@ -6,7 +6,7 @@
 using TweetDuck.Plugins.Enums;
 
 namespace TweetDuck.Plugins{
-    class Plugin{
+    sealed class Plugin{
         public string Identifier { get; }
         public PluginGroup Group { get; }
         public PluginEnvironment Environments { get; private set; }
diff --git a/Plugins/PluginBridge.cs b/Plugins/PluginBridge.cs
index 91dee055..fc384b8d 100644
--- a/Plugins/PluginBridge.cs
+++ b/Plugins/PluginBridge.cs
@@ -7,7 +7,7 @@
 using TweetDuck.Plugins.Events;
 
 namespace TweetDuck.Plugins{
-    class PluginBridge{
+    sealed class PluginBridge{
         private static string SanitizeCacheKey(string key){
             return key.Replace('\\', '/').Trim();
         }
diff --git a/Plugins/PluginScriptGenerator.cs b/Plugins/PluginScriptGenerator.cs
index 0b18c8bb..d90afb15 100644
--- a/Plugins/PluginScriptGenerator.cs
+++ b/Plugins/PluginScriptGenerator.cs
@@ -19,7 +19,7 @@ public static string GeneratePlugin(string pluginIdentifier, string pluginConten
 
 /* PluginGen
 
-(function(%params, $i, $d){
+(function(%params, $d){
   let tmp = {
     id: '%id',
     obj: new class extends PluginBase{%contents}
diff --git a/Reporter.cs b/Reporter.cs
index bcd1deca..940a8533 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -8,7 +8,7 @@
 using TweetDuck.Core.Other;
 
 namespace TweetDuck{
-    class Reporter{
+    sealed class Reporter{
         private readonly string logFile;
 
         public Reporter(string logFile){
diff --git a/Updates/UpdateInfo.cs b/Updates/UpdateInfo.cs
index 2f14fce9..8d859eda 100644
--- a/Updates/UpdateInfo.cs
+++ b/Updates/UpdateInfo.cs
@@ -4,7 +4,7 @@
 using TweetDuck.Core.Utils;
 
 namespace TweetDuck.Updates{
-    class UpdateInfo{
+    sealed class UpdateInfo{
         public string VersionTag { get; }
         public string InstallerPath { get; }
 
diff --git a/Updates/UpdaterSettings.cs b/Updates/UpdaterSettings.cs
index bba2ef0f..1232e4bb 100644
--- a/Updates/UpdaterSettings.cs
+++ b/Updates/UpdaterSettings.cs
@@ -1,5 +1,5 @@
 namespace TweetDuck.Updates{
-    class UpdaterSettings{
+    sealed class UpdaterSettings{
         public bool AllowPreReleases { get; set; }
         public string DismissedUpdate { get; set; }
         public string InstallerDownloadFolder { get; set; }

From 8de7e13aa3de0d03e2f1e4bf505f53a2dea9510f Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 19:22:33 +0200
Subject: [PATCH 13/17] Reorganize and refactor UserConfig and PluginConfig

---
 Configuration/UserConfig.cs | 107 ++++++++++++++++--------------------
 Plugins/PluginConfig.cs     |   6 +-
 2 files changed, 49 insertions(+), 64 deletions(-)

diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 1aa10751..956f8997 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -32,115 +32,102 @@ static UserConfig(){
             });
         }
         
-        // START OF CONFIGURATION
+        // CONFIGURATION DATA
 
-        public WindowState BrowserWindow { get; set; }
-        public WindowState PluginsWindow { get; set; }
+        public WindowState BrowserWindow { get; set; } = new WindowState();
+        public WindowState PluginsWindow { get; set; } = new WindowState();
 
-        public bool DisplayNotificationColumn { get; set; }
-        public bool DisplayNotificationTimer { get; set; }
-        public bool NotificationTimerCountDown { get; set; }
-        public bool NotificationSkipOnLinkClick { get; set; }
-        public bool NotificationNonIntrusiveMode { get; set; }
+        public bool ExpandLinksOnHover     { get; set; } = true;
+        public bool SwitchAccountSelectors { get; set; } = true;
+        public bool EnableSpellCheck       { get; set; } = false;
+        private int _zoomLevel                           = 100;
+        private bool _muteNotifications;
+        
+        private TrayIcon.Behavior _trayBehavior          = TrayIcon.Behavior.Disabled;
+        public bool EnableTrayHighlight    { get; set; } = true;
 
-        public int NotificationIdlePauseSeconds { get; set; }
-        public int NotificationDurationValue { get; set; }
-        public int NotificationScrollSpeed { get; set; }
+        public bool EnableUpdateCheck { get; set; } = true;
+        public string DismissedUpdate { get; set; } = null;
 
-        public TweetNotification.Position NotificationPosition { get; set; }
-        public Point CustomNotificationPosition { get; set; }
-        public int NotificationEdgeDistance { get; set; }
-        public int NotificationDisplay { get; set; }
+        public bool DisplayNotificationColumn    { get; set; } = false;
+        public bool NotificationSkipOnLinkClick  { get; set; } = false;
+        public bool NotificationNonIntrusiveMode { get; set; } = true;
+        public int NotificationIdlePauseSeconds  { get; set; } = 0;
 
-        public TweetNotification.Size NotificationSize { get; set; }
-        public Size CustomNotificationSize { get; set; }
+        public bool DisplayNotificationTimer     { get; set; } = true;
+        public bool NotificationTimerCountDown   { get; set; } = false;
+        public int NotificationDurationValue     { get; set; } = 25;
 
-        public bool EnableSpellCheck { get; set; }
-        public bool ExpandLinksOnHover { get; set; }
-        public bool SwitchAccountSelectors { get; set; }
-        public bool EnableTrayHighlight { get; set; }
+        public TweetNotification.Position NotificationPosition { get; set; } = TweetNotification.Position.TopRight;
+        public Point CustomNotificationPosition                { get; set; } = ControlExtensions.InvisibleLocation;
+        public int NotificationDisplay                         { get; set; } = 0;
+        public int NotificationEdgeDistance                    { get; set; } = 8;
 
-        public bool EnableUpdateCheck { get; set; }
-        public string DismissedUpdate { get; set; }
+        public TweetNotification.Size NotificationSize { get; set; } = TweetNotification.Size.Auto;
+        public Size CustomNotificationSize             { get; set; } = Size.Empty;
+        public int NotificationScrollSpeed             { get; set; } = 10;
 
-        public string CustomCefArgs { get; set; }
-        public string CustomBrowserCSS { get; set; }
-        public string CustomNotificationCSS { get; set; }
+        private string _notificationSoundPath;
+
+        public string CustomCefArgs         { get; set; } = null;
+        public string CustomBrowserCSS      { get; set; } = null;
+        public string CustomNotificationCSS { get; set; } = null;
+
+        // SPECIAL PROPERTIES
 
         public bool IsCustomNotificationPositionSet => CustomNotificationPosition != ControlExtensions.InvisibleLocation;
         public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
 
         public string NotificationSoundPath{
-            get => string.IsNullOrEmpty(notificationSoundPath) ? string.Empty : notificationSoundPath;
-            set => notificationSoundPath = value;
+            get => string.IsNullOrEmpty(_notificationSoundPath) ? string.Empty : _notificationSoundPath;
+            set => _notificationSoundPath = value;
         }
 
         public bool MuteNotifications{
-            get => muteNotifications;
+            get => _muteNotifications;
 
             set{
-                if (muteNotifications != value){
-                    muteNotifications = value;
+                if (_muteNotifications != value){
+                    _muteNotifications = value;
                     MuteToggled?.Invoke(this, new EventArgs());
                 }
             }
         }
 
         public int ZoomLevel{
-            get => zoomLevel;
+            get => _zoomLevel;
 
             set{
-                if (zoomLevel != value){
-                    zoomLevel = value;
+                if (_zoomLevel != value){
+                    _zoomLevel = value;
                     ZoomLevelChanged?.Invoke(this, new EventArgs());
                 }
             }
         }
 
-        public double ZoomMultiplier => zoomLevel/100.0;
+        public double ZoomMultiplier => _zoomLevel/100.0;
 
         public TrayIcon.Behavior TrayBehavior{
-            get => trayBehavior;
+            get => _trayBehavior;
 
             set{
-                if (trayBehavior != value){
-                    trayBehavior = value;
+                if (_trayBehavior != value){
+                    _trayBehavior = value;
                     TrayBehaviorChanged?.Invoke(this, new EventArgs());
                 }
             }
         }
 
-        // END OF CONFIGURATION
+        // EVENTS
         
         public event EventHandler MuteToggled;
         public event EventHandler ZoomLevelChanged;
         public event EventHandler TrayBehaviorChanged;
         
         private readonly string file;
-        
-        private bool muteNotifications;
-        private int zoomLevel;
-        private string notificationSoundPath;
-        private TrayIcon.Behavior trayBehavior;
 
         public UserConfig(string file){ // TODO make private after removing UserConfigLegacy
             this.file = file;
-
-            BrowserWindow = new WindowState();
-            ZoomLevel = 100;
-            DisplayNotificationTimer = true;
-            NotificationNonIntrusiveMode = true;
-            NotificationPosition = TweetNotification.Position.TopRight;
-            CustomNotificationPosition = ControlExtensions.InvisibleLocation;
-            NotificationSize = TweetNotification.Size.Auto;
-            NotificationEdgeDistance = 8;
-            NotificationDurationValue = 25;
-            NotificationScrollSpeed = 100;
-            EnableUpdateCheck = true;
-            ExpandLinksOnHover = true;
-            SwitchAccountSelectors = true;
-            EnableTrayHighlight = true;
-            PluginsWindow = new WindowState();
         }
 
         bool ISerializedObject.OnReadUnknownProperty(string property, string value){
diff --git a/Plugins/PluginConfig.cs b/Plugins/PluginConfig.cs
index 7a1b27ac..fe9e8e58 100644
--- a/Plugins/PluginConfig.cs
+++ b/Plugins/PluginConfig.cs
@@ -28,8 +28,7 @@ public bool IsEnabled(Plugin plugin){
 
         public void Load(string file){
             try{
-                using(FileStream stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
-                using(StreamReader reader = new StreamReader(stream, Encoding.UTF8)){
+                using(StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8)){
                     string line = reader.ReadLine();
 
                     if (line == "#Disabled"){
@@ -49,8 +48,7 @@ public void Load(string file){
 
         public void Save(string file){
             try{
-                using(FileStream stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
-                using(StreamWriter writer = new StreamWriter(stream, Encoding.UTF8)){
+                using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8)){
                     writer.WriteLine("#Disabled");
 
                     foreach(string disabled in Disabled){

From 9811f40a53c0479ac03f54e20ad162f987c027ad Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 22:56:36 +0200
Subject: [PATCH 14/17] Go fuck yourself CurrentCulture and stop messing with
 string interpolation

---
 Core/Utils/BrowserUtils.cs | 3 +--
 Program.cs                 | 7 +++++++
 Reporter.cs                | 3 +--
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index 9cfe5b82..672ab1b3 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -2,7 +2,6 @@
 using System;
 using System.Diagnostics;
 using System.Drawing;
-using System.Globalization;
 using System.IO;
 using System.Net;
 using System.Windows.Forms;
@@ -13,7 +12,7 @@ static class BrowserUtils{
 
         public static string HeaderAcceptLanguage{
             get{
-                string culture = CultureInfo.CurrentCulture.Name;
+                string culture = Program.Culture.Name;
 
                 if (culture == "en"){
                     return "en-us,en";
diff --git a/Program.cs b/Program.cs
index 232ff4e4..b2a4bb78 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,6 +1,7 @@
 using CefSharp;
 using System;
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -51,9 +52,15 @@ static class Program{
         public static UserConfig UserConfig { get; private set; }
         public static SystemConfig SystemConfig { get; private set; }
         public static Reporter Reporter { get; private set; }
+        public static CultureInfo Culture { get; }
 
         public static event EventHandler UserConfigReplaced;
 
+        static Program(){
+            Culture = CultureInfo.CurrentCulture;
+            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
+            CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
+
         [STAThread]
         private static void Main(){
             Application.EnableVisualStyles();
diff --git a/Reporter.cs b/Reporter.cs
index 940a8533..42c2e326 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Diagnostics;
 using System.Drawing;
-using System.Globalization;
 using System.IO;
 using System.Text;
 using System.Windows.Forms;
@@ -32,7 +31,7 @@ public bool Log(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", CultureInfo.CurrentCulture)).Append("]\r\n");
+            build.Append("[").Append(DateTime.Now.ToString("G", Program.Culture)).Append("]\r\n");
             build.Append(data).Append("\r\n\r\n");
 
             try{

From eb8159ca0f0714c48b3b7b890d9f4a0d165899f2 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 23:49:57 +0200
Subject: [PATCH 15/17] Add a tooltip to text box in the Sounds tab in Options

---
 Core/Other/Settings/TabSettingsSounds.Designer.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Core/Other/Settings/TabSettingsSounds.Designer.cs b/Core/Other/Settings/TabSettingsSounds.Designer.cs
index f75e43a0..04c3acbe 100644
--- a/Core/Other/Settings/TabSettingsSounds.Designer.cs
+++ b/Core/Other/Settings/TabSettingsSounds.Designer.cs
@@ -77,6 +77,7 @@ private void InitializeComponent() {
             this.tbCustomSound.Name = "tbCustomSound";
             this.tbCustomSound.Size = new System.Drawing.Size(316, 20);
             this.tbCustomSound.TabIndex = 0;
+            this.toolTip.SetToolTip(this.tbCustomSound, "When empty, the default TweetDeck sound notification is used.");
             // 
             // labelSoundNotification
             // 

From 14d44528b09043731d72f0833879c4ad223034f6 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Fri, 7 Jul 2017 23:53:04 +0200
Subject: [PATCH 16/17] Fuck CultureInfo some more and fix analysis violations
 (dispose pattern, lang features)

---
 Core/FormBrowser.cs                                |  2 +-
 Core/Handling/ContextMenuBase.cs                   |  2 +-
 Core/Notification/FormNotificationMain.cs          |  8 ++------
 Core/Other/Settings/TabSettingsNotifications.cs    |  8 ++++----
 Core/Utils/BrowserUtils.cs                         |  2 +-
 Core/Utils/StringUtils.cs                          |  2 +-
 Data/CommandLineArgs.cs                            | 14 +++++++-------
 Plugins/Plugin.cs                                  |  5 ++---
 Plugins/PluginScriptGenerator.cs                   |  5 ++---
 Program.cs                                         | 13 +++++++------
 Reporter.cs                                        |  4 +---
 lib/TweetLib.Audio/AudioPlayer.cs                  |  7 ++++++-
 lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs |  2 +-
 lib/TweetLib.Audio/Impl/SoundPlayerImplWMP.cs      |  2 +-
 lib/TweetLib.Audio/Utils/NativeCoreAudio.cs        |  4 +---
 15 files changed, 38 insertions(+), 42 deletions(-)

diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index b920d814..5dba72a2 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -198,7 +198,7 @@ private void browser_LoadError(object sender, LoadErrorEventArgs e){
                 return;
             }
 
-            if (!e.FailedUrl.StartsWith("http://td/")){
+            if (!e.FailedUrl.StartsWith("http://td/", StringComparison.Ordinal)){
                 string errorPage = ScriptLoader.LoadResource("pages/error.html", true);
 
                 if (errorPage != null){
diff --git a/Core/Handling/ContextMenuBase.cs b/Core/Handling/ContextMenuBase.cs
index beaa5248..cc915a4e 100644
--- a/Core/Handling/ContextMenuBase.cs
+++ b/Core/Handling/ContextMenuBase.cs
@@ -113,7 +113,7 @@ protected void SetClipboardText(string text){
             form.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
         }
         
-        protected void AddDebugMenuItems(IMenuModel model){
+        protected static void AddDebugMenuItems(IMenuModel model){
             model.AddItem((CefMenuCommand)MenuOpenDevTools, "Open dev tools");
         }
 
diff --git a/Core/Notification/FormNotificationMain.cs b/Core/Notification/FormNotificationMain.cs
index eec4cc0d..7e6e9a41 100644
--- a/Core/Notification/FormNotificationMain.cs
+++ b/Core/Notification/FormNotificationMain.cs
@@ -18,12 +18,8 @@ partial class FormNotificationMain : FormNotificationBase{
         private static readonly string NotificationScriptIdentifier = ScriptLoader.GetRootIdentifier(NotificationScriptFile);
         private static readonly string PluginScriptIdentifier = ScriptLoader.GetRootIdentifier(PluginManager.PluginNotificationScriptFile);
 
-        private static readonly string NotificationJS, PluginJS;
-
-        static FormNotificationMain(){
-            NotificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
-            PluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile);
-        }
+        private static readonly string NotificationJS = ScriptLoader.LoadResource(NotificationScriptFile);
+        private static readonly string PluginJS = ScriptLoader.LoadResource(PluginManager.PluginNotificationScriptFile);
         
         private readonly PluginManager plugins;
 
diff --git a/Core/Other/Settings/TabSettingsNotifications.cs b/Core/Other/Settings/TabSettingsNotifications.cs
index b2a9c2f5..315b2b2c 100644
--- a/Core/Other/Settings/TabSettingsNotifications.cs
+++ b/Core/Other/Settings/TabSettingsNotifications.cs
@@ -68,10 +68,10 @@ public TabSettingsNotifications(FormNotificationMain notification){
             checkNonIntrusive.Checked = Config.NotificationNonIntrusiveMode;
 
             trackBarScrollSpeed.SetValueSafe(Config.NotificationScrollSpeed);
-            labelScrollSpeedValue.Text = trackBarScrollSpeed.Value.ToString(CultureInfo.InvariantCulture)+"%";
+            labelScrollSpeedValue.Text = trackBarScrollSpeed.Value+"%";
 
             trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance);
-            labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value.ToString(CultureInfo.InvariantCulture)+" px";
+            labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value+" px";
             
             this.notification.CanMoveWindow = () => radioLocCustom.Checked;
             this.notification.CanResizeWindow = radioSizeCustom.Checked;
@@ -233,7 +233,7 @@ private void comboBoxIdlePause_SelectedValueChanged(object sender, EventArgs e){
 
         private void trackBarScrollSpeed_ValueChanged(object sender, EventArgs e){
             if (trackBarScrollSpeed.AlignValueToTick()){
-                labelScrollSpeedValue.Text = trackBarScrollSpeed.Value.ToString(CultureInfo.InvariantCulture)+"%";
+                labelScrollSpeedValue.Text = trackBarScrollSpeed.Value+"%";
                 Config.NotificationScrollSpeed = trackBarScrollSpeed.Value;
             }
         }
@@ -244,7 +244,7 @@ private void comboBoxDisplay_SelectedValueChanged(object sender, EventArgs e){
         }
 
         private void trackBarEdgeDistance_ValueChanged(object sender, EventArgs e){
-            labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value.ToString(CultureInfo.InvariantCulture)+" px";
+            labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value+" px";
             Config.NotificationEdgeDistance = trackBarEdgeDistance.Value;
             notification.ShowNotificationForSettings(false);
         }
diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index 672ab1b3..b8f4213d 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -18,7 +18,7 @@ public static string HeaderAcceptLanguage{
                     return "en-us,en";
                 }
                 else{
-                    return culture.ToLowerInvariant()+",en;q=0.9";
+                    return culture.ToLower()+",en;q=0.9";
                 }
             }
         }
diff --git a/Core/Utils/StringUtils.cs b/Core/Utils/StringUtils.cs
index 53cd89de..13e1f36c 100644
--- a/Core/Utils/StringUtils.cs
+++ b/Core/Utils/StringUtils.cs
@@ -14,7 +14,7 @@ public static int[] ParseInts(string str, char separator){
         }
         
         public static string ConvertPascalCaseToScreamingSnakeCase(string str){
-            return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpperInvariant();
+            return Regex.Replace(str, @"(\p{Ll})(\P{Ll})|(\P{Ll})(\P{Ll}\p{Ll})", "$1$3_$2$4").ToUpper();
         }
     }
 }
diff --git a/Data/CommandLineArgs.cs b/Data/CommandLineArgs.cs
index 0f525993..0caa437f 100644
--- a/Data/CommandLineArgs.cs
+++ b/Data/CommandLineArgs.cs
@@ -38,31 +38,31 @@ public static void ReadStringArray(char entryChar, string[] array, CommandLineAr
         public int Count => flags.Count+values.Count;
 
         public void AddFlag(string flag){
-            flags.Add(flag.ToLowerInvariant());
+            flags.Add(flag.ToLower());
         }
 
         public bool HasFlag(string flag){
-            return flags.Contains(flag.ToLowerInvariant());
+            return flags.Contains(flag.ToLower());
         }
 
         public void RemoveFlag(string flag){
-            flags.Remove(flag.ToLowerInvariant());
+            flags.Remove(flag.ToLower());
         }
 
         public void SetValue(string key, string value){
-            values[key.ToLowerInvariant()] = value;
+            values[key.ToLower()] = value;
         }
 
         public bool HasValue(string key){
-            return values.ContainsKey(key.ToLowerInvariant());
+            return values.ContainsKey(key.ToLower());
         }
 
         public string GetValue(string key, string defaultValue){
-            return values.TryGetValue(key.ToLowerInvariant(), out string val) ? val : defaultValue;
+            return values.TryGetValue(key.ToLower(), out string val) ? val : defaultValue;
         }
 
         public void RemoveValue(string key){
-            values.Remove(key.ToLowerInvariant());
+            values.Remove(key.ToLower());
         }
 
         public CommandLineArgs Clone(){
diff --git a/Plugins/Plugin.cs b/Plugins/Plugin.cs
index 9cbc48b6..1d738815 100644
--- a/Plugins/Plugin.cs
+++ b/Plugins/Plugin.cs
@@ -132,8 +132,7 @@ public override int GetHashCode(){
         }
 
         public override bool Equals(object obj){
-            Plugin plugin = obj as Plugin;
-            return plugin != null && plugin.Identifier.Equals(Identifier);
+            return obj is Plugin plugin && plugin.Identifier.Equals(Identifier);
         }
 
         public static Plugin CreateFromFolder(string path, PluginGroup group, out string error){
@@ -184,7 +183,7 @@ private static bool LoadMetadata(string path, Plugin plugin, out string error){
                         plugin.metadata[currentTag] = currentContents;
                     }
 
-                    currentTag = line.Substring(1, line.Length-2).ToUpperInvariant();
+                    currentTag = line.Substring(1, line.Length-2).ToUpper();
                     currentContents = "";
 
                     if (line.Equals(endTag[0])){
diff --git a/Plugins/PluginScriptGenerator.cs b/Plugins/PluginScriptGenerator.cs
index d90afb15..d05ac36d 100644
--- a/Plugins/PluginScriptGenerator.cs
+++ b/Plugins/PluginScriptGenerator.cs
@@ -1,5 +1,4 @@
-using System.Globalization;
-using TweetDuck.Plugins.Enums;
+using TweetDuck.Plugins.Enums;
 
 namespace TweetDuck.Plugins{
     static class PluginScriptGenerator{
@@ -11,7 +10,7 @@ public static string GeneratePlugin(string pluginIdentifier, string pluginConten
             return PluginGen
                 .Replace("%params", environment.GetScriptVariables())
                 .Replace("%id", pluginIdentifier)
-                .Replace("%token", pluginToken.ToString(CultureInfo.InvariantCulture))
+                .Replace("%token", pluginToken.ToString())
                 .Replace("%contents", pluginContents);
         }
 
diff --git a/Program.cs b/Program.cs
index b2a4bb78..d43f04a4 100644
--- a/Program.cs
+++ b/Program.cs
@@ -51,7 +51,7 @@ static class Program{
         
         public static UserConfig UserConfig { get; private set; }
         public static SystemConfig SystemConfig { get; private set; }
-        public static Reporter Reporter { get; private set; }
+        public static Reporter Reporter { get; }
         public static CultureInfo Culture { get; }
 
         public static event EventHandler UserConfigReplaced;
@@ -61,21 +61,22 @@ static Program(){
             Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
             CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
 
+            Reporter = new Reporter(ErrorLogFilePath);
+            Reporter.SetupUnhandledExceptionHandler(BrandName+" Has Failed :(");
+        }
+
         [STAThread]
         private static void Main(){
             Application.EnableVisualStyles();
             Application.SetCompatibleTextRenderingDefault(false);
-
+            
             WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
 
             if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
                 MessageBox.Show(BrandName+" does not have write permissions to the storage folder: "+StoragePath, "Permission Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                 return;
             }
-
-            Reporter = new Reporter(ErrorLogFilePath);
-            Reporter.SetupUnhandledExceptionHandler(BrandName+" Has Failed :(");
-
+            
             if (Arguments.HasFlag(Arguments.ArgRestart)){
                 for(int attempt = 0; attempt < 21; attempt++){
                     LockManager.Result lockResult = LockManager.Lock();
diff --git a/Reporter.cs b/Reporter.cs
index 42c2e326..e6803cd3 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -16,9 +16,7 @@ public Reporter(string logFile){
 
         public void SetupUnhandledExceptionHandler(string caption){
             AppDomain.CurrentDomain.UnhandledException += (sender, args) => {
-                Exception ex = args.ExceptionObject as Exception;
-
-                if (ex != null){
+                if (args.ExceptionObject is Exception ex) {
                     HandleException(caption, "An unhandled exception has occurred.", false, ex);
                 }
             };
diff --git a/lib/TweetLib.Audio/AudioPlayer.cs b/lib/TweetLib.Audio/AudioPlayer.cs
index 70730f45..7031a8cd 100644
--- a/lib/TweetLib.Audio/AudioPlayer.cs
+++ b/lib/TweetLib.Audio/AudioPlayer.cs
@@ -31,6 +31,11 @@ public static AudioPlayer New(){
 
         public abstract void Play(string file);
         public abstract void Stop();
-        public abstract void Dispose();
+        protected abstract void Dispose(bool disposing);
+
+        public void Dispose(){
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
     }
 }
diff --git a/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs b/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs
index 9bc73f93..df7b86a8 100644
--- a/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs
+++ b/lib/TweetLib.Audio/Impl/SoundPlayerImplFallback.cs
@@ -38,7 +38,7 @@ public override void Stop(){
             player.Stop();
         }
 
-        public override void Dispose(){
+        protected override void Dispose(bool disposing){
             player.Dispose();
         }
 
diff --git a/lib/TweetLib.Audio/Impl/SoundPlayerImplWMP.cs b/lib/TweetLib.Audio/Impl/SoundPlayerImplWMP.cs
index 232c1e7a..c0e19d0a 100644
--- a/lib/TweetLib.Audio/Impl/SoundPlayerImplWMP.cs
+++ b/lib/TweetLib.Audio/Impl/SoundPlayerImplWMP.cs
@@ -56,7 +56,7 @@ public override void Stop(){
             }
         }
 
-        public override void Dispose(){
+        protected override void Dispose(bool disposing){
             player.close();
             Marshal.ReleaseComObject(player);
         }
diff --git a/lib/TweetLib.Audio/Utils/NativeCoreAudio.cs b/lib/TweetLib.Audio/Utils/NativeCoreAudio.cs
index 91a5e691..74987b7f 100644
--- a/lib/TweetLib.Audio/Utils/NativeCoreAudio.cs
+++ b/lib/TweetLib.Audio/Utils/NativeCoreAudio.cs
@@ -82,9 +82,7 @@ private static ISimpleAudioVolume GetVolumeObject(string name){
             ISimpleAudioVolume volumeObj = null;
 
             for(int index = sessions.GetCount()-1; index >= 0; index--){
-                IAudioSessionControl2 ctl = sessions.GetSession(index) as IAudioSessionControl2;
-
-                if (ctl != null){
+                if (sessions.GetSession(index) is IAudioSessionControl2 ctl){
                     string identifier = ctl.GetSessionIdentifier();
 
                     if (identifier != null && identifier.Contains(name)){

From 0ded03ab9261abd4200fc3373f276ea9e8082681 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Sat, 8 Jul 2017 00:21:41 +0200
Subject: [PATCH 17/17] Fix more analysis violations (exceptions, native method
 pointers, form disposal)

---
 Core/Controls/ControlExtensions.cs |  2 +-
 Core/Utils/NativeMethods.cs        |  4 ++--
 Plugins/PluginBridge.cs            | 10 +++++-----
 Program.cs                         |  2 +-
 Reporter.cs                        |  7 ++++---
 5 files changed, 13 insertions(+), 12 deletions(-)

diff --git a/Core/Controls/ControlExtensions.cs b/Core/Controls/ControlExtensions.cs
index 337c8dd5..c8b02846 100644
--- a/Core/Controls/ControlExtensions.cs
+++ b/Core/Controls/ControlExtensions.cs
@@ -70,7 +70,7 @@ public static bool AlignValueToTick(this TrackBar trackBar){
         public static void SetElevated(this Button button){
             button.Text = " "+button.Text;
             button.FlatStyle = FlatStyle.System;
-            NativeMethods.SendMessage(button.Handle, NativeMethods.BCM_SETSHIELD, 0, new IntPtr(1));
+            NativeMethods.SendMessage(button.Handle, NativeMethods.BCM_SETSHIELD, new UIntPtr(0), new IntPtr(1));
         }
 
         public static void EnableMultilineShortcuts(this TextBox textBox){
diff --git a/Core/Utils/NativeMethods.cs b/Core/Utils/NativeMethods.cs
index 8fe77c76..b1ab111d 100644
--- a/Core/Utils/NativeMethods.cs
+++ b/Core/Utils/NativeMethods.cs
@@ -67,10 +67,10 @@ private struct MSLLHOOKSTRUCT{
         private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);
 
         [DllImport("user32.dll")]
-        public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam);
+        public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
         
         [DllImport("user32.dll")]
-        public static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, IntPtr lParam);
+        public static extern bool PostMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
 
         [DllImport("user32.dll")]
         public static extern uint RegisterWindowMessage(string messageName);
diff --git a/Plugins/PluginBridge.cs b/Plugins/PluginBridge.cs
index fc384b8d..e2f6665d 100644
--- a/Plugins/PluginBridge.cs
+++ b/Plugins/PluginBridge.cs
@@ -47,9 +47,9 @@ private string GetFullPathOrThrow(int token, PluginFolder folder, string path){
 
             if (fullPath.Length == 0){
                 switch(folder){
-                    case PluginFolder.Data: throw new Exception("File path has to be relative to the plugin data folder.");
-                    case PluginFolder.Root: throw new Exception("File path has to be relative to the plugin root folder.");
-                    default: throw new Exception("Invalid folder type "+folder+", this is a "+Program.BrandName+" error.");
+                    case PluginFolder.Data: throw new ArgumentException("File path has to be relative to the plugin data folder.");
+                    case PluginFolder.Root: throw new ArgumentException("File path has to be relative to the plugin root folder.");
+                    default: throw new ArgumentException("Invalid folder type "+folder+", this is a "+Program.BrandName+" error.");
                 }
             }
             else{
@@ -67,9 +67,9 @@ private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool
             try{
                 return fileCache[token, cacheKey] = File.ReadAllText(fullPath, Encoding.UTF8);
             }catch(FileNotFoundException){
-                throw new Exception("File not found.");
+                throw new FileNotFoundException("File not found.");
             }catch(DirectoryNotFoundException){
-                throw new Exception("Directory not found.");
+                throw new DirectoryNotFoundException("Directory not found.");
             }
         }
 
diff --git a/Program.cs b/Program.cs
index d43f04a4..0c228a8b 100644
--- a/Program.cs
+++ b/Program.cs
@@ -109,7 +109,7 @@ private static void Main(){
 
                 if (lockResult == LockManager.Result.HasProcess){
                     if (LockManager.LockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
-                        NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, WindowRestoreMessage, LockManager.LockingProcess.Id, IntPtr.Zero);
+                        NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, WindowRestoreMessage, new UIntPtr((uint)LockManager.LockingProcess.Id), IntPtr.Zero);
 
                         if (WindowsUtils.TrySleepUntil(() => {
                             LockManager.LockingProcess.Refresh();
diff --git a/Reporter.cs b/Reporter.cs
index e6803cd3..ef3d70c9 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -84,9 +84,10 @@ public static void HandleEarlyFailure(string caption, string message){
             Application.EnableVisualStyles();
             Application.SetCompatibleTextRenderingDefault(false);
 
-            FormMessage form = new FormMessage(caption, message, MessageBoxIcon.Error);
-            form.ActiveControl = form.AddButton("Exit");
-            form.ShowDialog();
+            using(FormMessage form = new FormMessage(caption, message, MessageBoxIcon.Error)){
+                form.ActiveControl = form.AddButton("Exit");
+                form.ShowDialog();
+            }
 
             try{
                 Process.GetCurrentProcess().Kill();