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 4e108ac9..b7408d8e 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -1,217 +1,140 @@
 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.Core.Utils;
+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 = StringUtils.ParseInts(value, ' ');
+                    return new Point(elements[0], elements[1]);
+                }
+            });
+            
+            Serializer.RegisterTypeConverter(typeof(Size), new SingleTypeConverter<Size>{
+                ConvertToString = value => $"{value.Width} {value.Height}",
+                ConvertToObject = value => {
+                    int[] elements = StringUtils.ParseInts(value, ' ');
+                    return new Size(elements[0], elements[1]);
+                }
+            });
         }
+        
+        // CONFIGURATION DATA
 
-        private const int CurrentFileVersion = 12;
+        public WindowState BrowserWindow { get; set; } = new WindowState();
+        public WindowState PluginsWindow { get; set; } = new WindowState();
 
-        // START OF CONFIGURATION
+        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 WindowState BrowserWindow { get; set; }
-        public WindowState PluginsWindow { get; set; }
+        public bool EnableUpdateCheck { get; set; } = true;
+        public string DismissedUpdate { get; set; } = null;
 
-        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 DisplayNotificationColumn    { get; set; } = false;
+        public bool NotificationSkipOnLinkClick  { get; set; } = false;
+        public bool NotificationNonIntrusiveMode { get; set; } = true;
+        public int NotificationIdlePauseSeconds  { get; set; } = 0;
 
-        public int NotificationIdlePauseSeconds { get; set; }
-        public int NotificationDurationValue { get; set; }
-        public int NotificationScrollSpeed { get; set; }
+        public bool DisplayNotificationTimer     { get; set; } = true;
+        public bool NotificationTimerCountDown   { get; set; } = false;
+        public int NotificationDurationValue     { get; set; } = 25;
 
-        public TweetNotification.Position NotificationPosition { get; set; }
-        public Point CustomNotificationPosition { get; set; }
-        public int NotificationEdgeDistance { get; set; }
-        public int NotificationDisplay { 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 TweetNotification.Size NotificationSize { get; set; }
-        public Size CustomNotificationSize { 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 bool EnableSpellCheck { get; set; }
-        public bool ExpandLinksOnHover { get; set; }
-        public bool SwitchAccountSelectors { get; set; }
-        public bool EnableTrayHighlight { get; set; }
+        private string _notificationSoundPath;
 
-        public bool EnableUpdateCheck { get; set; }
-        public string DismissedUpdate { get; set; }
+        public string CustomCefArgs         { get; set; } = null;
+        public string CustomBrowserCSS      { get; set; } = null;
+        public string CustomNotificationCSS { get; set; } = null;
 
-        public string CustomCefArgs { get; set; }
-        public string CustomBrowserCSS { get; set; }
-        public string CustomNotificationCSS { get; set; }
-
-        public bool EnableBrowserGCReload { get; set; }
-        public int BrowserMemoryThreshold { get; set; }
+        public bool EnableBrowserGCReload { get; set; } = false;
+        public int BrowserMemoryThreshold { get; set; } = 350;
+        
+        // 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
         
-        [field:NonSerialized]
         public event EventHandler MuteToggled;
-        
-        [field:NonSerialized]
         public event EventHandler ZoomLevelChanged;
-        
-        [field:NonSerialized]
         public event EventHandler TrayBehaviorChanged;
+        
+        private readonly string file;
 
-        [NonSerialized]
-        private string file;
-
-        private int fileVersion;
-        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();
-            ZoomLevel = 100;
-            DisplayNotificationTimer = true;
-            NotificationNonIntrusiveMode = true;
-            NotificationPosition = TweetNotification.Position.TopRight;
-            CustomNotificationPosition = ControlExtensions.InvisibleLocation;
-            NotificationSize = TweetNotification.Size.Auto;
-            NotificationEdgeDistance = 8;
-            NotificationDurationValue = 25;
-            NotificationScrollSpeed = 100;
-            BrowserMemoryThreshold = 350;
-            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 == 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;
-            }
-
-            if (fileVersion == 11){
-                BrowserMemoryThreshold = 350;
-                ++fileVersion;
-            }
-
-            // update the version
-            fileVersion = CurrentFileVersion;
-            Save();
+        bool ISerializedObject.OnReadUnknownProperty(string property, string value){
+            return false;
         }
 
         public bool Save(){
@@ -227,10 +150,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);
@@ -239,22 +159,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;
@@ -266,11 +184,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/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/FormBrowser.cs b/Core/FormBrowser.cs
index f89e4353..f4e81cae 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{
@@ -208,7 +208,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){
@@ -406,7 +406,7 @@ public void UpdateProperties(PropertyBridge.Properties properties){
         }
 
         public void ReloadToTweetDeck(){
-            browser.ExecuteScriptAsync("gc&&gc();window.location.href='https://tweetdeck.twitter.com'");
+            browser.ExecuteScriptAsync($"gc&&gc();window.location.href='{BrowserUtils.TweetDeckURL}'");
         }
 
         // callback handlers
@@ -485,7 +485,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/Handling/ContextMenuBase.cs b/Core/Handling/ContextMenuBase.cs
index 05325c07..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");
         }
 
@@ -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/Notification/FormNotificationBase.cs b/Core/Notification/FormNotificationBase.cs
index 8606d1b1..45216bb1 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);
 
@@ -183,7 +183,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/Notification/FormNotificationMain.cs b/Core/Notification/FormNotificationMain.cs
index a230c6b2..7e6e9a41 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;
@@ -17,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/Notification/Screenshot/FormNotificationScreenshotable.cs b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
index e1e427b8..e308aa4f 100644
--- a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
+++ b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
@@ -4,11 +4,17 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Bridge;
 using TweetDuck.Core.Utils;
+using TweetDuck.Data;
+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 +23,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
             };
 
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/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/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
             // 
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/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index e2e73b69..c56d00ba 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -3,23 +3,23 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Drawing;
-using System.Globalization;
 using System.IO;
 using System.Net;
-using System.Text.RegularExpressions;
 using System.Windows.Forms;
 
 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;
+                string culture = Program.Culture.Name;
 
                 if (culture == "en"){
                     return "en-us,en";
                 }
                 else{
-                    return culture.ToLowerInvariant()+",en;q=0.9";
+                    return culture.ToLower()+",en;q=0.9";
                 }
             }
         }
@@ -80,12 +80,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/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/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/Core/Utils/StringUtils.cs b/Core/Utils/StringUtils.cs
new file mode 100644
index 00000000..13e1f36c
--- /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").ToUpper();
+        }
+    }
+}
diff --git a/Core/Other/Settings/Export/CombinedFileStream.cs b/Data/CombinedFileStream.cs
similarity index 90%
rename from Core/Other/Settings/Export/CombinedFileStream.cs
rename to Data/CombinedFileStream.cs
index 792a28f0..223bded6 100644
--- a/Core/Other/Settings/Export/CombinedFileStream.cs
+++ b/Data/CombinedFileStream.cs
@@ -1,9 +1,10 @@
 using System;
 using System.IO;
 using System.Text;
+using TweetDuck.Core.Utils;
 
-namespace TweetDuck.Core.Other.Settings.Export{
-    class CombinedFileStream : IDisposable{
+namespace TweetDuck.Data{
+    sealed class CombinedFileStream : IDisposable{
         public const char KeySeparator = '|';
 
         private readonly Stream stream;
@@ -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/Core/Utils/CommandLineArgs.cs b/Data/CommandLineArgs.cs
similarity index 85%
rename from Core/Utils/CommandLineArgs.cs
rename to Data/CommandLineArgs.cs
index 295233ec..0caa437f 100644
--- a/Core/Utils/CommandLineArgs.cs
+++ b/Data/CommandLineArgs.cs
@@ -1,8 +1,8 @@
 using System.Collections.Generic;
 using System.Text;
 
-namespace TweetDuck.Core.Utils{
-    class CommandLineArgs{
+namespace TweetDuck.Data{
+    sealed class CommandLineArgs{
         public static CommandLineArgs FromStringArray(char entryChar, string[] array){
             CommandLineArgs args = new CommandLineArgs();
             ReadStringArray(entryChar, array, args);
@@ -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/Core/Utils/InjectedHTML.cs b/Data/InjectedHTML.cs
similarity index 94%
rename from Core/Utils/InjectedHTML.cs
rename to Data/InjectedHTML.cs
index 7982ff36..6b797b48 100644
--- a/Core/Utils/InjectedHTML.cs
+++ b/Data/InjectedHTML.cs
@@ -1,7 +1,7 @@
 using System;
 
-namespace TweetDuck.Core.Utils{
-    class InjectedHTML{
+namespace TweetDuck.Data{
+    sealed class InjectedHTML{
         public enum Position{
             Before, After
         }
diff --git a/Data/Serialization/FileSerializer.cs b/Data/Serialization/FileSerializer.cs
new file mode 100644
index 00000000..052ad227
--- /dev/null
+++ b/Data/Serialization/FileSerializer.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+
+namespace TweetDuck.Data.Serialization{
+    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 BasicTypeConverter();
+        
+        private readonly Dictionary<string, PropertyInfo> props;
+        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.converters = new Dictionary<Type, ITypeConverter>();
+        }
+
+        public void RegisterTypeConverter(Type type, ITypeConverter converter){
+            converters[type] = converter;
+        }
+
+        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 (!converters.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))){
+                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(' ');
+
+                    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 (!converters.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 BasicTypeConverter : ITypeConverter{
+            bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
+                switch(Type.GetTypeCode(type)){
+                    case TypeCode.Boolean:
+                        converted = value.ToString();
+                        return true;
+
+                    case TypeCode.Int32:
+                        converted = ((int)value).ToString(); // cast required for enums
+                        return true;
+
+                    case TypeCode.String:
+                        converted = value?.ToString();
+                        return true;
+
+                    default:
+                        converted = null;
+                        return false;
+                }
+            }
+
+            bool ITypeConverter.TryReadType(Type type, string value, out object converted){
+                switch(Type.GetTypeCode(type)){
+                    case TypeCode.Boolean:
+                        if (bool.TryParse(value, out bool b)){
+                            converted = b;
+                            return true;
+                        }
+                        else goto default;
+
+                    case TypeCode.Int32:
+                        if (int.TryParse(value, out int i)){
+                            converted = i;
+                            return true;
+                        }
+                        else goto default;
+
+                    case TypeCode.String:
+                        converted = value;
+                        return true;
+
+                    default:
+                        converted = null;
+                        return false;
+                }
+            }
+        }
+    }
+}
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/Data/Serialization/SingleTypeConverter.cs b/Data/Serialization/SingleTypeConverter.cs
new file mode 100644
index 00000000..182a949d
--- /dev/null
+++ b/Data/Serialization/SingleTypeConverter.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace TweetDuck.Data.Serialization{
+    sealed 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/Core/Utils/TwoKeyDictionary.cs b/Data/TwoKeyDictionary.cs
similarity index 97%
rename from Core/Utils/TwoKeyDictionary.cs
rename to Data/TwoKeyDictionary.cs
index 39db733d..cb96265d 100644
--- a/Core/Utils/TwoKeyDictionary.cs
+++ b/Data/TwoKeyDictionary.cs
@@ -1,8 +1,8 @@
 using System.Collections.Generic;
 using System.Linq;
 
-namespace TweetDuck.Core.Utils{
-    class TwoKeyDictionary<K1, K2, V>{
+namespace TweetDuck.Data{
+    sealed class TwoKeyDictionary<K1, K2, V>{
         private readonly Dictionary<K1, Dictionary<K2, V>> dict;
         private readonly int innerCapacity;
 
diff --git a/Core/Utils/WindowState.cs b/Data/WindowState.cs
similarity index 53%
rename from Core/Utils/WindowState.cs
rename to Data/WindowState.cs
index 2196df35..9ec75951 100644
--- a/Core/Utils/WindowState.cs
+++ b/Data/WindowState.cs
@@ -2,10 +2,12 @@
 using System.Drawing;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
+using TweetDuck.Core.Utils;
+using TweetDuck.Data.Serialization;
 
-namespace TweetDuck.Core.Utils{
-    [Serializable]
-    class WindowState{
+namespace TweetDuck.Data{
+    [Serializable] // TODO remove attribute with UserConfigLegacy
+    sealed 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 = StringUtils.ParseInts(value.Substring(1), ' ');
+                
+                return new WindowState{
+                    rect = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
+                    isMaximized = value[0] == 'M'
+                };
+            }
+        };
     }
 }
diff --git a/Plugins/Plugin.cs b/Plugins/Plugin.cs
index e5ccdecc..1d738815 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; }
@@ -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/PluginBridge.cs b/Plugins/PluginBridge.cs
index 64e2c815..e2f6665d 100644
--- a/Plugins/PluginBridge.cs
+++ b/Plugins/PluginBridge.cs
@@ -2,12 +2,12 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 using TweetDuck.Plugins.Enums;
 using TweetDuck.Plugins.Events;
 
 namespace TweetDuck.Plugins{
-    class PluginBridge{
+    sealed class PluginBridge{
         private static string SanitizeCacheKey(string key){
             return key.Replace('\\', '/').Trim();
         }
@@ -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/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){
diff --git a/Plugins/PluginScriptGenerator.cs b/Plugins/PluginScriptGenerator.cs
index 0b18c8bb..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);
         }
 
@@ -19,7 +18,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/Program.cs b/Program.cs
index 3265df59..00631511 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;
@@ -11,6 +12,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;
@@ -50,15 +52,25 @@ 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;
 
+        static Program(){
+            Culture = CultureInfo.CurrentCulture;
+            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");
             SubProcessMessage = NativeMethods.RegisterWindowMessage("TweetDuckSubProcess");
 
@@ -66,10 +78,7 @@ private static void Main(){
                 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();
@@ -102,7 +111,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 bcd1deca..ef3d70c9 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -1,14 +1,13 @@
 using System;
 using System.Diagnostics;
 using System.Drawing;
-using System.Globalization;
 using System.IO;
 using System.Text;
 using System.Windows.Forms;
 using TweetDuck.Core.Other;
 
 namespace TweetDuck{
-    class Reporter{
+    sealed class Reporter{
         private readonly string logFile;
 
         public Reporter(string logFile){
@@ -17,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);
                 }
             };
@@ -32,7 +29,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{
@@ -87,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();
diff --git a/Resources/Scripts/code.js b/Resources/Scripts/code.js
index 6ff4382b..57d2c65b 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(){
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index c689bcae..83d83f28 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">
@@ -164,7 +165,8 @@
     <Compile Include="Core\Other\Settings\Dialogs\DialogSettingsRestart.Designer.cs">
       <DependentUpon>DialogSettingsRestart.cs</DependentUpon>
     </Compile>
-    <Compile Include="Core\Other\Settings\Export\CombinedFileStream.cs" />
+    <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" />
     <Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs">
@@ -195,17 +197,20 @@
       <DependentUpon>TabSettingsNotifications.cs</DependentUpon>
     </Compile>
     <Compile Include="Core\Bridge\CallbackBridge.cs" />
-    <Compile Include="Core\Utils\BrowserProcesses.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="Data\Serialization\FileSerializer.cs" />
+    <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\MemoryUsageTracker.cs" />
-    <Compile Include="Core\Utils\TwoKeyDictionary.cs" />
-    <Compile Include="Core\Utils\WindowState.cs" />
     <Compile Include="Core\Utils\WindowsUtils.cs" />
     <Compile Include="Core\Bridge\TweetDeckBridge.cs" />
     <Compile Include="Core\Other\FormSettings.cs">
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; }
diff --git a/lib/TweetLib.Audio/AudioPlayer.cs b/lib/TweetLib.Audio/AudioPlayer.cs
index 787d07f9..7031a8cd 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{
@@ -32,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 d10219fe..df7b86a8 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{
@@ -39,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/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/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)){
diff --git a/tests/Core/Utils/TestBrowserUtils.cs b/tests/Core/TestBrowserUtils.cs
similarity index 80%
rename from tests/Core/Utils/TestBrowserUtils.cs
rename to tests/Core/TestBrowserUtils.cs
index fa0f72f4..2c7a9fff 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]
@@ -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/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/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/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/Data/TestFileSerializer.cs b/tests/Data/TestFileSerializer.cs
new file mode 100644
index 00000000..231fc943
--- /dev/null
+++ b/tests/Data/TestFileSerializer.cs
@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TweetDuck.Data.Serialization;
+
+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;
+            }
+        }
+
+        [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,
+                TestEnum = TestEnum.D
+            };
+
+            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);
+            Assert.AreEqual(TestEnum.D, read.TestEnum);
+        }
+
+        // TODO more complex tests
+    }
+}
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(){
diff --git a/tests/UnitTests.csproj b/tests/UnitTests.csproj
index 2e67154b..a5bb36ad 100644
--- a/tests/UnitTests.csproj
+++ b/tests/UnitTests.csproj
@@ -47,12 +47,14 @@
     </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="Core\TestStringUtils.cs" />
+    <Compile Include="Data\TestCombinedFileStream.cs" />
+    <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" />
     <Compile Include="TestUtils.cs" />
   </ItemGroup>