diff --git a/Application/DialogHandler.cs b/Application/FileDialogs.cs
similarity index 70%
rename from Application/DialogHandler.cs
rename to Application/FileDialogs.cs
index 23868c40..b2907ec6 100644
--- a/Application/DialogHandler.cs
+++ b/Application/FileDialogs.cs
@@ -2,21 +2,12 @@
 using System.Linq;
 using System.Text;
 using System.Windows.Forms;
-using TweetDuck.Dialogs;
 using TweetDuck.Management;
 using TweetLib.Core.Application;
 using TweetLib.Core.Systems.Dialogs;
 
 namespace TweetDuck.Application {
-	sealed class DialogHandler : IAppDialogHandler {
-		public void Information(string caption, string text, string buttonAccept, string buttonCancel = null) {
-			FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept, buttonCancel));
-		}
-
-		public void Error(string caption, string text, string buttonAccept, string buttonCancel = null) {
-			FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept, buttonCancel));
-		}
-
+	sealed class FileDialogs : IAppFileDialogs {
 		public void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted) {
 			static string FormatFilter(FileDialogFilter filter) {
 				var builder = new StringBuilder();
diff --git a/Application/Logger.cs b/Application/Logger.cs
deleted file mode 100644
index 687949c8..00000000
--- a/Application/Logger.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Text;
-using TweetLib.Core;
-using TweetLib.Core.Application;
-
-namespace TweetDuck.Application {
-	sealed class Logger : IAppLogger {
-		private string LogFilePath => Path.Combine(App.StoragePath, logFileName);
-
-		private readonly string logFileName;
-
-		public Logger(string logFileName) {
-			this.logFileName = logFileName;
-		}
-
-		bool IAppLogger.Debug(string message) {
-			#if DEBUG
-			return Log("DEBUG", message);
-			#else
-			return Configuration.Arguments.HasFlag(Configuration.Arguments.ArgLogging) && Log("DEBUG", message);
-			#endif
-		}
-
-		bool IAppLogger.Info(string message) {
-			return Log("INFO", message);
-		}
-
-		bool IAppLogger.Error(string message) {
-			return Log("ERROR", message);
-		}
-
-		bool IAppLogger.OpenLogFile() {
-			try {
-				using (Process.Start(LogFilePath)) {}
-			} catch (Exception) {
-				return false;
-			}
-
-			return true;
-		}
-
-		private bool Log(string level, string message) {
-			#if DEBUG
-			Debug.WriteLine("[" + level + "] " + message);
-			#endif
-
-			string logFilePath = LogFilePath;
-
-			StringBuilder build = new StringBuilder();
-
-			if (!File.Exists(logFilePath)) {
-				build.Append("Please, report all issues to: https://github.com/chylex/TweetDuck/issues\r\n\r\n");
-			}
-
-			build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("]\r\n");
-			build.Append(message).Append("\r\n\r\n");
-
-			try {
-				File.AppendAllText(logFilePath, build.ToString(), Encoding.UTF8);
-				return true;
-			} catch {
-				return false;
-			}
-		}
-	}
-}
diff --git a/Application/MessageDialogs.cs b/Application/MessageDialogs.cs
new file mode 100644
index 00000000..23bea118
--- /dev/null
+++ b/Application/MessageDialogs.cs
@@ -0,0 +1,15 @@
+using TweetDuck.Dialogs;
+using TweetDuck.Management;
+using TweetLib.Core.Application;
+
+namespace TweetDuck.Application {
+	sealed class MessageDialogs : IAppMessageDialogs {
+		public void Information(string caption, string text, string buttonAccept) {
+			FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept));
+		}
+
+		public void Error(string caption, string text, string buttonAccept) {
+			FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept));
+		}
+	}
+}
diff --git a/Application/SystemHandler.cs b/Application/SystemHandler.cs
index 91191019..d93ac253 100644
--- a/Application/SystemHandler.cs
+++ b/Application/SystemHandler.cs
@@ -4,26 +4,15 @@
 using System.IO;
 using System.Windows.Forms;
 using TweetDuck.Browser;
-using TweetDuck.Configuration;
 using TweetDuck.Dialogs;
 using TweetDuck.Management;
 using TweetLib.Core;
 using TweetLib.Core.Application;
 using TweetLib.Core.Features.Twitter;
+using TweetLib.Core.Systems.Configuration;
 
 namespace TweetDuck.Application {
 	sealed class SystemHandler : IAppSystemHandler {
-		public void OpenAssociatedProgram(string path) {
-			try {
-				using (Process.Start(new ProcessStartInfo {
-					FileName = path,
-					ErrorDialog = true
-				})) {}
-			} catch (Exception e) {
-				App.ErrorHandler.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
-			}
-		}
-
 		public void OpenBrowser(string url) {
 			if (string.IsNullOrWhiteSpace(url)) {
 				return;
@@ -92,7 +81,19 @@ public void OpenFileExplorer(string path) {
 			}
 		}
 
-		public void CopyImageFromFile(string path) {
+		public IAppSystemHandler.OpenAssociatedProgramFunc OpenAssociatedProgram { get; } = path => {
+			try {
+				using (Process.Start(new ProcessStartInfo {
+					FileName = path,
+					ErrorDialog = true
+				})) {}
+			} catch (Exception e) {
+				App.ErrorHandler.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
+			}
+		};
+
+
+		public IAppSystemHandler.CopyImageFromFileFunc CopyImageFromFile { get; } = path => {
 			FormManager.RunOnUIThreadAsync(() => {
 				Image image;
 
@@ -105,18 +106,18 @@ public void CopyImageFromFile(string path) {
 
 				ClipboardManager.SetImage(image);
 			});
-		}
+		};
 
-		public void CopyText(string text) {
+		public IAppSystemHandler.CopyTextFunc CopyText { get; } = text => {
 			FormManager.RunOnUIThreadAsync(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
-		}
+		};
 
-		public void SearchText(string text) {
+		public IAppSystemHandler.SearchTextFunc SearchText { get; } = text => {
 			if (string.IsNullOrWhiteSpace(text)) {
 				return;
 			}
 
-			FormManager.RunOnUIThreadAsync(() => {
+			void PerformSearch() {
 				var config = Program.Config.User;
 				string searchUrl = config.SearchEngineUrl;
 
@@ -138,15 +139,17 @@ public void SearchText(string text) {
 
 						settings.FormClosed += (sender, args) => {
 							if (args.CloseReason == CloseReason.UserClosing && config.SearchEngineUrl != searchUrl) {
-								SearchText(text);
+								PerformSearch();
 							}
 						};
 					}
 				}
 				else {
-					OpenBrowser(searchUrl + Uri.EscapeUriString(text));
+					App.SystemHandler.OpenBrowser(searchUrl + Uri.EscapeUriString(text));
 				}
-			});
-		}
+			}
+
+			FormManager.RunOnUIThreadAsync(PerformSearch);
+		};
 	}
 }
diff --git a/Browser/Adapters/CefResourceProvider.cs b/Browser/Adapters/CefResourceProvider.cs
deleted file mode 100644
index 4fc9ff5c..00000000
--- a/Browser/Adapters/CefResourceProvider.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.IO;
-using System.Net;
-using System.Text;
-using CefSharp;
-using TweetLib.Browser.Interfaces;
-
-namespace TweetDuck.Browser.Adapters {
-	internal sealed class ResourceProvider : IResourceProvider<IResourceHandler> {
-		public IResourceHandler Status(HttpStatusCode code, string message) {
-			var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
-			handler.StatusCode = (int) code;
-			return handler;
-		}
-
-		public IResourceHandler File(byte[] contents, string extension) {
-			if (contents.Length == 0) {
-				return Status(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
-			}
-
-			var handler = CreateHandler(contents);
-			handler.MimeType = Cef.GetMimeType(extension);
-			return handler;
-		}
-
-		private static ResourceHandler CreateHandler(byte[] bytes) {
-			var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
-			handler.Headers.Set("Access-Control-Allow-Origin", "*");
-			return handler;
-		}
-	}
-}
diff --git a/Browser/Adapters/CefSchemeHandlerFactory.cs b/Browser/Adapters/CefSchemeHandlerFactory.cs
index 9c6a7f56..db1ba2f8 100644
--- a/Browser/Adapters/CefSchemeHandlerFactory.cs
+++ b/Browser/Adapters/CefSchemeHandlerFactory.cs
@@ -5,7 +5,7 @@
 
 namespace TweetDuck.Browser.Adapters {
 	internal sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory {
-		public static void Register(CefSettings settings, ICustomSchemeHandler<IResourceHandler> handler) {
+		public static void Register(CefSettings settings, ICustomSchemeHandler handler) {
 			settings.RegisterScheme(new CefCustomScheme {
 				SchemeName = handler.Protocol,
 				IsStandard = false,
@@ -16,14 +16,14 @@ public static void Register(CefSettings settings, ICustomSchemeHandler<IResource
 			});
 		}
 
-		private readonly ICustomSchemeHandler<IResourceHandler> handler;
+		private readonly ICustomSchemeHandler handler;
 
-		private CefSchemeHandlerFactory(ICustomSchemeHandler<IResourceHandler> handler) {
+		private CefSchemeHandlerFactory(ICustomSchemeHandler handler) {
 			this.handler = handler;
 		}
 
 		public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
-			return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri) : null;
+			return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(CefSchemeResourceVisitor.Instance) : null;
 		}
 	}
 }
diff --git a/Browser/Adapters/CefSchemeResourceVisitor.cs b/Browser/Adapters/CefSchemeResourceVisitor.cs
new file mode 100644
index 00000000..9d3c2d3f
--- /dev/null
+++ b/Browser/Adapters/CefSchemeResourceVisitor.cs
@@ -0,0 +1,39 @@
+using System.IO;
+using System.Net;
+using System.Text;
+using CefSharp;
+using TweetLib.Browser.Interfaces;
+using TweetLib.Browser.Request;
+
+namespace TweetDuck.Browser.Adapters {
+	internal sealed class CefSchemeResourceVisitor : ISchemeResourceVisitor<IResourceHandler> {
+		public static CefSchemeResourceVisitor Instance { get; } = new CefSchemeResourceVisitor();
+
+		private static readonly SchemeResource.Status FileIsEmpty = new SchemeResource.Status(HttpStatusCode.NoContent, "File is empty.");
+
+		private CefSchemeResourceVisitor() {}
+
+		public IResourceHandler Status(SchemeResource.Status status) {
+			var handler = CreateHandler(Encoding.UTF8.GetBytes(status.Message));
+			handler.StatusCode = (int) status.Code;
+			return handler;
+		}
+
+		public IResourceHandler File(SchemeResource.File file) {
+			byte[] contents = file.Contents;
+			if (contents.Length == 0) {
+				return Status(FileIsEmpty); // FromByteArray crashes CEF internals with no contents
+			}
+
+			var handler = CreateHandler(contents);
+			handler.MimeType = Cef.GetMimeType(file.Extension);
+			return handler;
+		}
+
+		private static ResourceHandler CreateHandler(byte[] bytes) {
+			var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
+			handler.Headers.Set("Access-Control-Allow-Origin", "*");
+			return handler;
+		}
+	}
+}
diff --git a/Browser/FormBrowser.cs b/Browser/FormBrowser.cs
index f485dd06..28f67dd6 100644
--- a/Browser/FormBrowser.cs
+++ b/Browser/FormBrowser.cs
@@ -21,6 +21,7 @@
 using TweetLib.Core.Features.Plugins;
 using TweetLib.Core.Features.TweetDeck;
 using TweetLib.Core.Resources;
+using TweetLib.Core.Systems.Configuration;
 using TweetLib.Core.Systems.Updates;
 
 namespace TweetDuck.Browser {
@@ -51,11 +52,12 @@ public bool IsWaiting {
 		private readonly FormNotificationTweet notification;
 		#pragma warning restore IDE0069 // Disposable fields should be disposed
 
-		private readonly CachingResourceProvider<IResourceHandler> resourceProvider;
+		private readonly ResourceCache resourceCache;
 		private readonly ITweetDeckInterface tweetDeckInterface;
 		private readonly PluginManager plugins;
 		private readonly UpdateChecker updates;
 		private readonly ContextMenu contextMenu;
+		private readonly uint windowRestoreMessage;
 
 		private bool isLoaded;
 		private FormWindowState prevState;
@@ -63,12 +65,12 @@ public bool IsWaiting {
 		private TweetScreenshotManager notificationScreenshotManager;
 		private VideoPlayer videoPlayer;
 
-		public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, PluginManager pluginManager, IUpdateCheckClient updateCheckClient) {
+		public FormBrowser(ResourceCache resourceCache, PluginManager pluginManager, IUpdateCheckClient updateCheckClient, uint windowRestoreMessage) {
 			InitializeComponent();
 
 			Text = Program.BrandName;
 
-			this.resourceProvider = resourceProvider;
+			this.resourceCache = resourceCache;
 
 			this.plugins = pluginManager;
 
@@ -84,19 +86,21 @@ public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, P
 			this.browser = new TweetDeckBrowser(this, plugins, tweetDeckInterface, updates);
 			this.contextMenu = ContextMenuBrowser.CreateMenu(this);
 
+			this.windowRestoreMessage = windowRestoreMessage;
+
 			Controls.Add(new MenuStrip { Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
 
+			Config.MuteToggled += Config_MuteToggled;
+			Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
+
 			Disposed += (sender, args) => {
 				Config.MuteToggled -= Config_MuteToggled;
 				Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
 				browser.Dispose();
 			};
 
-			Config.MuteToggled += Config_MuteToggled;
-
 			this.trayIcon.ClickRestore += trayIcon_ClickRestore;
 			this.trayIcon.ClickClose += trayIcon_ClickClose;
-			Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
 
 			UpdateTray();
 
@@ -162,7 +166,7 @@ private void FormBrowser_Activated(object sender, EventArgs e) {
 
 			trayIcon.HasNotifications = false;
 
-			if (!browser.Enabled) {      // when taking a screenshot, the window is unfocused and
+			if (!browser.Enabled) {     // when taking a screenshot, the window is unfocused and
 				browser.Enabled = true; // the browser is disabled; if the user clicks back into
 			}                           // the window, enable the browser again
 		}
@@ -308,7 +312,7 @@ private void updateInteractionManager_UpdateDismissed(object sender, UpdateInfo
 		}
 
 		protected override void WndProc(ref Message m) {
-			if (isLoaded && m.Msg == Program.WindowRestoreMessage) {
+			if (isLoaded && m.Msg == windowRestoreMessage) {
 				using Process me = Process.GetCurrentProcess();
 
 				if (me.Id == m.WParam.ToInt32()) {
@@ -345,10 +349,10 @@ public void ResumeNotification() {
 		public void ReloadToTweetDeck() {
 			#if DEBUG
 			Resources.ResourceHotSwap.Run();
-			resourceProvider.ClearCache();
+			resourceCache.ClearCache();
 			#else
 			if (ModifierKeys.HasFlag(Keys.Shift)) {
-				resourceProvider.ClearCache();
+				resourceCache.ClearCache();
 			}
 			#endif
 
@@ -361,7 +365,7 @@ public void OpenDevTools() {
 
 		// callback handlers
 
-		public void OnIntroductionClosed(bool showGuide) {
+		private void OnIntroductionClosed(bool showGuide) {
 			if (Config.FirstRun) {
 				Config.FirstRun = false;
 				Config.Save();
@@ -372,7 +376,7 @@ public void OnIntroductionClosed(bool showGuide) {
 			}
 		}
 
-		public void OpenContextMenu() {
+		private void OpenContextMenu() {
 			contextMenu.Show(this, PointToClient(Cursor.Position));
 		}
 
@@ -428,7 +432,7 @@ public void OpenPlugins() {
 			}
 		}
 
-		public void OpenProfileImport() {
+		private void OpenProfileImport() {
 			FormManager.TryFind<FormSettings>()?.Close();
 
 			using DialogSettingsManage dialog = new DialogSettingsManage(plugins, true);
@@ -440,11 +444,11 @@ public void OpenProfileImport() {
 			}
 		}
 
-		public void ShowDesktopNotification(DesktopNotification notification) {
+		private void ShowDesktopNotification(DesktopNotification notification) {
 			this.notification.ShowNotification(notification);
 		}
 
-		public void OnTweetNotification() { // may be called multiple times, once for each type of notification
+		private void OnTweetNotification() { // may be called multiple times, once for each type of notification
 			if (Config.EnableTrayHighlight && !ContainsFocus) {
 				trayIcon.HasNotifications = true;
 			}
@@ -454,7 +458,7 @@ public void SaveVideo(string url, string username) {
 			browser.SaveVideo(url, username);
 		}
 
-		public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
+		private void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
 			if (Arguments.HasFlag(Arguments.ArgHttpVideo)) {
 				videoUrl = Regex.Replace(videoUrl, "^https://", "http://");
 			}
@@ -486,7 +490,7 @@ public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavasc
 			}
 		}
 
-		public void StopVideo() {
+		private void StopVideo() {
 			videoPlayer?.Close();
 		}
 
@@ -502,12 +506,12 @@ public bool ShowTweetDetail(string columnId, string chirpId, string fallbackUrl)
 			return true;
 		}
 
-		public void OnTweetScreenshotReady(string html, int width) {
+		private void OnTweetScreenshotReady(string html, int width) {
 			notificationScreenshotManager ??= new TweetScreenshotManager(this, plugins);
 			notificationScreenshotManager.Trigger(html, width);
 		}
 
-		public void DisplayTooltip(string text) {
+		private void DisplayTooltip(string text) {
 			if (string.IsNullOrEmpty(text)) {
 				toolTip.Hide(this);
 			}
@@ -530,5 +534,75 @@ bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) {
 
 			return false;
 		}
+
+		private sealed class TweetDeckInterfaceImpl : ITweetDeckInterface {
+			private readonly FormBrowser form;
+
+			public TweetDeckInterfaceImpl(FormBrowser form) {
+				this.form = form;
+			}
+
+			public void Alert(string type, string contents) {
+				MessageBoxIcon icon = type switch {
+					"error"   => MessageBoxIcon.Error,
+					"warning" => MessageBoxIcon.Warning,
+					"info"    => MessageBoxIcon.Information,
+					_         => MessageBoxIcon.None
+				};
+
+				FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
+			}
+
+			public void DisplayTooltip(string text) {
+				form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
+			}
+
+			public void FixClipboard() {
+				form.InvokeAsyncSafe(ClipboardManager.StripHtmlStyles);
+			}
+
+			public int GetIdleSeconds() {
+				return NativeMethods.GetIdleSeconds();
+			}
+
+			public void OnIntroductionClosed(bool showGuide) {
+				form.InvokeAsyncSafe(() => form.OnIntroductionClosed(showGuide));
+			}
+
+			public void OnSoundNotification() {
+				form.InvokeAsyncSafe(form.OnTweetNotification);
+			}
+
+			public void OpenContextMenu() {
+				form.InvokeAsyncSafe(form.OpenContextMenu);
+			}
+
+			public void OpenProfileImport() {
+				form.InvokeAsyncSafe(form.OpenProfileImport);
+			}
+
+			public void PlayVideo(string videoUrl, string tweetUrl, string username, object callShowOverlay) {
+				form.InvokeAsyncSafe(() => form.PlayVideo(videoUrl, tweetUrl, username, (IJavascriptCallback) callShowOverlay));
+			}
+
+			public void ScreenshotTweet(string html, int width) {
+				form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width));
+			}
+
+			public void ShowDesktopNotification(DesktopNotification notification) {
+				form.InvokeAsyncSafe(() => {
+					form.OnTweetNotification();
+					form.ShowDesktopNotification(notification);
+				});
+			}
+
+			public void StopVideo() {
+				form.InvokeAsyncSafe(form.StopVideo);
+			}
+
+			public Task ExecuteCallback(object callback, params object[] parameters) {
+				return ((IJavascriptCallback) callback).ExecuteAsync(parameters);
+			}
+		}
 	}
 }
diff --git a/Browser/Handling/ContextMenuBrowser.cs b/Browser/Handling/ContextMenuBrowser.cs
index 689726cc..c7fad0fa 100644
--- a/Browser/Handling/ContextMenuBrowser.cs
+++ b/Browser/Handling/ContextMenuBrowser.cs
@@ -1,11 +1,11 @@
 using System.Windows.Forms;
 using CefSharp;
 using TweetDuck.Browser.Adapters;
-using TweetDuck.Configuration;
 using TweetDuck.Controls;
 using TweetLib.Browser.Contexts;
 using TweetLib.Core.Features.TweetDeck;
 using TweetLib.Core.Features.Twitter;
+using TweetLib.Core.Systems.Configuration;
 using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
 
 namespace TweetDuck.Browser.Handling {
diff --git a/Browser/Handling/FileDialogHandler.cs b/Browser/Handling/FileDialogHandler.cs
index a1751c25..29676aa2 100644
--- a/Browser/Handling/FileDialogHandler.cs
+++ b/Browser/Handling/FileDialogHandler.cs
@@ -4,6 +4,7 @@
 using System.Linq;
 using System.Windows.Forms;
 using CefSharp;
+using TweetLib.Core;
 using TweetLib.Utils.Static;
 
 namespace TweetDuck.Browser.Handling {
@@ -46,17 +47,22 @@ private static IEnumerable<string> ParseFileType(string type) {
 				return new string[] { type };
 			}
 
-			switch (type) {
-				case "image/jpeg": return new string[] { ".jpg", ".jpeg" };
-				case "image/png": return new string[] { ".png" };
-				case "image/gif": return new string[] { ".gif" };
-				case "image/webp": return new string[] { ".webp" };
-				case "video/mp4": return new string[] { ".mp4" };
-				case "video/quicktime": return new string[] { ".mov", ".qt" };
+			string[] extensions = type switch {
+				"image/jpeg"      => new string[] { ".jpg", ".jpeg" },
+				"image/png"       => new string[] { ".png" },
+				"image/gif"       => new string[] { ".gif" },
+				"image/webp"      => new string[] { ".webp" },
+				"video/mp4"       => new string[] { ".mp4" },
+				"video/quicktime" => new string[] { ".mov", ".qt" },
+				_                 => StringUtils.EmptyArray
+			};
+
+			if (extensions.Length == 0) {
+				App.Logger.Warn("Unknown file type: " + type);
+				Debugger.Break();
 			}
 
-			Debugger.Break();
-			return StringUtils.EmptyArray;
+			return extensions;
 		}
 	}
 }
diff --git a/Browser/Notification/FormNotificationBase.cs b/Browser/Notification/FormNotificationBase.cs
index 1a2d4d69..199db797 100644
--- a/Browser/Notification/FormNotificationBase.cs
+++ b/Browser/Notification/FormNotificationBase.cs
@@ -9,6 +9,7 @@
 using TweetLib.Browser.Interfaces;
 using TweetLib.Core.Features.Notifications;
 using TweetLib.Core.Features.Twitter;
+using TweetLib.Core.Systems.Configuration;
 
 namespace TweetDuck.Browser.Notification {
 	abstract partial class FormNotificationBase : Form {
@@ -59,7 +60,9 @@ protected virtual Point PrimaryLocation {
 		protected virtual bool CanDragWindow => true;
 
 		public new Point Location {
-			get { return base.Location; }
+			get {
+				return base.Location;
+			}
 
 			set {
 				Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
diff --git a/Browser/Notification/FormNotificationMain.cs b/Browser/Notification/FormNotificationMain.cs
index 7ef45660..ccfa9c2c 100644
--- a/Browser/Notification/FormNotificationMain.cs
+++ b/Browser/Notification/FormNotificationMain.cs
@@ -61,7 +61,9 @@ private static int FontSizeLevel {
 		private int? prevFontSize;
 
 		public virtual bool RequiresResize {
-			get { return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel; }
+			get {
+				return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel;
+			}
 
 			set {
 				if (value) {
diff --git a/Browser/TrayIcon.cs b/Browser/TrayIcon.cs
index 86fd3a38..44b83ccb 100644
--- a/Browser/TrayIcon.cs
+++ b/Browser/TrayIcon.cs
@@ -2,6 +2,7 @@
 using System.ComponentModel;
 using System.Windows.Forms;
 using TweetDuck.Configuration;
+using TweetLib.Core.Systems.Configuration;
 using Res = TweetDuck.Properties.Resources;
 
 namespace TweetDuck.Browser {
@@ -20,7 +21,9 @@ public enum Behavior { // keep order
 		public event EventHandler ClickClose;
 
 		public bool Visible {
-			get { return notifyIcon.Visible; }
+			get {
+				return notifyIcon.Visible;
+			}
 
 			set {
 				notifyIcon.Visible = value;
@@ -30,7 +33,9 @@ public bool Visible {
 		}
 
 		public bool HasNotifications {
-			get { return hasNotifications; }
+			get {
+				return hasNotifications;
+			}
 
 			set {
 				if (hasNotifications != value) {
@@ -74,7 +79,7 @@ protected override void Dispose(bool disposing) {
 
 		private void UpdateIcon() {
 			if (Visible) {
-				notifyIcon.Icon = hasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray;
+				notifyIcon.Icon = HasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray;
 			}
 		}
 
diff --git a/Browser/TweetDeckBrowser.cs b/Browser/TweetDeckBrowser.cs
index 572c638a..300d8e2a 100644
--- a/Browser/TweetDeckBrowser.cs
+++ b/Browser/TweetDeckBrowser.cs
@@ -66,7 +66,7 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDe
 			if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) {
 				browserComponent.PageLoadEnd += (sender, args) => {
 					if (TwitterUrls.IsTweetDeck(args.Url)) {
-						browserImpl.ScriptExecutor.RunScript("gen:gdpr", "TD.storage.Account.prototype.requiresConsent = function() { return false; }");
+						browserComponent.RunScript("gen:gdpr", "TD.storage.Account.prototype.requiresConsent = function() { return false; }");
 					}
 				};
 			}
@@ -126,7 +126,7 @@ public void HideVideoOverlay(bool focus) {
 				browser.GetBrowser().GetHost().SendFocusEvent(true);
 			}
 
-			browserImpl.ScriptExecutor.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
+			browserComponent.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
 		}
 	}
 }
diff --git a/Browser/TweetDeckInterfaceImpl.cs b/Browser/TweetDeckInterfaceImpl.cs
deleted file mode 100644
index 548d7fc3..00000000
--- a/Browser/TweetDeckInterfaceImpl.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System.Threading.Tasks;
-using System.Windows.Forms;
-using CefSharp;
-using TweetDuck.Controls;
-using TweetDuck.Dialogs;
-using TweetDuck.Management;
-using TweetDuck.Utils;
-using TweetLib.Core.Features.Notifications;
-using TweetLib.Core.Features.TweetDeck;
-
-namespace TweetDuck.Browser {
-	sealed class TweetDeckInterfaceImpl : ITweetDeckInterface {
-		private readonly FormBrowser form;
-
-		public TweetDeckInterfaceImpl(FormBrowser form) {
-			this.form = form;
-		}
-
-		public void Alert(string type, string contents) {
-			MessageBoxIcon icon = type switch {
-				"error"   => MessageBoxIcon.Error,
-				"warning" => MessageBoxIcon.Warning,
-				"info"    => MessageBoxIcon.Information,
-				_         => MessageBoxIcon.None
-			};
-
-			FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
-		}
-
-		public void DisplayTooltip(string text) {
-			form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
-		}
-
-		public void FixClipboard() {
-			form.InvokeAsyncSafe(ClipboardManager.StripHtmlStyles);
-		}
-
-		public int GetIdleSeconds() {
-			return NativeMethods.GetIdleSeconds();
-		}
-
-		public void OnIntroductionClosed(bool showGuide) {
-			form.InvokeAsyncSafe(() => form.OnIntroductionClosed(showGuide));
-		}
-
-		public void OnSoundNotification() {
-			form.InvokeAsyncSafe(form.OnTweetNotification);
-		}
-
-		public void OpenContextMenu() {
-			form.InvokeAsyncSafe(form.OpenContextMenu);
-		}
-
-		public void OpenProfileImport() {
-			form.InvokeAsyncSafe(form.OpenProfileImport);
-		}
-
-		public void PlayVideo(string videoUrl, string tweetUrl, string username, object callShowOverlay) {
-			form.InvokeAsyncSafe(() => form.PlayVideo(videoUrl, tweetUrl, username, (IJavascriptCallback) callShowOverlay));
-		}
-
-		public void ScreenshotTweet(string html, int width) {
-			form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width));
-		}
-
-		public void ShowDesktopNotification(DesktopNotification notification) {
-			form.InvokeAsyncSafe(() => {
-				form.OnTweetNotification();
-				form.ShowDesktopNotification(notification);
-			});
-		}
-
-		public void StopVideo() {
-			form.InvokeAsyncSafe(form.StopVideo);
-		}
-
-		public Task ExecuteCallback(object callback, params object[] parameters) {
-			return ((IJavascriptCallback) callback).ExecuteAsync(parameters);
-		}
-	}
-}
diff --git a/Configuration/ConfigManager.cs b/Configuration/ConfigManager.cs
deleted file mode 100644
index 358074d3..00000000
--- a/Configuration/ConfigManager.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Drawing;
-using TweetDuck.Dialogs;
-using TweetLib.Core.Features.Plugins.Config;
-using TweetLib.Core.Systems.Configuration;
-using TweetLib.Utils.Serialization.Converters;
-using TweetLib.Utils.Static;
-
-namespace TweetDuck.Configuration {
-	sealed class ConfigManager : IConfigManager {
-		internal sealed class Paths {
-			public string UserConfig { get; set; }
-			public string SystemConfig { get; set; }
-			public string PluginConfig { get; set; }
-		}
-
-		public Paths FilePaths { get; }
-
-		public UserConfig User { get; }
-		public SystemConfig System { get; }
-		public PluginConfig Plugins { get; }
-
-		public event EventHandler ProgramRestartRequested;
-
-		private readonly FileConfigInstance<UserConfig> infoUser;
-		private readonly FileConfigInstance<SystemConfig> infoSystem;
-		private readonly PluginConfigInstance<PluginConfig> infoPlugins;
-
-		private readonly IConfigInstance<BaseConfig>[] infoList;
-
-		public ConfigManager(UserConfig userConfig, Paths paths) {
-			FilePaths = paths;
-
-			User = userConfig;
-			System = new SystemConfig();
-			Plugins = new PluginConfig();
-
-			infoList = new IConfigInstance<BaseConfig>[] {
-				infoUser = new FileConfigInstance<UserConfig>(paths.UserConfig, User, "program options"),
-				infoSystem = new FileConfigInstance<SystemConfig>(paths.SystemConfig, System, "system options"),
-				infoPlugins = new PluginConfigInstance<PluginConfig>(paths.PluginConfig, Plugins)
-			};
-
-			// TODO refactor further
-
-			infoUser.Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
-
-			infoUser.Serializer.RegisterTypeConverter(typeof(Point), new BasicTypeConverter<Point> {
-				ConvertToString = value => $"{value.X} {value.Y}",
-				ConvertToObject = value => {
-					int[] elements = StringUtils.ParseInts(value, ' ');
-					return new Point(elements[0], elements[1]);
-				}
-			});
-
-			infoUser.Serializer.RegisterTypeConverter(typeof(Size), new BasicTypeConverter<Size> {
-				ConvertToString = value => $"{value.Width} {value.Height}",
-				ConvertToObject = value => {
-					int[] elements = StringUtils.ParseInts(value, ' ');
-					return new Size(elements[0], elements[1]);
-				}
-			});
-		}
-
-		public void LoadAll() {
-			infoUser.Load();
-			infoSystem.Load();
-			infoPlugins.Load();
-		}
-
-		public void SaveAll() {
-			infoUser.Save();
-			infoSystem.Save();
-			infoPlugins.Save();
-		}
-
-		public void ReloadAll() {
-			infoUser.Reload();
-			infoSystem.Reload();
-			infoPlugins.Reload();
-		}
-
-		public void Save(BaseConfig instance) {
-			((IConfigManager) this).GetInstanceInfo(instance).Save();
-		}
-
-		public void Reset(BaseConfig instance) {
-			((IConfigManager) this).GetInstanceInfo(instance).Reset();
-		}
-
-		public void TriggerProgramRestartRequested() {
-			ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
-		}
-
-		IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance) {
-			Type instanceType = instance.GetType();
-			return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null
-		}
-	}
-
-	static class ConfigManagerExtensions {
-		public static void Save(this BaseConfig instance) {
-			Program.Config.Save(instance);
-		}
-
-		public static void Reset(this BaseConfig instance) {
-			Program.Config.Reset(instance);
-		}
-	}
-}
diff --git a/Configuration/PluginConfig.cs b/Configuration/PluginConfig.cs
deleted file mode 100644
index 9e1283de..00000000
--- a/Configuration/PluginConfig.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.Collections.Generic;
-using TweetLib.Core.Features.Plugins;
-using TweetLib.Core.Features.Plugins.Config;
-using TweetLib.Core.Features.Plugins.Events;
-using TweetLib.Core.Systems.Configuration;
-
-namespace TweetDuck.Configuration {
-	sealed class PluginConfig : BaseConfig, IPluginConfig {
-		private static readonly string[] DefaultDisabled = {
-			"official/clear-columns",
-			"official/reply-account"
-		};
-
-		// CONFIGURATION DATA
-
-		private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
-
-		// EVENTS
-
-		public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
-
-		// END OF CONFIG
-
-		protected override BaseConfig ConstructWithDefaults() {
-			return new PluginConfig();
-		}
-
-		// INTERFACE IMPLEMENTATION
-
-		IEnumerable<string> IPluginConfig.DisabledPlugins => disabled;
-
-		void IPluginConfig.Reset(IEnumerable<string> newDisabledPlugins) {
-			disabled.Clear();
-			disabled.UnionWith(newDisabledPlugins);
-		}
-
-		public void SetEnabled(Plugin plugin, bool enabled) {
-			if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))) {
-				PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
-				this.Save();
-			}
-		}
-
-		public bool IsEnabled(Plugin plugin) {
-			return !disabled.Contains(plugin.Identifier);
-		}
-	}
-}
diff --git a/Configuration/SystemConfig.cs b/Configuration/SystemConfig.cs
index 5ddebef3..28692d47 100644
--- a/Configuration/SystemConfig.cs
+++ b/Configuration/SystemConfig.cs
@@ -1,7 +1,8 @@
-using TweetLib.Core.Systems.Configuration;
+using TweetLib.Core;
+using TweetLib.Core.Systems.Configuration;
 
 namespace TweetDuck.Configuration {
-	sealed class SystemConfig : BaseConfig {
+	sealed class SystemConfig : BaseConfig<SystemConfig> {
 		private bool _hardwareAcceleration = true;
 
 		public bool ClearCacheAutomatically { get; set; } = true;
@@ -11,12 +12,12 @@ sealed class SystemConfig : BaseConfig {
 
 		public bool HardwareAcceleration {
 			get => _hardwareAcceleration;
-			set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		// END OF CONFIG
 
-		protected override BaseConfig ConstructWithDefaults() {
+		public override SystemConfig ConstructWithDefaults() {
 			return new SystemConfig();
 		}
 	}
diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 86c06dec..18755c89 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -3,14 +3,15 @@
 using System.Drawing;
 using TweetDuck.Browser;
 using TweetDuck.Controls;
-using TweetDuck.Dialogs;
+using TweetLib.Core;
 using TweetLib.Core.Application;
 using TweetLib.Core.Features.Notifications;
 using TweetLib.Core.Features.Twitter;
 using TweetLib.Core.Systems.Configuration;
+using TweetLib.Utils.Data;
 
 namespace TweetDuck.Configuration {
-	sealed class UserConfig : BaseConfig, IAppUserConfiguration {
+	sealed class UserConfig : BaseConfig<UserConfig>, IAppUserConfiguration {
 		public bool FirstRun { get; set; } = true;
 
 		[SuppressMessage("ReSharper", "UnusedMember.Global")]
@@ -45,7 +46,7 @@ sealed class UserConfig : BaseConfig, IAppUserConfiguration {
 		private string _spellCheckLanguage         = "en-US";
 
 		public string TranslationTarget { get; set; } = "en";
-		public int  CalendarFirstDay    { get; set; } = -1;
+		public int CalendarFirstDay     { get; set; } = -1;
 
 		private TrayIcon.Behavior _trayBehavior       = TrayIcon.Behavior.Disabled;
 		public bool EnableTrayHighlight { get; set; } = true;
@@ -121,32 +122,32 @@ public TrayIcon.Behavior TrayBehavior {
 
 		public bool EnableSmoothScrolling {
 			get => _enableSmoothScrolling;
-			set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		public bool EnableTouchAdjustment {
 			get => _enableTouchAdjustment;
-			set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		public bool EnableColorProfileDetection {
 			get => _enableColorProfileDetection;
-			set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		public bool UseSystemProxyForAllConnections {
 			get => _useSystemProxyForAllConnections;
-			set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		public string CustomCefArgs {
 			get => _customCefArgs;
-			set => UpdatePropertyWithCallback(ref _customCefArgs, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _customCefArgs, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		public string SpellCheckLanguage {
 			get => _spellCheckLanguage;
-			set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, Program.Config.TriggerProgramRestartRequested);
+			set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, App.ConfigManager.TriggerProgramRestartRequested);
 		}
 
 		// EVENTS
@@ -163,7 +164,7 @@ public void TriggerOptionsDialogClosed() {
 
 		// END OF CONFIG
 
-		protected override BaseConfig ConstructWithDefaults() {
+		public override UserConfig ConstructWithDefaults() {
 			return new UserConfig();
 		}
 	}
diff --git a/Controls/ControlExtensions.cs b/Controls/ControlExtensions.cs
index b2654a1c..abbcbb89 100644
--- a/Controls/ControlExtensions.cs
+++ b/Controls/ControlExtensions.cs
@@ -2,6 +2,7 @@
 using System.Drawing;
 using System.Linq;
 using System.Windows.Forms;
+using TweetLib.Utils.Data;
 
 namespace TweetDuck.Controls {
 	static class ControlExtensions {
@@ -80,5 +81,23 @@ public static void EnableMultilineShortcuts(this TextBox textBox) {
 				}
 			};
 		}
+
+		public static void Save(this WindowState state, Form form) {
+			state.Bounds = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds;
+			state.IsMaximized = form.WindowState == FormWindowState.Maximized;
+		}
+
+		public static void Restore(this WindowState state, Form form, bool firstTimeFullscreen) {
+			if (state.Bounds != Rectangle.Empty) {
+				form.DesktopBounds = state.Bounds;
+				form.WindowState = state.IsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
+			}
+
+			if ((state.Bounds == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) {
+				form.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
+				form.WindowState = FormWindowState.Maximized;
+				state.Save(form);
+			}
+		}
 	}
 }
diff --git a/Dialogs/FormAbout.cs b/Dialogs/FormAbout.cs
index 1a31434e..96eacbc0 100644
--- a/Dialogs/FormAbout.cs
+++ b/Dialogs/FormAbout.cs
@@ -9,7 +9,6 @@
 namespace TweetDuck.Dialogs {
 	sealed partial class FormAbout : Form, FormManager.IAppDialog {
 		private const string TipsLink = "https://github.com/chylex/TweetDuck/wiki";
-		private const string IssuesLink = "https://github.com/chylex/TweetDuck/issues";
 
 		public FormAbout() {
 			InitializeComponent();
@@ -20,7 +19,7 @@ public FormAbout() {
 
 			labelWebsite.Links.Add(new LinkLabel.Link(0, labelWebsite.Text.Length, Program.Website));
 			labelTips.Links.Add(new LinkLabel.Link(0, labelTips.Text.Length, TipsLink));
-			labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, IssuesLink));
+			labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, Lib.IssueTrackerUrl));
 
 			try {
 				pictureLogo.Image = Image.FromFile(Path.Combine(App.ResourcesPath, "images/logo.png"));
diff --git a/Dialogs/FormMessage.cs b/Dialogs/FormMessage.cs
index 27ffb0c9..3b2bf2c7 100644
--- a/Dialogs/FormMessage.cs
+++ b/Dialogs/FormMessage.cs
@@ -38,11 +38,7 @@ public static bool Question(string caption, string text, string buttonAccept, st
 			return Show(caption, text, MessageBoxIcon.Question, buttonAccept, buttonCancel);
 		}
 
-		public static bool Show(string caption, string text, MessageBoxIcon icon, string button) {
-			return Show(caption, text, icon, button, null);
-		}
-
-		public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel) {
+		public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel = null) {
 			using FormMessage message = new FormMessage(caption, text, icon);
 
 			if (buttonCancel == null) {
diff --git a/Dialogs/FormPlugins.cs b/Dialogs/FormPlugins.cs
index ae99d7a3..eb690f25 100644
--- a/Dialogs/FormPlugins.cs
+++ b/Dialogs/FormPlugins.cs
@@ -7,6 +7,8 @@
 using TweetDuck.Plugins;
 using TweetLib.Core;
 using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Enums;
+using TweetLib.Core.Systems.Configuration;
 
 namespace TweetDuck.Dialogs {
 	sealed partial class FormPlugins : Form, FormManager.IAppDialog {
@@ -28,14 +30,18 @@ public FormPlugins(PluginManager pluginManager) : this() {
 				Size = new Size(Math.Max(MinimumSize.Width, targetSize.Width), Math.Max(MinimumSize.Height, targetSize.Height));
 			}
 
-			Shown += (sender, args) => { ReloadPluginList(); };
+			Shown += (sender, args) => {
+				ReloadPluginList();
+			};
 
 			FormClosed += (sender, args) => {
 				Config.PluginsWindowSize = Size;
 				Config.Save();
 			};
 
-			ResizeEnd += (sender, args) => { timerLayout.Start(); };
+			ResizeEnd += (sender, args) => {
+				timerLayout.Start();
+			};
 		}
 
 		private int GetPluginOrderIndex(Plugin plugin) {
@@ -92,7 +98,7 @@ private void flowLayoutPlugins_Resize(object sender, EventArgs e) {
 		}
 
 		private void btnOpenFolder_Click(object sender, EventArgs e) {
-			App.SystemHandler.OpenFileExplorer(pluginManager.CustomPluginFolder);
+			App.SystemHandler.OpenFileExplorer(pluginManager.GetPluginFolder(PluginGroup.Custom));
 		}
 
 		private void btnReload_Click(object sender, EventArgs e) {
diff --git a/Dialogs/FormSettings.cs b/Dialogs/FormSettings.cs
index e61de79d..a16a0415 100644
--- a/Dialogs/FormSettings.cs
+++ b/Dialogs/FormSettings.cs
@@ -9,6 +9,7 @@
 using TweetDuck.Dialogs.Settings;
 using TweetDuck.Management;
 using TweetDuck.Utils;
+using TweetLib.Core;
 using TweetLib.Core.Features.Plugins;
 using TweetLib.Core.Features.TweetDeck;
 using TweetLib.Core.Systems.Updates;
@@ -50,14 +51,14 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateChecker up
 		}
 
 		private void PrepareLoad() {
-			Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
+			App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
 		}
 
 		private void PrepareUnload() { // TODO refactor this further later
 			currentTab.Control.OnClosing();
 
-			Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
-			Program.Config.SaveAll();
+			App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
+			App.ConfigManager.SaveAll();
 		}
 
 		private void Config_ProgramRestartRequested(object sender, EventArgs e) {
diff --git a/Dialogs/Settings/DialogSettingsManage.cs b/Dialogs/Settings/DialogSettingsManage.cs
index d70cbb80..4109ef98 100644
--- a/Dialogs/Settings/DialogSettingsManage.cs
+++ b/Dialogs/Settings/DialogSettingsManage.cs
@@ -6,6 +6,7 @@
 using TweetDuck.Management;
 using TweetLib.Core;
 using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Systems.Configuration;
 using TweetLib.Utils.Static;
 
 namespace TweetDuck.Dialogs.Settings {
@@ -133,7 +134,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
 
 				case State.Reset:
 					if (FormMessage.Warning("Reset TweetDuck Options", "This will reset the selected items. Are you sure you want to proceed?", FormMessage.Yes, FormMessage.No)) {
-						Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
+						App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
 
 						if (SelectedItems.HasFlag(ProfileManager.Items.UserConfig)) {
 							Program.Config.User.Reset();
@@ -143,7 +144,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
 							Program.Config.System.Reset();
 						}
 
-						Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
+						App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
 
 						if (SelectedItems.HasFlag(ProfileManager.Items.PluginData)) {
 							Program.Config.Plugins.Reset();
@@ -174,9 +175,9 @@ private void btnContinue_Click(object sender, EventArgs e) {
 
 				case State.Import:
 					if (importManager.Import(SelectedItems)) {
-						Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
-						Program.Config.ReloadAll();
-						Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
+						App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
+						App.ConfigManager.ReloadAll();
+						App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
 
 						if (SelectedItemsForceRestart) {
 							RestartProgram(SelectedItems.HasFlag(ProfileManager.Items.Session) ? new string[] { Arguments.ArgImportCookies } : StringUtils.EmptyArray);
diff --git a/Dialogs/Settings/TabSettingsAdvanced.cs b/Dialogs/Settings/TabSettingsAdvanced.cs
index 709e05cc..299a55da 100644
--- a/Dialogs/Settings/TabSettingsAdvanced.cs
+++ b/Dialogs/Settings/TabSettingsAdvanced.cs
@@ -125,7 +125,9 @@ private void checkClearCacheAuto_CheckedChanged(object sender, EventArgs e) {
 		private void btnEditCefArgs_Click(object sender, EventArgs e) {
 			DialogSettingsCefArgs form = new DialogSettingsCefArgs(Config.CustomCefArgs);
 
-			form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); };
+			form.VisibleChanged += (sender2, args2) => {
+				form.MoveToCenter(ParentForm);
+			};
 
 			form.FormClosed += (sender2, args2) => {
 				RestoreParentForm();
@@ -144,7 +146,9 @@ private void btnEditCefArgs_Click(object sender, EventArgs e) {
 		private void btnEditCSS_Click(object sender, EventArgs e) {
 			DialogSettingsCSS form = new DialogSettingsCSS(Config.CustomBrowserCSS, Config.CustomNotificationCSS, reinjectBrowserCSS, openDevTools);
 
-			form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); };
+			form.VisibleChanged += (sender2, args2) => {
+				form.MoveToCenter(ParentForm);
+			};
 
 			form.FormClosed += (sender2, args2) => {
 				RestoreParentForm();
diff --git a/Dialogs/Settings/TabSettingsFeedback.cs b/Dialogs/Settings/TabSettingsFeedback.cs
index c03faeea..8b333bea 100644
--- a/Dialogs/Settings/TabSettingsFeedback.cs
+++ b/Dialogs/Settings/TabSettingsFeedback.cs
@@ -14,7 +14,7 @@ public override void OnReady() {
 		#region Feedback
 
 		private void btnSendFeedback_Click(object sender, EventArgs e) {
-			App.SystemHandler.OpenBrowser("https://github.com/chylex/TweetDuck/issues/new");
+			App.SystemHandler.OpenBrowser(Lib.IssueTrackerUrl + "/new");
 		}
 
 		#endregion
diff --git a/Dialogs/WindowState.cs b/Dialogs/WindowState.cs
deleted file mode 100644
index 6df8f38a..00000000
--- a/Dialogs/WindowState.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System.Drawing;
-using System.Windows.Forms;
-using TweetDuck.Controls;
-using TweetLib.Utils.Serialization.Converters;
-using TweetLib.Utils.Static;
-
-namespace TweetDuck.Dialogs {
-	sealed class WindowState {
-		private Rectangle rect;
-		private bool isMaximized;
-
-		public void Save(Form form) {
-			rect = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds;
-			isMaximized = form.WindowState == FormWindowState.Maximized;
-		}
-
-		public void Restore(Form form, bool firstTimeFullscreen) {
-			if (rect != Rectangle.Empty) {
-				form.DesktopBounds = rect;
-				form.WindowState = isMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
-			}
-
-			if ((rect == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) {
-				form.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
-				form.WindowState = FormWindowState.Maximized;
-				Save(form);
-			}
-		}
-
-		public static readonly BasicTypeConverter<WindowState> Converter = new BasicTypeConverter<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/Management/LockManager.cs b/Management/LockManager.cs
index f477f63b..277a1776 100644
--- a/Management/LockManager.cs
+++ b/Management/LockManager.cs
@@ -13,6 +13,8 @@ sealed class LockManager {
 		private const int CloseNaturallyTimeout = 10000;
 		private const int CloseKillTimeout = 5000;
 
+		public uint WindowRestoreMessage { get; } = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
+
 		private readonly LockFile lockFile;
 
 		public LockManager(string path) {
@@ -33,7 +35,7 @@ private bool LaunchNormally() {
 			LockResult lockResult = lockFile.Lock();
 
 			if (lockResult is LockResult.HasProcess info) {
-				if (!RestoreProcess(info.Process) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)) {
+				if (!RestoreProcess(info.Process, WindowRestoreMessage) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)) {
 					if (!CloseProcess(info.Process)) {
 						FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
 						return false;
@@ -83,9 +85,9 @@ private static void ShowGenericException(LockResult.Fail fail) {
 			App.ErrorHandler.HandleException("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", false, fail.Exception);
 		}
 
-		private static bool RestoreProcess(Process process) {
+		private static bool RestoreProcess(Process process, uint windowRestoreMessage) {
 			if (process.MainWindowHandle == IntPtr.Zero) { // restore if the original process is in tray
-				NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint) process.Id, 0);
+				NativeMethods.BroadcastMessage(windowRestoreMessage, (uint) process.Id, 0);
 
 				if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)) {
 					return true;
diff --git a/Management/ProfileManager.cs b/Management/ProfileManager.cs
index 1fd4cd76..2d43b2c0 100644
--- a/Management/ProfileManager.cs
+++ b/Management/ProfileManager.cs
@@ -24,8 +24,7 @@ public enum Items {
 			UserConfig = 1,
 			SystemConfig = 2,
 			Session = 4,
-			PluginData = 8,
-			All = UserConfig | SystemConfig | Session | PluginData
+			PluginData = 8
 		}
 
 		private readonly string file;
@@ -41,15 +40,15 @@ public bool Export(Items items) {
 				using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None));
 
 				if (items.HasFlag(Items.UserConfig)) {
-					stream.WriteFile("config", Program.Config.FilePaths.UserConfig);
+					stream.WriteFile("config", App.ConfigManager.UserPath);
 				}
 
 				if (items.HasFlag(Items.SystemConfig)) {
-					stream.WriteFile("system", Program.Config.FilePaths.SystemConfig);
+					stream.WriteFile("system", App.ConfigManager.SystemPath);
 				}
 
 				if (items.HasFlag(Items.PluginData)) {
-					stream.WriteFile("plugin.config", Program.Config.FilePaths.PluginConfig);
+					stream.WriteFile("plugin.config", App.ConfigManager.PluginsPath);
 
 					foreach (Plugin plugin in plugins.Plugins) {
 						foreach (PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))) {
@@ -122,21 +121,21 @@ public bool Import(Items items) {
 						switch (entry.KeyName) {
 							case "config":
 								if (items.HasFlag(Items.UserConfig)) {
-									entry.WriteToFile(Program.Config.FilePaths.UserConfig);
+									entry.WriteToFile(App.ConfigManager.UserPath);
 								}
 
 								break;
 
 							case "system":
 								if (items.HasFlag(Items.SystemConfig)) {
-									entry.WriteToFile(Program.Config.FilePaths.SystemConfig);
+									entry.WriteToFile(App.ConfigManager.SystemPath);
 								}
 
 								break;
 
 							case "plugin.config":
 								if (items.HasFlag(Items.PluginData)) {
-									entry.WriteToFile(Program.Config.FilePaths.PluginConfig);
+									entry.WriteToFile(App.ConfigManager.PluginsPath);
 								}
 
 								break;
diff --git a/Management/VideoPlayer.cs b/Management/VideoPlayer.cs
index 5a300158..ad147aec 100644
--- a/Management/VideoPlayer.cs
+++ b/Management/VideoPlayer.cs
@@ -8,6 +8,7 @@
 using TweetDuck.Dialogs;
 using TweetLib.Communication.Pipe;
 using TweetLib.Core;
+using TweetLib.Core.Systems.Configuration;
 
 namespace TweetDuck.Management {
 	sealed class VideoPlayer : IDisposable {
diff --git a/Plugins/PluginControl.cs b/Plugins/PluginControl.cs
index 0d62491a..2dfef825 100644
--- a/Plugins/PluginControl.cs
+++ b/Plugins/PluginControl.cs
@@ -6,6 +6,7 @@
 using TweetLib.Core;
 using TweetLib.Core.Features.Plugins;
 using TweetLib.Core.Features.Plugins.Enums;
+using TweetLib.Core.Systems.Configuration;
 
 namespace TweetDuck.Plugins {
 	sealed partial class PluginControl : UserControl {
@@ -92,6 +93,7 @@ private void btnConfigure_Click(object sender, EventArgs e) {
 
 		private void btnToggleState_Click(object sender, EventArgs e) {
 			pluginManager.Config.SetEnabled(plugin, !pluginManager.Config.IsEnabled(plugin));
+			pluginManager.Config.Save();
 			UpdatePluginState();
 		}
 
diff --git a/Program.cs b/Program.cs
index da18b52a..221260bd 100644
--- a/Program.cs
+++ b/Program.cs
@@ -8,19 +8,18 @@
 using TweetDuck.Browser.Adapters;
 using TweetDuck.Browser.Handling;
 using TweetDuck.Configuration;
-using TweetDuck.Dialogs;
 using TweetDuck.Management;
 using TweetDuck.Updates;
 using TweetDuck.Utils;
 using TweetLib.Core;
 using TweetLib.Core.Application;
-using TweetLib.Core.Features;
 using TweetLib.Core.Features.Chromium;
 using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Config;
 using TweetLib.Core.Features.TweetDeck;
 using TweetLib.Core.Resources;
+using TweetLib.Core.Systems.Configuration;
 using TweetLib.Utils.Collections;
-using TweetLib.Utils.Static;
 using Win = System.Windows.Forms;
 
 namespace TweetDuck {
@@ -30,22 +29,17 @@ static class Program {
 
 		public const string Website = "https://tweetduck.chylex.com";
 
-		private const string PluginDataFolder = "TD_Plugins";
 		private const string InstallerFolder = "TD_Updates";
 		private const string CefDataFolder = "TD_Chromium";
-
-		private const string ProgramLogFile = "TD_Log.txt";
 		private const string ConsoleLogFile = "TD_Console.txt";
 
 		public static string ExecutablePath => Win.Application.ExecutablePath;
 
-		public static uint WindowRestoreMessage;
-
-		private static LockManager lockManager;
 		private static Reporter errorReporter;
+		private static LockManager lockManager;
 		private static bool hasCleanedUp;
 
-		public static ConfigManager Config { get; private set; }
+		public static ConfigObjects<UserConfig, SystemConfig> Config { get; private set; }
 
 		internal static void SetupWinForms() {
 			Win.Application.EnableVisualStyles();
@@ -59,112 +53,99 @@ private static void Main() {
 			SetupWinForms();
 			Cef.EnableHighDPISupport();
 
-			var startup = new AppStartup {
-				CustomDataFolder = Arguments.GetValue(Arguments.ArgDataFolder)
-			};
-
 			var reporter = new Reporter();
-			var userConfig = new UserConfig();
 
-			Lib.Initialize(new AppBuilder {
-				Startup = startup,
-				Logger = new Logger(ProgramLogFile),
+			Config = new ConfigObjects<UserConfig, SystemConfig>(
+				new UserConfig(),
+				new SystemConfig(),
+				new PluginConfig(new string[] {
+					"official/clear-columns",
+					"official/reply-account"
+				})
+			);
+
+			Lib.AppLauncher launch = Lib.Initialize(new AppBuilder {
+				Setup = new Setup(),
 				ErrorHandler = reporter,
 				SystemHandler = new SystemHandler(),
-				DialogHandler = new DialogHandler(),
-				UserConfiguration = userConfig
+				MessageDialogs = new MessageDialogs(),
+				FileDialogs = new FileDialogs(),
 			});
 
-			LaunchApp(reporter, userConfig);
-		}
-
-		private static void LaunchApp(Reporter reporter, UserConfig userConfig) {
-			App.Launch();
-
 			errorReporter = reporter;
-			string storagePath = App.StoragePath;
+			launch();
+		}
 
-			Config = new ConfigManager(userConfig, new ConfigManager.Paths {
-				UserConfig   = Path.Combine(storagePath, "TD_UserConfig.cfg"),
-				SystemConfig = Path.Combine(storagePath, "TD_SystemConfig.cfg"),
-				PluginConfig = Path.Combine(storagePath, "TD_PluginConfig.cfg")
-			});
+		private sealed class Setup : IAppSetup {
+			public bool IsPortable => File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "makeportable"));
+			public bool IsDebugLogging => Arguments.HasFlag(Arguments.ArgLogging);
+			public string CustomDataFolder => Arguments.GetValue(Arguments.ArgDataFolder);
+			public string ResourceRewriteRules => Arguments.GetValue(Arguments.ArgFreeze);
 
-			lockManager = new LockManager(Path.Combine(storagePath, ".lock"));
-			WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
-
-			if (!lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))) {
-				return;
+			public ConfigManager CreateConfigManager(string storagePath) {
+				return new ConfigManager<UserConfig, SystemConfig>(storagePath, Config);
 			}
 
-			Config.LoadAll();
-
-			if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
-				ProfileManager.ImportCookies();
-			}
-			else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
-				ProfileManager.DeleteCookies();
+			public bool TryLockDataFolder(string lockFile) {
+				lockManager = new LockManager(lockFile);
+				return lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart));
 			}
 
-			var installerFolderPath = Path.Combine(storagePath, InstallerFolder);
-
-			if (Arguments.HasFlag(Arguments.ArgUpdated)) {
-				WindowsUtils.TryDeleteFolderWhenAble(installerFolderPath, 8000);
-				WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(storagePath, "Service Worker"), 4000);
-				BrowserCache.TryClearNow();
-			}
-
-			try {
-				BaseResourceRequestHandler.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
-			} catch (Exception e) {
-				FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK);
-				return;
-			}
-
-			WebUtils.DefaultUserAgent = BrowserUtils.UserAgentVanilla;
-
-			if (Config.User.UseSystemProxyForAllConnections) {
-				WebUtils.EnableSystemProxy();
-			}
-
-			BrowserCache.RefreshTimer();
-
-			CefSharpSettings.WcfEnabled = false;
-
-			CefSettings settings = new CefSettings {
-				UserAgent = BrowserUtils.UserAgentChrome,
-				BrowserSubprocessPath = Path.Combine(App.ProgramPath, BrandName + ".Browser.exe"),
-				CachePath = storagePath,
-				UserDataPath = Path.Combine(storagePath, CefDataFolder),
-				LogFile = Path.Combine(storagePath, ConsoleLogFile),
-				#if !DEBUG
-				LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable
-				#endif
-			};
-
-			var resourceProvider = new CachingResourceProvider<IResourceHandler>(new ResourceProvider());
-			var pluginManager = new PluginManager(Config.Plugins, Path.Combine(storagePath, PluginDataFolder));
-
-			CefSchemeHandlerFactory.Register(settings, new TweetDuckSchemeHandler<IResourceHandler>(resourceProvider));
-			CefSchemeHandlerFactory.Register(settings, new PluginSchemeHandler<IResourceHandler>(resourceProvider, pluginManager));
-
-			CefUtils.ParseCommandLineArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
-			BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
-
-			Cef.Initialize(settings, false, new BrowserProcessHandler());
-
-			Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
-			FormBrowser mainForm = new FormBrowser(resourceProvider, pluginManager, new UpdateCheckClient(installerFolderPath));
-			Win.Application.Run(mainForm);
-
-			if (mainForm.UpdateInstaller != null) {
-				ExitCleanup();
-
-				if (mainForm.UpdateInstaller.Launch()) {
-					Win.Application.Exit();
+			public void BeforeLaunch() {
+				if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
+					ProfileManager.ImportCookies();
 				}
-				else {
-					RestartWithArgsInternal(Arguments.GetCurrentClean());
+				else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
+					ProfileManager.DeleteCookies();
+				}
+
+				if (Arguments.HasFlag(Arguments.ArgUpdated)) {
+					WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(App.StoragePath, InstallerFolder), 8000);
+					WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(App.StoragePath, "Service Worker"), 4000);
+					BrowserCache.TryClearNow();
+				}
+			}
+
+			public void Launch(ResourceCache resourceCache, PluginManager pluginManager) {
+				string storagePath = App.StoragePath;
+
+				BrowserCache.RefreshTimer();
+
+				CefSharpSettings.WcfEnabled = false;
+
+				CefSettings settings = new CefSettings {
+					UserAgent = BrowserUtils.UserAgentChrome,
+					BrowserSubprocessPath = Path.Combine(App.ProgramPath, BrandName + ".Browser.exe"),
+					CachePath = storagePath,
+					UserDataPath = Path.Combine(storagePath, CefDataFolder),
+					LogFile = Path.Combine(storagePath, ConsoleLogFile),
+					#if !DEBUG
+					LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable
+					#endif
+				};
+
+				CefSchemeHandlerFactory.Register(settings, new TweetDuckSchemeHandler(resourceCache));
+				CefSchemeHandlerFactory.Register(settings, new PluginSchemeHandler(resourceCache, pluginManager));
+
+				CefUtils.ParseCommandLineArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
+				BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
+
+				Cef.Initialize(settings, false, new BrowserProcessHandler());
+
+				Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
+				var updateCheckClient = new UpdateCheckClient(Path.Combine(storagePath, InstallerFolder));
+				var mainForm = new FormBrowser(resourceCache, pluginManager, updateCheckClient, lockManager.WindowRestoreMessage);
+				Win.Application.Run(mainForm);
+
+				if (mainForm.UpdateInstaller != null) {
+					ExitCleanup();
+
+					if (mainForm.UpdateInstaller.Launch()) {
+						Win.Application.Exit();
+					}
+					else {
+						RestartWithArgsInternal(Arguments.GetCurrentClean());
+					}
 				}
 			}
 		}
@@ -213,7 +194,7 @@ private static void ExitCleanup() {
 				return;
 			}
 
-			Config.SaveAll();
+			App.Close();
 
 			Cef.Shutdown();
 			BrowserCache.Exit();
diff --git a/Reporter.cs b/Reporter.cs
index 5c75a443..8606b47b 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -48,7 +48,7 @@ public void HandleException(string caption, string message, bool canIgnore, Exce
 				};
 
 				btnOpenLog.Click += (sender, args) => {
-					if (!App.Logger.OpenLogFile()) {
+					if (!OpenLogFile()) {
 						FormMessage.Error("Error Log", "Cannot open error log.", FormMessage.OK);
 					}
 				};
@@ -61,6 +61,16 @@ public void HandleException(string caption, string message, bool canIgnore, Exce
 			});
 		}
 
+		private static bool OpenLogFile() {
+			try {
+				using (Process.Start(App.Logger.LogFilePath)) {}
+			} catch (Exception) {
+				return false;
+			}
+
+			return true;
+		}
+
 		public sealed class ExpandedLogException : Exception {
 			private readonly string details;
 
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 6917f1d5..487f37e1 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -62,14 +62,14 @@
     <Reference Include="System.Windows.Forms" />
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="Application\DialogHandler.cs" />
-    <Compile Include="Application\Logger.cs" />
+    <Compile Include="Application\FileDialogs.cs" />
+    <Compile Include="Application\MessageDialogs.cs" />
     <Compile Include="Browser\Adapters\CefBrowserComponent.cs" />
     <Compile Include="Browser\Adapters\CefContextMenuActionRegistry.cs" />
     <Compile Include="Browser\Adapters\CefContextMenuModel.cs" />
     <Compile Include="Browser\Adapters\CefResourceHandlerFactory.cs" />
     <Compile Include="Browser\Adapters\CefResourceHandlerRegistry.cs" />
-    <Compile Include="Browser\Adapters\CefResourceProvider.cs" />
+    <Compile Include="Browser\Adapters\CefSchemeResourceVisitor.cs" />
     <Compile Include="Browser\Adapters\CefResourceRequestHandler.cs" />
     <Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
     <Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
@@ -80,8 +80,6 @@
     <Compile Include="Browser\Notification\FormNotificationExample.cs">
       <SubType>Form</SubType>
     </Compile>
-    <Compile Include="Browser\TweetDeckInterfaceImpl.cs" />
-    <Compile Include="Dialogs\WindowState.cs" />
     <Compile Include="Management\LockManager.cs" />
     <Compile Include="Application\SystemHandler.cs" />
     <Compile Include="Browser\Handling\ContextMenuBase.cs" />
@@ -98,8 +96,6 @@
     <Compile Include="Browser\Notification\SoundNotification.cs" />
     <Compile Include="Browser\TweetDeckBrowser.cs" />
     <Compile Include="Configuration\Arguments.cs" />
-    <Compile Include="Configuration\ConfigManager.cs" />
-    <Compile Include="Configuration\PluginConfig.cs" />
     <Compile Include="Configuration\SystemConfig.cs" />
     <Compile Include="Configuration\UserConfig.cs" />
     <Compile Include="Controls\ControlExtensions.cs" />
diff --git a/Utils/BrowserUtils.cs b/Utils/BrowserUtils.cs
index 06a6fbc4..5838104e 100644
--- a/Utils/BrowserUtils.cs
+++ b/Utils/BrowserUtils.cs
@@ -10,7 +10,6 @@
 
 namespace TweetDuck.Utils {
 	static class BrowserUtils {
-		public static string UserAgentVanilla => Program.BrandName + " " + System.Windows.Forms.Application.ProductVersion;
 		public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + Cef.ChromiumVersion + " Safari/537.36";
 
 		private static UserConfig Config => Program.Config.User;
diff --git a/lib/TweetLib.Browser/Base/BaseBrowser.cs b/lib/TweetLib.Browser/Base/BaseBrowser.cs
index 21a67218..67c56473 100644
--- a/lib/TweetLib.Browser/Base/BaseBrowser.cs
+++ b/lib/TweetLib.Browser/Base/BaseBrowser.cs
@@ -3,15 +3,11 @@
 
 namespace TweetLib.Browser.Base {
 	public class BaseBrowser<T> : IDisposable where T : BaseBrowser<T> {
-		public IScriptExecutor ScriptExecutor { get; }
-
 		protected readonly IBrowserComponent browserComponent;
 
 		protected BaseBrowser(IBrowserComponent browserComponent, Func<T, BrowserSetup> setup) {
 			this.browserComponent = browserComponent;
 			this.browserComponent.Setup(setup((T) this));
-
-			this.ScriptExecutor = browserComponent;
 		}
 
 		public virtual void Dispose() {}
diff --git a/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs b/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs
index 5974960e..bb71ae5a 100644
--- a/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs
+++ b/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs
@@ -1,8 +1,9 @@
 using System;
+using TweetLib.Browser.Request;
 
 namespace TweetLib.Browser.Interfaces {
-	public interface ICustomSchemeHandler<T> where T : class {
+	public interface ICustomSchemeHandler {
 		string Protocol { get; }
-		T? Resolve(Uri uri);
+		SchemeResource? Resolve(Uri uri);
 	}
 }
diff --git a/lib/TweetLib.Browser/Interfaces/ISchemeResourceVisitor.cs b/lib/TweetLib.Browser/Interfaces/ISchemeResourceVisitor.cs
new file mode 100644
index 00000000..9bd2117a
--- /dev/null
+++ b/lib/TweetLib.Browser/Interfaces/ISchemeResourceVisitor.cs
@@ -0,0 +1,8 @@
+using TweetLib.Browser.Request;
+
+namespace TweetLib.Browser.Interfaces {
+	public interface ISchemeResourceVisitor<T> {
+		T Status(SchemeResource.Status status);
+		T File(SchemeResource.File file);
+	}
+}
diff --git a/lib/TweetLib.Browser/Request/SchemeResource.cs b/lib/TweetLib.Browser/Request/SchemeResource.cs
new file mode 100644
index 00000000..2434f6f4
--- /dev/null
+++ b/lib/TweetLib.Browser/Request/SchemeResource.cs
@@ -0,0 +1,38 @@
+using System.Net;
+using TweetLib.Browser.Interfaces;
+
+namespace TweetLib.Browser.Request {
+	public abstract class SchemeResource {
+		private SchemeResource() {}
+
+		public abstract T Visit<T>(ISchemeResourceVisitor<T> visitor);
+
+		public sealed class Status : SchemeResource {
+			public HttpStatusCode Code { get; }
+			public string Message { get; }
+
+			public Status(HttpStatusCode code, string message) {
+				Code = code;
+				Message = message;
+			}
+
+			public override T Visit<T>(ISchemeResourceVisitor<T> visitor) {
+				return visitor.Status(this);
+			}
+		}
+
+		public sealed class File : SchemeResource {
+			public byte[] Contents { get; }
+			public string Extension { get; }
+
+			public File(byte[] contents, string extension) {
+				Contents = contents;
+				Extension = extension;
+			}
+
+			public override T Visit<T>(ISchemeResourceVisitor<T> visitor) {
+				return visitor.File(this);
+			}
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/App.cs b/lib/TweetLib.Core/App.cs
index 6fd92fea..4d59de71 100644
--- a/lib/TweetLib.Core/App.cs
+++ b/lib/TweetLib.Core/App.cs
@@ -1,29 +1,91 @@
 using System;
 using System.IO;
+using System.Linq;
 using TweetLib.Core.Application;
+using TweetLib.Core.Features;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Resources;
+using TweetLib.Core.Systems.Configuration;
+using TweetLib.Core.Systems.Logging;
 using TweetLib.Utils.Static;
+using Version = TweetDuck.Version;
 
 namespace TweetLib.Core {
 	public static class App {
+		private static IAppSetup Setup { get; } = Validate(Builder.Setup, nameof(Builder.Setup));
+
 		public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
-		public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
+		public static readonly bool IsPortable = Setup.IsPortable;
 
 		public static readonly string ResourcesPath = Path.Combine(ProgramPath, "resources");
 		public static readonly string PluginPath    = Path.Combine(ProgramPath, "plugins");
 		public static readonly string GuidePath     = Path.Combine(ProgramPath, "guide");
 
-		public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : Validate(Builder.Startup, nameof(Builder.Startup)).GetDataFolder();
+		public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
 
-		public static IAppLogger Logger                       { get; } = Validate(Builder.Logger, nameof(Builder.Logger));
-		public static IAppErrorHandler ErrorHandler           { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
-		public static IAppSystemHandler SystemHandler         { get; } = Validate(Builder.SystemHandler, nameof(Builder.SystemHandler));
-		public static IAppDialogHandler DialogHandler         { get; } = Validate(Builder.DialogHandler, nameof(Builder.DialogHandler));
-		public static IAppUserConfiguration UserConfiguration { get; } = Validate(Builder.UserConfiguration, nameof(Builder.UserConfiguration));
+		public static Logger Logger               { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
+		public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
 
-		public static void Launch() {
+		public static IAppErrorHandler ErrorHandler     { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
+		public static IAppSystemHandler SystemHandler   { get; } = Validate(Builder.SystemHandler, nameof(Builder.SystemHandler));
+		public static IAppMessageDialogs MessageDialogs { get; } = Validate(Builder.MessageDialogs, nameof(Builder.MessageDialogs));
+		public static IAppFileDialogs? FileDialogs      { get; } = Builder.FileDialogs;
+
+		internal static IAppUserConfiguration UserConfiguration => ConfigManager.User;
+
+		private static string GetDataFolder() {
+			string? custom = Setup.CustomDataFolder;
+
+			if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))) {
+				if (Path.GetInvalidPathChars().Any(custom.Contains)) {
+					throw new AppException("Data Folder Invalid", "The data folder contains invalid characters:\n" + custom);
+				}
+				else if (!Path.IsPathRooted(custom)) {
+					throw new AppException("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n" + custom);
+				}
+
+				return Environment.ExpandEnvironmentVariables(custom);
+			}
+			else {
+				return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), custom ?? Lib.BrandName);
+			}
+		}
+
+		internal static void Launch() {
 			if (!FileUtils.CheckFolderWritePermission(StoragePath)) {
 				throw new AppException("Permission Error", "TweetDuck does not have write permissions to the storage folder: " + StoragePath);
 			}
+
+			if (!Setup.TryLockDataFolder(Path.Combine(StoragePath, ".lock"))) {
+				return;
+			}
+
+			ConfigManager.LoadAll();
+			Setup.BeforeLaunch();
+
+			var resourceRewriteRules = Setup.ResourceRewriteRules;
+			if (resourceRewriteRules != null) {
+				try {
+					BaseResourceRequestHandler.LoadResourceRewriteRules(resourceRewriteRules);
+				} catch (Exception e) {
+					throw new AppException("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message);
+				}
+			}
+
+			WebUtils.DefaultUserAgent = Lib.BrandName + " " + Version.Tag;
+
+			if (UserConfiguration.UseSystemProxyForAllConnections) {
+				WebUtils.EnableSystemProxy();
+			}
+
+			var resourceCache = new ResourceCache();
+			var pluginManager = new PluginManager(ConfigManager.Plugins, PluginPath, Path.Combine(StoragePath, "TD_Plugins"));
+
+			Setup.Launch(resourceCache, pluginManager);
+		}
+
+		public static void Close() {
+			ConfigManager.SaveAll();
 		}
 
 		// Setup
@@ -46,19 +108,19 @@ private static T Validate<T>(T? obj, string name) where T : class {
 	}
 
 	public sealed class AppBuilder {
-		public AppStartup? Startup { get; set; }
-		public IAppLogger? Logger { get; set; }
+		public IAppSetup? Setup { get; set; }
 		public IAppErrorHandler? ErrorHandler { get; set; }
 		public IAppSystemHandler? SystemHandler { get; set; }
-		public IAppDialogHandler? DialogHandler { get; set; }
-		public IAppUserConfiguration? UserConfiguration { get; set; }
+		public IAppMessageDialogs? MessageDialogs { get; set; }
+		public IAppFileDialogs? FileDialogs { get; set; }
 
 		internal static AppBuilder? Instance { get; private set; }
 
-		internal void Build() {
+		internal Lib.AppLauncher Build() {
 			Instance = this;
 			App.Initialize();
 			Instance = null;
+			return App.Launch;
 		}
 	}
 }
diff --git a/lib/TweetLib.Core/Application/AppException.cs b/lib/TweetLib.Core/Application/AppException.cs
index f3555522..80ae2dc5 100644
--- a/lib/TweetLib.Core/Application/AppException.cs
+++ b/lib/TweetLib.Core/Application/AppException.cs
@@ -4,10 +4,6 @@ namespace TweetLib.Core.Application {
 	public sealed class AppException : Exception {
 		public string Title { get; }
 
-		internal AppException(string title) {
-			this.Title = title;
-		}
-
 		internal AppException(string title, string message) : base(message) {
 			this.Title = title;
 		}
diff --git a/lib/TweetLib.Core/Application/AppStartup.cs b/lib/TweetLib.Core/Application/AppStartup.cs
deleted file mode 100644
index 22adcd68..00000000
--- a/lib/TweetLib.Core/Application/AppStartup.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-
-namespace TweetLib.Core.Application {
-	public sealed class AppStartup {
-		public string? CustomDataFolder { get; set; }
-
-		internal string GetDataFolder() {
-			string? custom = CustomDataFolder;
-
-			if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))) {
-				if (Path.GetInvalidPathChars().Any(custom.Contains)) {
-					throw new AppException("Data Folder Invalid", "The data folder contains invalid characters:\n" + custom);
-				}
-				else if (!Path.IsPathRooted(custom)) {
-					throw new AppException("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n" + custom);
-				}
-
-				return Environment.ExpandEnvironmentVariables(custom);
-			}
-			else {
-				return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), custom ?? Lib.BrandName);
-			}
-		}
-	}
-}
diff --git a/lib/TweetLib.Core/Application/IAppDialogHandler.cs b/lib/TweetLib.Core/Application/IAppDialogHandler.cs
deleted file mode 100644
index a8fe3aa2..00000000
--- a/lib/TweetLib.Core/Application/IAppDialogHandler.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-using TweetLib.Core.Systems.Dialogs;
-
-namespace TweetLib.Core.Application {
-	public interface IAppDialogHandler {
-		void Information(string caption, string text, string buttonAccept, string? buttonCancel = null);
-		void Error(string caption, string text, string buttonAccept, string? buttonCancel = null);
-
-		void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted);
-	}
-}
diff --git a/lib/TweetLib.Core/Application/IAppFileDialogs.cs b/lib/TweetLib.Core/Application/IAppFileDialogs.cs
new file mode 100644
index 00000000..80a89cc9
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppFileDialogs.cs
@@ -0,0 +1,8 @@
+using System;
+using TweetLib.Core.Systems.Dialogs;
+
+namespace TweetLib.Core.Application {
+	public interface IAppFileDialogs {
+		void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted);
+	}
+}
diff --git a/lib/TweetLib.Core/Application/IAppLogger.cs b/lib/TweetLib.Core/Application/IAppLogger.cs
deleted file mode 100644
index 9389b283..00000000
--- a/lib/TweetLib.Core/Application/IAppLogger.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace TweetLib.Core.Application {
-	public interface IAppLogger {
-		bool Debug(string message);
-		bool Info(string message);
-		bool Error(string message);
-		bool OpenLogFile();
-	}
-}
diff --git a/lib/TweetLib.Core/Application/IAppMessageDialogs.cs b/lib/TweetLib.Core/Application/IAppMessageDialogs.cs
new file mode 100644
index 00000000..18878c1a
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppMessageDialogs.cs
@@ -0,0 +1,8 @@
+using TweetLib.Core.Systems.Dialogs;
+
+namespace TweetLib.Core.Application {
+	public interface IAppMessageDialogs {
+		void Information(string caption, string text, string buttonAccept = Dialogs.OK);
+		void Error(string caption, string text, string buttonAccept = Dialogs.OK);
+	}
+}
diff --git a/lib/TweetLib.Core/Application/IAppSetup.cs b/lib/TweetLib.Core/Application/IAppSetup.cs
new file mode 100644
index 00000000..72db9d33
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppSetup.cs
@@ -0,0 +1,20 @@
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Resources;
+using TweetLib.Core.Systems.Configuration;
+
+namespace TweetLib.Core.Application {
+	public interface IAppSetup {
+		bool IsPortable { get; }
+		bool IsDebugLogging { get; }
+		string? CustomDataFolder { get; }
+		string? ResourceRewriteRules { get; }
+
+		ConfigManager CreateConfigManager(string storagePath);
+
+		bool TryLockDataFolder(string lockFile);
+
+		void BeforeLaunch();
+
+		void Launch(ResourceCache resourceCache, PluginManager pluginManager);
+	}
+}
diff --git a/lib/TweetLib.Core/Application/IAppSystemHandler.cs b/lib/TweetLib.Core/Application/IAppSystemHandler.cs
index 2eda33c7..04563f8d 100644
--- a/lib/TweetLib.Core/Application/IAppSystemHandler.cs
+++ b/lib/TweetLib.Core/Application/IAppSystemHandler.cs
@@ -1,10 +1,19 @@
 namespace TweetLib.Core.Application {
 	public interface IAppSystemHandler {
-		void OpenAssociatedProgram(string path);
 		void OpenBrowser(string? url);
 		void OpenFileExplorer(string path);
-		void CopyImageFromFile(string path);
-		void CopyText(string text);
-		void SearchText(string text);
+
+		OpenAssociatedProgramFunc? OpenAssociatedProgram { get; }
+		CopyImageFromFileFunc? CopyImageFromFile { get; }
+		CopyTextFunc? CopyText { get; }
+		SearchTextFunc? SearchText { get; }
+
+		public delegate void OpenAssociatedProgramFunc(string path);
+
+		public delegate void CopyImageFromFileFunc(string path);
+
+		public delegate void CopyTextFunc(string text);
+
+		public delegate void SearchTextFunc(string text);
 	}
 }
diff --git a/lib/TweetLib.Core/Application/IAppUserConfiguration.cs b/lib/TweetLib.Core/Application/IAppUserConfiguration.cs
index b9f6a31c..5ea16b57 100644
--- a/lib/TweetLib.Core/Application/IAppUserConfiguration.cs
+++ b/lib/TweetLib.Core/Application/IAppUserConfiguration.cs
@@ -3,8 +3,8 @@
 
 namespace TweetLib.Core.Application {
 	public interface IAppUserConfiguration {
-		string CustomBrowserCSS { get; }
-		string CustomNotificationCSS { get; }
+		string? CustomBrowserCSS { get; }
+		string? CustomNotificationCSS { get; }
 		string? DismissedUpdate { get; }
 		bool ExpandLinksOnHover { get; }
 		bool FirstRun { get; }
@@ -20,6 +20,7 @@ public interface IAppUserConfiguration {
 		int CalendarFirstDay { get; }
 		string TranslationTarget { get; }
 		ImageQuality TwitterImageQuality { get; }
+		bool UseSystemProxyForAllConnections { get; }
 
 		event EventHandler MuteToggled;
 		event EventHandler OptionsDialogClosed;
diff --git a/lib/TweetLib.Core/Features/BaseContextMenu.cs b/lib/TweetLib.Core/Features/BaseContextMenu.cs
index fb62408c..53b8cdba 100644
--- a/lib/TweetLib.Core/Features/BaseContextMenu.cs
+++ b/lib/TweetLib.Core/Features/BaseContextMenu.cs
@@ -1,9 +1,7 @@
-using System;
 using System.Text.RegularExpressions;
 using TweetLib.Browser.Contexts;
 using TweetLib.Browser.Interfaces;
 using TweetLib.Core.Features.Twitter;
-using TweetLib.Core.Systems.Dialogs;
 using TweetLib.Utils.Static;
 
 namespace TweetLib.Core.Features {
@@ -13,14 +11,14 @@ internal class BaseContextMenu : IContextMenuHandler {
 
 		public BaseContextMenu(IBrowserComponent browser) {
 			this.browser = browser;
-			this.fileDownloadManager = new FileDownloadManager(browser);
+			this.fileDownloadManager = new FileDownloadManager(browser.FileDownloader);
 		}
 
 		public virtual void Show(IContextMenuBuilder menu, Context context) {
 			if (context.Selection is { Editable: false } selection) {
 				AddSearchSelectionItems(menu, selection.Text);
 				menu.AddSeparator();
-				menu.AddAction("Apply ROT13", () => App.DialogHandler.Information("ROT13", StringUtils.ConvertRot13(selection.Text), Dialogs.OK));
+				menu.AddAction("Apply ROT13", () => App.MessageDialogs.Information("ROT13", StringUtils.ConvertRot13(selection.Text)));
 				menu.AddSeparator();
 			}
 
@@ -32,13 +30,13 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
 				Match match = TwitterUrls.RegexAccount.Match(link.CopyUrl);
 
 				if (match.Success) {
-					menu.AddAction(TextOpen("account"), OpenLink(link.Url));
-					menu.AddAction(TextCopy("account"), CopyText(link.CopyUrl));
-					menu.AddAction("Copy account username", CopyText(match.Groups[1].Value));
+					AddOpenAction(menu, TextOpen("account"), link.Url);
+					AddCopyAction(menu, TextCopy("account"), link.CopyUrl);
+					AddCopyAction(menu, "Copy account username", match.Groups[1].Value);
 				}
 				else {
-					menu.AddAction(TextOpen("link"), OpenLink(link.Url));
-					menu.AddAction(TextCopy("link"), CopyText(link.CopyUrl));
+					AddOpenAction(menu, TextOpen("link"), link.Url);
+					AddCopyAction(menu, TextCopy("link"), link.CopyUrl);
 				}
 
 				menu.AddSeparator();
@@ -49,24 +47,37 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
 
 				switch (media.MediaType) {
 					case Media.Type.Image:
-						menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url));
-						menu.AddAction(TextOpen("image"), OpenLink(media.Url));
-						menu.AddAction(TextCopy("image"), CopyText(media.Url));
-						menu.AddAction("Copy image", () => fileDownloadManager.CopyImage(media.Url));
-						menu.AddAction(TextSave("image"), () => fileDownloadManager.SaveImages(new string[] { media.Url }, tweet?.MediaAuthor));
+						if (fileDownloadManager.SupportsViewingImage) {
+							menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url));
+						}
 
-						var imageUrls = tweet?.ImageUrls;
-						if (imageUrls?.Length > 1) {
-							menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor));
+						AddOpenAction(menu, TextOpen("image"), media.Url);
+						AddCopyAction(menu, TextCopy("image"), media.Url);
+
+						if (fileDownloadManager.SupportsCopyingImage) {
+							menu.AddAction("Copy image", () => fileDownloadManager.CopyImage(media.Url));
+						}
+
+						if (fileDownloadManager.SupportsFileSaving) {
+							menu.AddAction(TextSave("image"), () => fileDownloadManager.SaveImages(new string[] { media.Url }, tweet?.MediaAuthor));
+
+							var imageUrls = tweet?.ImageUrls;
+							if (imageUrls?.Length > 1) {
+								menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor));
+							}
 						}
 
 						menu.AddSeparator();
 						break;
 
 					case Media.Type.Video:
-						menu.AddAction(TextOpen("video"), OpenLink(media.Url));
-						menu.AddAction(TextCopy("video"), CopyText(media.Url));
-						menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor));
+						AddOpenAction(menu, TextOpen("video"), media.Url);
+						AddCopyAction(menu, TextCopy("video"), media.Url);
+
+						if (fileDownloadManager.SupportsFileSaving) {
+							menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor));
+						}
+
 						menu.AddSeparator();
 						break;
 				}
@@ -74,22 +85,26 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
 		}
 
 		protected virtual void AddSearchSelectionItems(IContextMenuBuilder menu, string selectedText) {
-			menu.AddAction("Search in browser", () => {
-				App.SystemHandler.SearchText(selectedText);
-				DeselectAll();
-			});
+			if (App.SystemHandler.SearchText is {} searchText) {
+				menu.AddAction("Search in browser", () => {
+					searchText(selectedText);
+					DeselectAll();
+				});
+			}
 		}
 
 		protected void DeselectAll() {
 			browser.RunScript("gen:deselect", "window.getSelection().removeAllRanges()");
 		}
 
-		protected static Action OpenLink(string url) {
-			return () => App.SystemHandler.OpenBrowser(url);
+		protected static void AddOpenAction(IContextMenuBuilder menu, string title, string url) {
+			menu.AddAction(title, () => App.SystemHandler.OpenBrowser(url));
 		}
 
-		protected static Action CopyText(string text) {
-			return () => App.SystemHandler.CopyText(text);
+		protected static void AddCopyAction(IContextMenuBuilder menu, string title, string textToCopy) {
+			if (App.SystemHandler.CopyText is {} copyText) {
+				menu.AddAction(title, () => copyText(textToCopy));
+			}
 		}
 	}
 }
diff --git a/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs b/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs
index d4176146..116774b2 100644
--- a/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs
+++ b/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs
@@ -7,7 +7,7 @@
 using TweetLib.Utils.Static;
 
 namespace TweetLib.Core.Features {
-	public class BaseResourceRequestHandler : IResourceRequestHandler {
+	internal class BaseResourceRequestHandler : IResourceRequestHandler {
 		private static readonly Regex TweetDeckResourceUrl = new (@"/dist/(.*?)\.(.*?)\.(css|js)$");
 		private static readonly SortedList<string, string> TweetDeckHashes = new (4);
 
diff --git a/lib/TweetLib.Core/Features/FileDownloadManager.cs b/lib/TweetLib.Core/Features/FileDownloadManager.cs
index 623c2463..3ecfa030 100644
--- a/lib/TweetLib.Core/Features/FileDownloadManager.cs
+++ b/lib/TweetLib.Core/Features/FileDownloadManager.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using TweetLib.Browser.Interfaces;
@@ -7,16 +8,21 @@
 using TweetLib.Utils.Static;
 
 namespace TweetLib.Core.Features {
+	[SuppressMessage("ReSharper", "MemberCanBeInternal")]
 	public sealed class FileDownloadManager {
-		private readonly IBrowserComponent browser;
+		public bool SupportsViewingImage => App.SystemHandler.OpenAssociatedProgram != null;
+		public bool SupportsCopyingImage => App.SystemHandler.CopyImageFromFile != null;
+		public bool SupportsFileSaving => App.FileDialogs != null;
 
-		internal FileDownloadManager(IBrowserComponent browser) {
-			this.browser = browser;
+		private readonly IFileDownloader fileDownloader;
+
+		internal FileDownloadManager(IFileDownloader fileDownloader) {
+			this.fileDownloader = fileDownloader;
 		}
 
 		private void DownloadTempImage(string url, Action<string> process) {
 			string? staticFileName = TwitterUrls.GetImageFileName(url);
-			string file = Path.Combine(browser.FileDownloader.CacheFolder, staticFileName ?? Path.GetRandomFileName());
+			string file = Path.Combine(fileDownloader.CacheFolder, staticFileName ?? Path.GetRandomFileName());
 
 			if (staticFileName != null && FileUtils.FileExistsAndNotEmpty(file)) {
 				process(file);
@@ -27,14 +33,18 @@ void OnSuccess() {
 				}
 
 				static void OnFailure(Exception ex) {
-					App.DialogHandler.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, Dialogs.OK);
+					App.MessageDialogs.Error("Image Download", "An error occurred while downloading the image: " + ex.Message);
 				}
 
-				browser.FileDownloader.DownloadFile(url, file, OnSuccess, OnFailure);
+				fileDownloader.DownloadFile(url, file, OnSuccess, OnFailure);
 			}
 		}
 
 		public void ViewImage(string url) {
+			if (App.SystemHandler.OpenAssociatedProgram == null) {
+				return;
+			}
+
 			DownloadTempImage(url, static path => {
 				string ext = Path.GetExtension(path);
 
@@ -42,16 +52,24 @@ public void ViewImage(string url) {
 					App.SystemHandler.OpenAssociatedProgram(path);
 				}
 				else {
-					App.DialogHandler.Error("Image Download", "Unknown image file extension: " + ext, Dialogs.OK);
+					App.MessageDialogs.Error("Image Download", "Unknown image file extension: " + ext);
 				}
 			});
 		}
 
 		public void CopyImage(string url) {
-			DownloadTempImage(url, App.SystemHandler.CopyImageFromFile);
+			if (App.SystemHandler.CopyImageFromFile is {} copyImageFromFile) {
+				DownloadTempImage(url, new Action<string>(copyImageFromFile));
+			}
 		}
 
 		public void SaveImages(string[] urls, string? author) {
+			var fileDialogs = App.FileDialogs;
+			if (fileDialogs == null) {
+				App.MessageDialogs.Error("Image Download", "Saving files is not supported!");
+				return;
+			}
+
 			if (urls.Length == 0) {
 				return;
 			}
@@ -70,26 +88,32 @@ public void SaveImages(string[] urls, string? author) {
 				Filters = new [] { new FileDialogFilter(oneImage ? "Image" : "Images", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) }
 			};
 
-			App.DialogHandler.SaveFile(settings, path => {
+			fileDialogs.SaveFile(settings, path => {
 				static void OnFailure(Exception ex) {
-					App.DialogHandler.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, Dialogs.OK);
+					App.MessageDialogs.Error("Image Download", "An error occurred while downloading the image: " + ex.Message);
 				}
 
 				if (oneImage) {
-					browser.FileDownloader.DownloadFile(firstImageLink, path, null, OnFailure);
+					fileDownloader.DownloadFile(firstImageLink, path, null, OnFailure);
 				}
 				else {
 					string pathBase = Path.ChangeExtension(path, null);
 					string pathExt = Path.GetExtension(path);
 
 					for (int index = 0; index < urls.Length; index++) {
-						browser.FileDownloader.DownloadFile(urls[index], $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
+						fileDownloader.DownloadFile(urls[index], $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
 					}
 				}
 			});
 		}
 
 		public void SaveVideo(string url, string? author) {
+			var fileDialogs = App.FileDialogs;
+			if (fileDialogs == null) {
+				App.MessageDialogs.Error("Video Download", "Saving files is not supported!");
+				return;
+			}
+
 			string? filename = TwitterUrls.GetFileNameFromUrl(url);
 			string? ext = Path.GetExtension(filename);
 
@@ -100,12 +124,12 @@ public void SaveVideo(string url, string? author) {
 				Filters = new [] { new FileDialogFilter("Video", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) }
 			};
 
-			App.DialogHandler.SaveFile(settings, path => {
+			fileDialogs.SaveFile(settings, path => {
 				static void OnError(Exception ex) {
-					App.DialogHandler.Error("Video Download", "An error occurred while downloading the video: " + ex.Message, Dialogs.OK);
+					App.MessageDialogs.Error("Video Download", "An error occurred while downloading the video: " + ex.Message);
 				}
 
-				browser.FileDownloader.DownloadFile(url, path, null, OnError);
+				fileDownloader.DownloadFile(url, path, null, OnError);
 			});
 		}
 	}
diff --git a/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs b/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs
index 565cbfa1..e7c4fdfe 100644
--- a/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs
+++ b/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs
@@ -17,8 +17,8 @@ public class Tweet : NotificationBrowser {
 			private readonly PluginManager pluginManager;
 
 			protected Tweet(IBrowserComponent browserComponent, INotificationInterface notificationInterface, ICommonInterface commonInterface, PluginManager pluginManager, Func<NotificationBrowser, BrowserSetup> setup) : base(browserComponent, setup) {
-				this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface));
 				this.browserComponent.PageLoadEnd += BrowserComponentOnPageLoadEnd;
+				this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface));
 
 				this.notificationInterface = notificationInterface;
 				this.pluginManager = pluginManager;
@@ -56,10 +56,10 @@ public override void Show(IContextMenuBuilder menu, Context context) {
 					menu.AddSeparator();
 
 					if (context.Notification is {} notification) {
-						menu.AddAction("Copy tweet address", CopyText(notification.TweetUrl));
+						AddCopyAction(menu, "Copy tweet address", notification.TweetUrl);
 
 						if (!string.IsNullOrEmpty(notification.QuoteUrl)) {
-							menu.AddAction("Copy quoted tweet address", CopyText(notification.QuoteUrl!));
+							AddCopyAction(menu, "Copy quoted tweet address", notification.QuoteUrl!);
 						}
 
 						menu.AddSeparator();
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs b/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
deleted file mode 100644
index 6d09f90e..00000000
--- a/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using TweetLib.Core.Features.Plugins.Events;
-
-namespace TweetLib.Core.Features.Plugins.Config {
-	public interface IPluginConfig {
-		event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
-
-		IEnumerable<string> DisabledPlugins { get; }
-		void Reset(IEnumerable<string> newDisabledPlugins);
-
-		void SetEnabled(Plugin plugin, bool enabled);
-		bool IsEnabled(Plugin plugin);
-	}
-}
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs
new file mode 100644
index 00000000..fd0ee996
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using TweetLib.Core.Features.Plugins.Events;
+using TweetLib.Core.Systems.Configuration;
+
+namespace TweetLib.Core.Features.Plugins.Config {
+	public sealed class PluginConfig : IConfigObject<PluginConfig> {
+		internal IEnumerable<string> DisabledPlugins => disabled;
+
+		public event EventHandler<PluginChangedStateEventArgs>? PluginChangedState;
+
+		private readonly HashSet<string> defaultDisabled;
+		private readonly HashSet<string> disabled;
+
+		public PluginConfig(IEnumerable<string> defaultDisabled) {
+			this.defaultDisabled = new HashSet<string>(defaultDisabled);
+			this.disabled = new HashSet<string>(this.defaultDisabled);
+		}
+
+		public PluginConfig ConstructWithDefaults() {
+			return new PluginConfig(defaultDisabled);
+		}
+
+		internal void Reset(IEnumerable<string> newDisabledPlugins) {
+			disabled.Clear();
+			disabled.UnionWith(newDisabledPlugins);
+		}
+
+		internal void ResetToDefault() {
+			Reset(defaultDisabled);
+		}
+
+		public void SetEnabled(Plugin plugin, bool enabled) {
+			if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))) {
+				PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
+			}
+		}
+
+		public bool IsEnabled(Plugin plugin) {
+			return !disabled.Contains(plugin.Identifier);
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
index b88d5a32..8babcc22 100644
--- a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
@@ -5,14 +5,14 @@
 using TweetLib.Core.Systems.Configuration;
 
 namespace TweetLib.Core.Features.Plugins.Config {
-	public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig {
-		public T Instance { get; }
+	internal sealed class PluginConfigInstance : IConfigInstance {
+		public PluginConfig Instance { get; }
 
 		private readonly string filename;
 
-		public PluginConfigInstance(string filename, T instance) {
-			this.filename = filename;
+		public PluginConfigInstance(string filename, PluginConfig instance) {
 			this.Instance = instance;
+			this.filename = filename;
 		}
 
 		public void Load() {
@@ -58,7 +58,7 @@ public void Reload() {
 		public void Reset() {
 			try {
 				File.Delete(filename);
-				Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins);
+				Instance.ResetToDefault();
 			} catch (Exception e) {
 				OnException("Could not delete the plugin configuration file.", e);
 				return;
diff --git a/lib/TweetLib.Core/Features/Plugins/PluginManager.cs b/lib/TweetLib.Core/Features/Plugins/PluginManager.cs
index 1f3008d6..066c482c 100644
--- a/lib/TweetLib.Core/Features/Plugins/PluginManager.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginManager.cs
@@ -11,12 +11,11 @@
 
 namespace TweetLib.Core.Features.Plugins {
 	public sealed class PluginManager {
-		public string CustomPluginFolder => Path.Combine(App.PluginPath, PluginGroup.Custom.GetSubFolder());
-
 		public IEnumerable<Plugin> Plugins => plugins;
 		public IEnumerable<InjectedString> NotificationInjections => bridge.NotificationInjections;
 
-		public IPluginConfig Config { get; }
+		public PluginConfig Config { get; }
+		public string PluginFolder { get; }
 		public string PluginDataFolder { get; }
 
 		public event EventHandler<PluginErrorEventArgs>? Reloaded;
@@ -27,13 +26,18 @@ public sealed class PluginManager {
 
 		private readonly HashSet<Plugin> plugins = new ();
 
-		public PluginManager(IPluginConfig config, string pluginDataFolder) {
+		public PluginManager(PluginConfig config, string pluginFolder, string pluginDataFolder) {
 			this.Config = config;
 			this.Config.PluginChangedState += Config_PluginChangedState;
+			this.PluginFolder = pluginFolder;
 			this.PluginDataFolder = pluginDataFolder;
 			this.bridge = new PluginBridge(this);
 		}
 
+		public string GetPluginFolder(PluginGroup group) {
+			return Path.Combine(PluginFolder, group.GetSubFolder());
+		}
+
 		internal void Register(PluginEnvironment environment, IBrowserComponent browserComponent) {
 			browserComponent.AttachBridgeObject("$TDP", bridge);
 
@@ -47,7 +51,7 @@ public void Reload() {
 
 			var errors = new List<string>(1);
 
-			foreach (var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(App.PluginPath, PluginDataFolder, group))) {
+			foreach (var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(PluginFolder, PluginDataFolder, group))) {
 				if (result.HasValue) {
 					plugins.Add(result.Value);
 				}
@@ -111,7 +115,7 @@ public void ConfigurePlugin(Plugin plugin) {
 				browserExecutor.RunFunction("TDPF_configurePlugin", plugin);
 			}
 			else if (plugin.HasConfig) {
-				App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(PluginFolder.Data));
+				App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(Enums.PluginFolder.Data));
 			}
 		}
 	}
diff --git a/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs b/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs
index 0ae9f242..0bb37525 100644
--- a/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs
@@ -2,22 +2,25 @@
 using System.Linq;
 using System.Net;
 using TweetLib.Browser.Interfaces;
+using TweetLib.Browser.Request;
 using TweetLib.Core.Features.Plugins.Enums;
 using TweetLib.Core.Resources;
 
 namespace TweetLib.Core.Features.Plugins {
-	public sealed class PluginSchemeHandler<T> : ICustomSchemeHandler<T> where T : class {
+	public sealed class PluginSchemeHandler : ICustomSchemeHandler {
+		private static readonly SchemeResource PathMustBeRelativeToRoot = new SchemeResource.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.");
+
 		public string Protocol => "tdp";
 
-		private readonly CachingResourceProvider<T> resourceProvider;
+		private readonly ResourceCache resourceCache;
 		private readonly PluginBridge bridge;
 
-		public PluginSchemeHandler(CachingResourceProvider<T> resourceProvider, PluginManager pluginManager) {
-			this.resourceProvider = resourceProvider;
+		public PluginSchemeHandler(ResourceCache resourceCache, PluginManager pluginManager) {
+			this.resourceCache = resourceCache;
 			this.bridge = pluginManager.bridge;
 		}
 
-		public T? Resolve(Uri uri) {
+		public SchemeResource? Resolve(Uri uri) {
 			if (!uri.IsAbsoluteUri || uri.Scheme != Protocol || !int.TryParse(uri.Authority, out var identifier)) {
 				return null;
 			}
@@ -35,15 +38,15 @@ public PluginSchemeHandler(CachingResourceProvider<T> resourceProvider, PluginMa
 				}
 			}
 
-			return resourceProvider.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath);
+			return new SchemeResource.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath);
 		}
 
-		private T DoReadRootFile(int identifier, string[] segments) {
+		private SchemeResource DoReadRootFile(int identifier, string[] segments) {
 			string path = string.Join("/", segments, 1, segments.Length - 1);
 
 			Plugin? plugin = bridge.GetPluginFromToken(identifier);
 			string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(PluginFolder.Root, path);
-			return fullPath.Length == 0 ? resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.") : resourceProvider.CachedFile(fullPath);
+			return fullPath.Length == 0 ? PathMustBeRelativeToRoot : resourceCache.ReadFile(fullPath);
 		}
 	}
 }
diff --git a/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs b/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
index d649d684..dc7d96d6 100644
--- a/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
@@ -4,7 +4,7 @@
 
 namespace TweetLib.Core.Features.Plugins {
 	internal static class PluginScriptGenerator {
-		public static string GenerateConfig(IPluginConfig config) {
+		public static string GenerateConfig(PluginConfig config) {
 			return "window.TD_PLUGINS_DISABLE = [" + string.Join(",", config.DisabledPlugins.Select(static id => '"' + id + '"')) + "]";
 		}
 
diff --git a/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs b/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs
index 4a9df223..ea791b2f 100644
--- a/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs
+++ b/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs
@@ -10,7 +10,6 @@
 using TweetLib.Core.Features.Plugins.Events;
 using TweetLib.Core.Features.Twitter;
 using TweetLib.Core.Resources;
-using TweetLib.Core.Systems.Dialogs;
 using TweetLib.Core.Systems.Updates;
 using TweetLib.Utils.Static;
 using Version = TweetDuck.Version;
@@ -21,22 +20,18 @@ public sealed class TweetDeckBrowser : BaseBrowser<TweetDeckBrowser> {
 		private const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
 
 		public TweetDeckFunctions Functions { get; }
-		public FileDownloadManager FileDownloadManager => new (browserComponent);
+		public FileDownloadManager FileDownloadManager => new (browserComponent.FileDownloader);
 
 		private readonly ISoundNotificationHandler soundNotificationHandler;
 		private readonly PluginManager pluginManager;
-		private readonly UpdateChecker updateChecker;
 
 		private bool isBrowserReady;
 		private bool ignoreUpdateCheckError;
 		private string? prevSoundNotificationPath = null;
 
-		public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface tweetDeckInterface, TweetDeckExtraContext extraContext, ISoundNotificationHandler soundNotificationHandler, PluginManager pluginManager, UpdateChecker updateChecker) : base(browserComponent, CreateSetupObject) {
+		public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface tweetDeckInterface, TweetDeckExtraContext extraContext, ISoundNotificationHandler soundNotificationHandler, PluginManager pluginManager, UpdateChecker? updateChecker = null) : base(browserComponent, CreateSetupObject) {
 			this.Functions = new TweetDeckFunctions(this.browserComponent);
 
-			this.browserComponent.AttachBridgeObject("$TD", new TweetDeckBridgeObject(tweetDeckInterface, this, extraContext));
-			this.browserComponent.AttachBridgeObject("$TDU", updateChecker.InteractionManager.BridgeObject);
-
 			this.soundNotificationHandler = soundNotificationHandler;
 
 			this.pluginManager = pluginManager;
@@ -45,13 +40,17 @@ public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface
 			this.pluginManager.Executed += pluginManager_Executed;
 			this.pluginManager.Reload();
 
-			this.updateChecker = updateChecker;
-			this.updateChecker.CheckFinished += updateChecker_CheckFinished;
-
 			this.browserComponent.BrowserLoaded += browserComponent_BrowserLoaded;
 			this.browserComponent.PageLoadStart += browserComponent_PageLoadStart;
 			this.browserComponent.PageLoadEnd += browserComponent_PageLoadEnd;
 
+			this.browserComponent.AttachBridgeObject("$TD", new TweetDeckBridgeObject(tweetDeckInterface, this, extraContext));
+
+			if (updateChecker != null) {
+				updateChecker.CheckFinished += updateChecker_CheckFinished;
+				this.browserComponent.AttachBridgeObject("$TDU", updateChecker.InteractionManager.BridgeObject);
+			}
+
 			App.UserConfiguration.MuteToggled += UserConfiguration_GeneralEventHandler;
 			App.UserConfiguration.OptionsDialogClosed += UserConfiguration_GeneralEventHandler;
 			App.UserConfiguration.SoundNotificationChanged += UserConfiguration_SoundNotificationChanged;
@@ -105,7 +104,7 @@ private void browserComponent_PageLoadEnd(object sender, PageLoadEventArgs e) {
 
 		private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
 			if (e.HasErrors) {
-				App.DialogHandler.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), Dialogs.OK);
+				App.MessageDialogs.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors));
 			}
 
 			if (isBrowserReady) {
@@ -115,11 +114,13 @@ private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
 
 		private void pluginManager_Executed(object sender, PluginErrorEventArgs e) {
 			if (e.HasErrors) {
-				App.DialogHandler.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), Dialogs.OK);
+				App.MessageDialogs.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors));
 			}
 		}
 
 		private void updateChecker_CheckFinished(object sender, UpdateCheckEventArgs e) {
+			var updateChecker = (UpdateChecker) sender;
+
 			e.Result.Handle(update => {
 				string tag = update.VersionTag;
 
@@ -201,14 +202,14 @@ public override void Show(IContextMenuBuilder menu, Context context) {
 				base.Show(menu, context);
 
 				if (context.Selection == null && context.Tweet is {} tweet) {
-					menu.AddAction("Open tweet in browser", OpenLink(tweet.Url));
-					menu.AddAction("Copy tweet address", CopyText(tweet.Url));
+					AddOpenAction(menu, "Open tweet in browser", tweet.Url);
+					AddCopyAction(menu, "Copy tweet address", tweet.Url);
 					menu.AddAction("Screenshot tweet to clipboard", () => owner.Functions.TriggerTweetScreenshot(tweet.ColumnId, tweet.ChirpId));
 					menu.AddSeparator();
 
 					if (!string.IsNullOrEmpty(tweet.QuoteUrl)) {
-						menu.AddAction("Open quoted tweet in browser", OpenLink(tweet.QuoteUrl!));
-						menu.AddAction("Copy quoted tweet address", CopyText(tweet.QuoteUrl!));
+						AddOpenAction(menu, "Open quoted tweet in browser", tweet.QuoteUrl!);
+						AddCopyAction(menu, "Copy quoted tweet address", tweet.QuoteUrl!);
 						menu.AddSeparator();
 					}
 				}
diff --git a/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs b/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs
index edd0c902..a7033c2e 100644
--- a/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs
+++ b/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs
@@ -1,20 +1,24 @@
 using System;
 using System.Net;
 using TweetLib.Browser.Interfaces;
+using TweetLib.Browser.Request;
 using TweetLib.Core.Resources;
 using TweetLib.Utils.Static;
 
 namespace TweetLib.Core.Features.TweetDeck {
-	public sealed class TweetDuckSchemeHandler<T> : ICustomSchemeHandler<T> where T : class {
+	public sealed class TweetDuckSchemeHandler : ICustomSchemeHandler {
+		private static readonly SchemeResource InvalidUrl = new SchemeResource.Status(HttpStatusCode.NotFound, "Invalid URL.");
+		private static readonly SchemeResource PathMustBeRelativeToRoot = new SchemeResource.Status(HttpStatusCode.Forbidden, "File path has to be relative to the root folder.");
+
 		public string Protocol => "td";
 
-		private readonly CachingResourceProvider<T> resourceProvider;
+		private readonly ResourceCache resourceCache;
 
-		public TweetDuckSchemeHandler(CachingResourceProvider<T> resourceProvider) {
-			this.resourceProvider = resourceProvider;
+		public TweetDuckSchemeHandler(ResourceCache resourceCache) {
+			this.resourceCache = resourceCache;
 		}
 
-		public T Resolve(Uri uri) {
+		public SchemeResource Resolve(Uri uri) {
 			string? rootPath = uri.Authority switch {
 				"resources" => App.ResourcesPath,
 				"guide"     => App.GuidePath,
@@ -22,11 +26,11 @@ public T Resolve(Uri uri) {
 			};
 
 			if (rootPath == null) {
-				return resourceProvider.Status(HttpStatusCode.NotFound, "Invalid URL.");
+				return InvalidUrl;
 			}
 
 			string filePath = FileUtils.ResolveRelativePathSafely(rootPath, uri.AbsolutePath.TrimStart('/'));
-			return filePath.Length == 0 ? resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the root folder.") : resourceProvider.CachedFile(filePath);
+			return filePath.Length == 0 ? PathMustBeRelativeToRoot : resourceCache.ReadFile(filePath);
 		}
 	}
 }
diff --git a/lib/TweetLib.Core/Lib.cs b/lib/TweetLib.Core/Lib.cs
index 3b02607e..c17607cb 100644
--- a/lib/TweetLib.Core/Lib.cs
+++ b/lib/TweetLib.Core/Lib.cs
@@ -9,10 +9,13 @@
 namespace TweetLib.Core {
 	public static class Lib {
 		public const string BrandName = "TweetDuck";
+		public const string IssueTrackerUrl = "https://github.com/chylex/TweetDuck/issues";
 
 		public static CultureInfo Culture { get; } = CultureInfo.CurrentCulture;
 
-		public static void Initialize(AppBuilder app) {
+		public delegate void AppLauncher();
+
+		public static AppLauncher Initialize(AppBuilder app) {
 			Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
 			CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
 
@@ -20,7 +23,7 @@ public static void Initialize(AppBuilder app) {
 			CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
 			#endif
 
-			app.Build();
+			return app.Build();
 		}
 	}
 }
diff --git a/lib/TweetLib.Core/Resources/CachingResourceProvider.cs b/lib/TweetLib.Core/Resources/CachingResourceProvider.cs
deleted file mode 100644
index 543a98ea..00000000
--- a/lib/TweetLib.Core/Resources/CachingResourceProvider.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using TweetLib.Browser.Interfaces;
-using IOFile = System.IO.File;
-
-namespace TweetLib.Core.Resources {
-	public sealed class CachingResourceProvider<T> : IResourceProvider<T> {
-		private readonly IResourceProvider<T> resourceProvider;
-		private readonly Dictionary<string, ICachedResource> cache = new ();
-
-		public CachingResourceProvider(IResourceProvider<T> resourceProvider) {
-			this.resourceProvider = resourceProvider;
-		}
-
-		public void ClearCache() {
-			cache.Clear();
-		}
-
-		public T Status(HttpStatusCode code, string message) {
-			return resourceProvider.Status(code, message);
-		}
-
-		public T File(byte[] contents, string extension) {
-			return resourceProvider.File(contents, extension);
-		}
-
-		internal T CachedFile(string path) {
-			string key = new Uri(path).LocalPath;
-
-			if (cache.TryGetValue(key, out var cachedResource)) {
-				return cachedResource.GetResource(resourceProvider);
-			}
-
-			ICachedResource resource;
-			try {
-				resource = new CachedFileResource(IOFile.ReadAllBytes(path), Path.GetExtension(path));
-			} catch (FileNotFoundException) {
-				resource = new CachedStatusResource(HttpStatusCode.NotFound, "File not found.");
-			} catch (DirectoryNotFoundException) {
-				resource = new CachedStatusResource(HttpStatusCode.NotFound, "Directory not found.");
-			} catch (Exception e) {
-				resource = new CachedStatusResource(HttpStatusCode.InternalServerError, e.Message);
-			}
-
-			cache[key] = resource;
-			return resource.GetResource(resourceProvider);
-		}
-
-		private interface ICachedResource {
-			T GetResource(IResourceProvider<T> resourceProvider);
-		}
-
-		private sealed class CachedFileResource : ICachedResource {
-			private readonly byte[] contents;
-			private readonly string extension;
-
-			public CachedFileResource(byte[] contents, string extension) {
-				this.contents = contents;
-				this.extension = extension;
-			}
-
-			T ICachedResource.GetResource(IResourceProvider<T> resourceProvider) {
-				return resourceProvider.File(contents, extension);
-			}
-		}
-
-		private sealed class CachedStatusResource : ICachedResource {
-			private readonly HttpStatusCode code;
-			private readonly string message;
-
-			public CachedStatusResource(HttpStatusCode code, string message) {
-				this.code = code;
-				this.message = message;
-			}
-
-			public T GetResource(IResourceProvider<T> resourceProvider) {
-				return resourceProvider.Status(code, message);
-			}
-		}
-	}
-}
diff --git a/lib/TweetLib.Core/Resources/ResourceCache.cs b/lib/TweetLib.Core/Resources/ResourceCache.cs
new file mode 100644
index 00000000..ef8411a3
--- /dev/null
+++ b/lib/TweetLib.Core/Resources/ResourceCache.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using TweetLib.Browser.Request;
+using IOFile = System.IO.File;
+
+namespace TweetLib.Core.Resources {
+	public sealed class ResourceCache {
+		private readonly Dictionary<string, SchemeResource> cache = new ();
+
+		public void ClearCache() {
+			cache.Clear();
+		}
+
+		internal SchemeResource ReadFile(string path) {
+			string key = new Uri(path).LocalPath;
+
+			if (cache.TryGetValue(key, out var cachedResource)) {
+				return cachedResource;
+			}
+
+			SchemeResource resource;
+			try {
+				resource = new SchemeResource.File(IOFile.ReadAllBytes(path), Path.GetExtension(path));
+			} catch (FileNotFoundException) {
+				resource = new SchemeResource.Status(HttpStatusCode.NotFound, "File not found.");
+			} catch (DirectoryNotFoundException) {
+				resource = new SchemeResource.Status(HttpStatusCode.NotFound, "Directory not found.");
+			} catch (Exception e) {
+				resource = new SchemeResource.Status(HttpStatusCode.InternalServerError, e.Message);
+			}
+
+			cache[key] = resource;
+			return resource;
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs b/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs
index 7f439160..855cf6c6 100644
--- a/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs
@@ -2,22 +2,18 @@
 using System.Collections.Generic;
 
 namespace TweetLib.Core.Systems.Configuration {
-	public abstract class BaseConfig {
-		internal T ConstructWithDefaults<T>() where T : BaseConfig {
-			return (T) ConstructWithDefaults();
-		}
+	public abstract class BaseConfig<T> : IConfigObject<T> where T : BaseConfig<T> {
+		public abstract T ConstructWithDefaults();
 
-		protected abstract BaseConfig ConstructWithDefaults();
-
-		protected void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler? eventHandler) {
-			if (!EqualityComparer<T>.Default.Equals(field, value)) {
+		protected void UpdatePropertyWithEvent<V>(ref V field, V value, EventHandler? eventHandler) {
+			if (!EqualityComparer<V>.Default.Equals(field, value)) {
 				field = value;
 				eventHandler?.Invoke(this, EventArgs.Empty);
 			}
 		}
 
-		protected void UpdatePropertyWithCallback<T>(ref T field, T value, Action action) {
-			if (!EqualityComparer<T>.Default.Equals(field, value)) {
+		protected void UpdatePropertyWithCallback<V>(ref V field, V value, Action action) {
+			if (!EqualityComparer<V>.Default.Equals(field, value)) {
 				field = value;
 				action();
 			}
diff --git a/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs b/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
new file mode 100644
index 00000000..c8d6fc4d
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Drawing;
+using System.IO;
+using TweetLib.Core.Application;
+using TweetLib.Core.Features.Plugins.Config;
+using TweetLib.Utils.Data;
+using TweetLib.Utils.Serialization;
+using TweetLib.Utils.Serialization.Converters;
+using TweetLib.Utils.Static;
+
+namespace TweetLib.Core.Systems.Configuration {
+	public abstract class ConfigManager {
+		protected static TypeConverterRegistry ConverterRegistry { get; } = new ();
+
+		static ConfigManager() {
+			ConverterRegistry.Register(typeof(WindowState), new BasicTypeConverter<WindowState> {
+				ConvertToString = static value => $"{(value.IsMaximized ? 'M' : '_')}{value.Bounds.X} {value.Bounds.Y} {value.Bounds.Width} {value.Bounds.Height}",
+				ConvertToObject = static value => {
+					int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
+
+					return new WindowState {
+						Bounds = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
+						IsMaximized = value[0] == 'M'
+					};
+				}
+			});
+
+			ConverterRegistry.Register(typeof(Point), new BasicTypeConverter<Point> {
+				ConvertToString = static value => $"{value.X} {value.Y}",
+				ConvertToObject = static value => {
+					int[] elements = StringUtils.ParseInts(value, ' ');
+					return new Point(elements[0], elements[1]);
+				}
+			});
+
+			ConverterRegistry.Register(typeof(Size), new BasicTypeConverter<Size> {
+				ConvertToString = static value => $"{value.Width} {value.Height}",
+				ConvertToObject = static value => {
+					int[] elements = StringUtils.ParseInts(value, ' ');
+					return new Size(elements[0], elements[1]);
+				}
+			});
+		}
+
+		public string UserPath { get; }
+		public string SystemPath { get; }
+		public string PluginsPath { get; }
+
+		public event EventHandler? ProgramRestartRequested;
+
+		internal IAppUserConfiguration User { get; }
+		internal PluginConfig Plugins { get; }
+
+		protected ConfigManager(string storagePath, IAppUserConfiguration user, PluginConfig plugins) {
+			UserPath = Path.Combine(storagePath, "TD_UserConfig.cfg");
+			SystemPath = Path.Combine(storagePath, "TD_SystemConfig.cfg");
+			PluginsPath = Path.Combine(storagePath, "TD_PluginConfig.cfg");
+
+			User = user;
+			Plugins = plugins;
+		}
+
+		public abstract void LoadAll();
+		public abstract void SaveAll();
+		public abstract void ReloadAll();
+
+		internal void Save(IConfigObject instance) {
+			this.GetInstanceInfo(instance).Save();
+		}
+
+		internal void Reset(IConfigObject instance) {
+			this.GetInstanceInfo(instance).Reset();
+		}
+
+		public void TriggerProgramRestartRequested() {
+			ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
+		}
+
+		protected abstract IConfigInstance GetInstanceInfo(IConfigObject instance);
+	}
+
+	public sealed class ConfigManager<TUser, TSystem> : ConfigManager where TUser : class, IAppUserConfiguration, IConfigObject<TUser> where TSystem : class, IConfigObject<TSystem> {
+		private new TUser User { get; }
+		private TSystem System { get; }
+
+		private readonly FileConfigInstance<TUser> infoUser;
+		private readonly FileConfigInstance<TSystem> infoSystem;
+		private readonly PluginConfigInstance infoPlugins;
+
+		public ConfigManager(string storagePath, ConfigObjects<TUser, TSystem> configObjects) : base(storagePath, configObjects.User, configObjects.Plugins) {
+			User = configObjects.User;
+			System = configObjects.System;
+
+			infoUser = new FileConfigInstance<TUser>(UserPath, User, "program options", ConverterRegistry);
+			infoSystem = new FileConfigInstance<TSystem>(SystemPath, System, "system options", ConverterRegistry);
+			infoPlugins = new PluginConfigInstance(PluginsPath, Plugins);
+		}
+
+		public override void LoadAll() {
+			infoUser.Load();
+			infoSystem.Load();
+			infoPlugins.Load();
+		}
+
+		public override void SaveAll() {
+			infoUser.Save();
+			infoSystem.Save();
+			infoPlugins.Save();
+		}
+
+		public override void ReloadAll() {
+			infoUser.Reload();
+			infoSystem.Reload();
+			infoPlugins.Reload();
+		}
+
+		protected override IConfigInstance GetInstanceInfo(IConfigObject instance) {
+			if (instance == User) {
+				return infoUser;
+			}
+			else if (instance == System) {
+				return infoSystem;
+			}
+			else if (instance == Plugins) {
+				return infoPlugins;
+			}
+			else {
+				throw new ArgumentException("Invalid configuration instance: " + instance.GetType());
+			}
+		}
+	}
+
+	public static class ConfigManagerExtensions {
+		public static void Save(this IConfigObject instance) {
+			App.ConfigManager.Save(instance);
+		}
+
+		public static void Reset(this IConfigObject instance) {
+			App.ConfigManager.Reset(instance);
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs b/lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs
new file mode 100644
index 00000000..d6a2fa70
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs
@@ -0,0 +1,16 @@
+using TweetLib.Core.Application;
+using TweetLib.Core.Features.Plugins.Config;
+
+namespace TweetLib.Core.Systems.Configuration {
+	public sealed class ConfigObjects<TUser, TSystem> where TUser : IAppUserConfiguration where TSystem : IConfigObject {
+		public TUser User { get; }
+		public TSystem System { get; }
+		public PluginConfig Plugins { get; }
+
+		public ConfigObjects(TUser user, TSystem system, PluginConfig plugins) {
+			User = user;
+			System = system;
+			Plugins = plugins;
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs b/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs
index e64b9274..02a250c8 100644
--- a/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs
@@ -3,25 +3,26 @@
 using TweetLib.Utils.Serialization;
 
 namespace TweetLib.Core.Systems.Configuration {
-	public sealed class FileConfigInstance<T> : IConfigInstance<T> where T : BaseConfig {
+	internal sealed class FileConfigInstance<T> : IConfigInstance where T : IConfigObject<T> {
 		public T Instance { get; }
-		public SimpleObjectSerializer<T> Serializer { get; }
+
+		private readonly SimpleObjectSerializer<T> serializer;
 
 		private readonly string filenameMain;
 		private readonly string filenameBackup;
 		private readonly string identifier;
 
-		public FileConfigInstance(string filename, T instance, string identifier) {
+		public FileConfigInstance(string filename, T instance, string identifier, TypeConverterRegistry converterRegistry) {
+			this.Instance = instance;
+			this.serializer = new SimpleObjectSerializer<T>(converterRegistry);
+
 			this.filenameMain = filename ?? throw new ArgumentNullException(nameof(filename), "Config file name must not be null!");
 			this.filenameBackup = filename + ".bak";
 			this.identifier = identifier;
-
-			this.Instance = instance;
-			this.Serializer = new SimpleObjectSerializer<T>();
 		}
 
 		private void LoadInternal(bool backup) {
-			Serializer.Read(backup ? filenameBackup : filenameMain, Instance);
+			serializer.Read(backup ? filenameBackup : filenameMain, Instance);
 		}
 
 		public void Load() {
@@ -63,7 +64,7 @@ public void Save() {
 					File.Move(filenameMain, filenameBackup);
 				}
 
-				Serializer.Write(filenameMain, Instance);
+				serializer.Write(filenameMain, Instance);
 			} catch (SerializationSoftException e) {
 				OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e);
 			} catch (Exception e) {
@@ -76,7 +77,7 @@ public void Reload() {
 				LoadInternal(false);
 			} catch (FileNotFoundException) {
 				try {
-					Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>());
+					serializer.Write(filenameMain, Instance.ConstructWithDefaults());
 					LoadInternal(false);
 				} catch (Exception e) {
 					OnException($"Could not regenerate the configuration file for {identifier}.", e);
diff --git a/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs b/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs
index cde35044..566e4568 100644
--- a/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs
@@ -1,9 +1,6 @@
 namespace TweetLib.Core.Systems.Configuration {
-	public interface IConfigInstance<out T> {
-		T Instance { get; }
-
+	public interface IConfigInstance {
 		void Save();
-		void Reload();
 		void Reset();
 	}
 }
diff --git a/lib/TweetLib.Core/Systems/Configuration/IConfigManager.cs b/lib/TweetLib.Core/Systems/Configuration/IConfigManager.cs
deleted file mode 100644
index acf534ad..00000000
--- a/lib/TweetLib.Core/Systems/Configuration/IConfigManager.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace TweetLib.Core.Systems.Configuration {
-	public interface IConfigManager {
-		IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
-	}
-}
diff --git a/lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs b/lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs
new file mode 100644
index 00000000..3374d12e
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs
@@ -0,0 +1,7 @@
+namespace TweetLib.Core.Systems.Configuration {
+	public interface IConfigObject {}
+
+	public interface IConfigObject<T> : IConfigObject where T : IConfigObject<T> {
+		T ConstructWithDefaults();
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Logging/Logger.cs b/lib/TweetLib.Core/Systems/Logging/Logger.cs
new file mode 100644
index 00000000..53221d3c
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Logging/Logger.cs
@@ -0,0 +1,57 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace TweetLib.Core.Systems.Logging {
+	public sealed class Logger {
+		public string LogFilePath { get; }
+
+		private readonly bool debug;
+
+		internal Logger(string logPath, bool debug) {
+			this.LogFilePath = logPath;
+			this.debug = debug;
+			#if DEBUG
+			this.debug = true;
+			#endif
+		}
+
+		public bool Debug(string message) {
+			return debug && Log("DEBUG", message);
+		}
+
+		public bool Info(string message) {
+			return Log("INFO", message);
+		}
+
+		public bool Warn(string message) {
+			return Log("WARN", message);
+		}
+
+		public bool Error(string message) {
+			return Log("ERROR", message);
+		}
+
+		private bool Log(string level, string message) {
+			#if DEBUG
+			System.Diagnostics.Debug.WriteLine("[" + level + "] " + message);
+			#endif
+
+			StringBuilder build = new StringBuilder();
+
+			if (!File.Exists(LogFilePath)) {
+				build.Append("Please, report all issues to: ").Append(Lib.IssueTrackerUrl).Append("\r\n\r\n");
+			}
+
+			build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("] ").Append(level).Append("\r\n");
+			build.Append(message).Append("\r\n\r\n");
+
+			try {
+				File.AppendAllText(LogFilePath, build.ToString(), Encoding.UTF8);
+				return true;
+			} catch {
+				return false;
+			}
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs b/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs
index 587214ac..4eb86ac2 100644
--- a/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs
+++ b/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs
@@ -2,7 +2,6 @@
 using System.IO;
 using System.Net;
 using TweetLib.Utils.Static;
-using Version = TweetDuck.Version;
 
 namespace TweetLib.Core.Systems.Updates {
 	public sealed class UpdateInfo {
@@ -49,7 +48,7 @@ internal void BeginSilentDownload() {
 					return;
 				}
 
-				WebClient client = WebUtils.NewClient($"{Lib.BrandName} {Version.Tag}");
+				WebClient client = WebUtils.NewClient();
 
 				client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
 					DownloadStatus = UpdateDownloadStatus.Done;
diff --git a/lib/TweetLib.Utils/Data/WindowState.cs b/lib/TweetLib.Utils/Data/WindowState.cs
new file mode 100644
index 00000000..5c399da5
--- /dev/null
+++ b/lib/TweetLib.Utils/Data/WindowState.cs
@@ -0,0 +1,8 @@
+using System.Drawing;
+
+namespace TweetLib.Utils.Data {
+	public sealed class WindowState {
+		public Rectangle Bounds { get; set; }
+		public bool IsMaximized { get; set; }
+	}
+}
diff --git a/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs b/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs
index 332efae5..a48a4725 100644
--- a/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs
+++ b/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs
@@ -50,16 +50,12 @@ private static string UnescapeStream(StreamReader reader) {
 			return build.Append(data.Substring(index)).ToString();
 		}
 
+		private readonly TypeConverterRegistry converterRegistry;
 		private readonly Dictionary<string, PropertyInfo> props;
-		private readonly Dictionary<Type, ITypeConverter> converters;
 
-		public SimpleObjectSerializer() {
+		public SimpleObjectSerializer(TypeConverterRegistry converterRegistry) {
+			this.converterRegistry = converterRegistry;
 			this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(static prop => prop.CanWrite).ToDictionary(static 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) {
@@ -69,14 +65,10 @@ 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);
+					var type = prop.Value.PropertyType;
+					var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
 
-					if (!converters.TryGetValue(type, out ITypeConverter serializer)) {
-						serializer = ClrTypeConverter.Instance;
-					}
-
-					if (serializer.TryWriteType(type, value, out string? converted)) {
+					if (converter.TryWriteType(type, prop.Value.GetValue(obj), out string? converted)) {
 						if (converted != null) {
 							writer.Write(prop.Key);
 							writer.Write(' ');
@@ -140,11 +132,10 @@ public void Read(string file, T obj) {
 				string value = UnescapeLine(line.Substring(space + 1));
 
 				if (props.TryGetValue(property, out PropertyInfo info)) {
-					if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)) {
-						serializer = ClrTypeConverter.Instance;
-					}
+					var type = info.PropertyType;
+					var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
 
-					if (serializer.TryReadType(info.PropertyType, value, out object? converted)) {
+					if (converter.TryReadType(type, value, out object? converted)) {
 						info.SetValue(obj, converted);
 					}
 					else {
diff --git a/lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs b/lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs
new file mode 100644
index 00000000..ea2fee13
--- /dev/null
+++ b/lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace TweetLib.Utils.Serialization {
+	public sealed class TypeConverterRegistry {
+		private readonly Dictionary<Type, ITypeConverter> converters = new ();
+
+		public void Register(Type type, ITypeConverter converter) {
+			converters[type] = converter;
+		}
+
+		public ITypeConverter? TryGet(Type type) {
+			return converters.TryGetValue(type, out var converter) ? converter : null;
+		}
+	}
+}