diff --git a/Browser/Adapters/CefBrowserComponent.cs b/Browser/Adapters/CefBrowserComponent.cs
deleted file mode 100644
index 88e19cb2..00000000
--- a/Browser/Adapters/CefBrowserComponent.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-using System;
-using System.IO;
-using CefSharp;
-using CefSharp.WinForms;
-using TweetDuck.Browser.Handling;
-using TweetDuck.Management;
-using TweetDuck.Utils;
-using TweetLib.Browser.Base;
-using TweetLib.Browser.Events;
-using TweetLib.Browser.Interfaces;
-using TweetLib.Utils.Static;
-using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
-using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
-
-namespace TweetDuck.Browser.Adapters {
-	abstract class CefBrowserComponent : IBrowserComponent {
-		public bool Ready { get; private set; }
-
-		public string Url => browser.Address;
-		public string CacheFolder => BrowserCache.CacheFolder;
-
-		public event EventHandler<BrowserLoadedEventArgs> BrowserLoaded;
-		public event EventHandler<PageLoadEventArgs> PageLoadStart;
-		public event EventHandler<PageLoadEventArgs> PageLoadEnd;
-
-		private readonly ChromiumWebBrowser browser;
-
-		protected CefBrowserComponent(ChromiumWebBrowser browser) {
-			this.browser = browser;
-			this.browser.JsDialogHandler = new JavaScriptDialogHandler();
-			this.browser.LifeSpanHandler = new CustomLifeSpanHandler();
-			this.browser.LoadingStateChanged += OnLoadingStateChanged;
-			this.browser.LoadError += OnLoadError;
-			this.browser.FrameLoadStart += OnFrameLoadStart;
-			this.browser.FrameLoadEnd += OnFrameLoadEnd;
-			this.browser.SetupZoomEvents();
-		}
-
-		void IBrowserComponent.Setup(BrowserSetup setup) {
-			browser.MenuHandler = SetupContextMenu(setup.ContextMenuHandler);
-			browser.ResourceRequestHandlerFactory = SetupResourceHandlerFactory(setup.ResourceRequestHandler);
-		}
-
-		protected abstract ContextMenuBase SetupContextMenu(IContextMenuHandler handler);
-
-		protected abstract CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler);
-
-		private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
-			if (!e.IsLoading) {
-				Ready = true;
-				browser.LoadingStateChanged -= OnLoadingStateChanged;
-				BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser));
-				BrowserLoaded = null;
-			}
-		}
-
-		private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs {
-			private readonly IWebBrowser browser;
-
-			public BrowserLoadedEventArgsImpl(IWebBrowser browser) {
-				this.browser = browser;
-			}
-
-			public override void AddDictionaryWords(params string[] words) {
-				foreach (string word in words) {
-					browser.AddWordToDictionary(word);
-				}
-			}
-		}
-
-		private void OnLoadError(object sender, LoadErrorEventArgs e) {
-			if (e.ErrorCode == CefErrorCode.Aborted) {
-				return;
-			}
-
-			if (!e.FailedUrl.StartsWithOrdinal("td://resources/error/")) {
-				string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
-				string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
-				browser.Load("td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle));
-			}
-		}
-
-		private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) {
-			if (e.Frame.IsMain) {
-				PageLoadStart?.Invoke(this, new PageLoadEventArgs(e.Url));
-			}
-		}
-
-		private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
-			if (e.Frame.IsMain) {
-				PageLoadEnd?.Invoke(this, new PageLoadEventArgs(e.Url));
-			}
-		}
-
-		public void AttachBridgeObject(string name, object bridge) {
-			browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
-			browser.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder);
-		}
-
-		public void RunScript(string identifier, string script) {
-			using IFrame frame = browser.GetMainFrame();
-			frame.ExecuteJavaScriptAsync(script, identifier, 1);
-		}
-
-		public void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onError) {
-			Cef.UIThreadTaskFactory.StartNew(() => {
-				try {
-					using IFrame frame = browser.GetMainFrame();
-					var request = frame.CreateRequest(false);
-
-					request.Method = "GET";
-					request.Url = url;
-					request.Flags = UrlRequestFlags.AllowStoredCredentials;
-					request.SetReferrer(Url, ReferrerPolicy.Default);
-
-					var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
-					var client = new DownloadRequestClient(fileStream, onSuccess, onError);
-					frame.CreateUrlRequest(request, client);
-				} catch (Exception e) {
-					onError?.Invoke(e);
-				}
-			});
-		}
-	}
-}
diff --git a/Browser/Adapters/CefContextMenuActionRegistry.cs b/Browser/Adapters/CefContextMenuActionRegistry.cs
deleted file mode 100644
index ae07c9dc..00000000
--- a/Browser/Adapters/CefContextMenuActionRegistry.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Generic;
-using CefSharp;
-
-namespace TweetDuck.Browser.Adapters {
-	sealed class CefContextMenuActionRegistry {
-		private readonly Dictionary<CefMenuCommand, Action> actions = new Dictionary<CefMenuCommand, Action>();
-
-		public CefMenuCommand AddAction(Action action) {
-			CefMenuCommand id = CefMenuCommand.UserFirst + 500 + actions.Count;
-			actions[id] = action;
-			return id;
-		}
-
-		public bool Execute(CefMenuCommand id) {
-			if (actions.TryGetValue(id, out var action)) {
-				action();
-				return true;
-			}
-
-			return false;
-		}
-
-		public void Clear() {
-			actions.Clear();
-		}
-	}
-}
diff --git a/Browser/Adapters/CefResourceHandlerFactory.cs b/Browser/Adapters/CefResourceHandlerFactory.cs
deleted file mode 100644
index ad41974e..00000000
--- a/Browser/Adapters/CefResourceHandlerFactory.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using CefSharp;
-using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
-
-namespace TweetDuck.Browser.Adapters {
-	sealed class CefResourceHandlerFactory : IResourceRequestHandlerFactory {
-		bool IResourceRequestHandlerFactory.HasHandlers => registry != null;
-
-		private readonly CefResourceRequestHandler resourceRequestHandler;
-		private readonly CefResourceHandlerRegistry registry;
-
-		public CefResourceHandlerFactory(IResourceRequestHandler resourceRequestHandler, CefResourceHandlerRegistry registry) {
-			this.resourceRequestHandler = new CefResourceRequestHandler(registry, resourceRequestHandler);
-			this.registry = registry;
-		}
-
-		[SuppressMessage("ReSharper", "RedundantAssignment")]
-		CefSharp.IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) {
-			disableDefaultHandling = registry != null && registry.HasHandler(request.Url);
-			return resourceRequestHandler;
-		}
-	}
-}
diff --git a/Browser/Adapters/CefResourceHandlerRegistry.cs b/Browser/Adapters/CefResourceHandlerRegistry.cs
deleted file mode 100644
index b0e97e6b..00000000
--- a/Browser/Adapters/CefResourceHandlerRegistry.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Text;
-using CefSharp;
-
-namespace TweetDuck.Browser.Adapters {
-	sealed class CefResourceHandlerRegistry {
-		private readonly ConcurrentDictionary<string, Func<IResourceHandler>> resourceHandlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase);
-
-		public bool HasHandler(string url) {
-			return resourceHandlers.ContainsKey(url);
-		}
-
-		public IResourceHandler GetHandler(string url) {
-			return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null;
-		}
-
-		private void Register(string url, Func<IResourceHandler> factory) {
-			if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
-				throw new ArgumentException("Resource handler URL must be absolute!");
-			}
-
-			resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory);
-		}
-
-		public void RegisterStatic(string url, byte[] staticData, string mimeType = ResourceHandler.DefaultMimeType) {
-			Register(url, () => ResourceHandler.FromByteArray(staticData, mimeType));
-		}
-
-		public void RegisterStatic(string url, string staticData, string mimeType = ResourceHandler.DefaultMimeType) {
-			Register(url, () => ResourceHandler.FromString(staticData, Encoding.UTF8, mimeType: mimeType));
-		}
-
-		public void RegisterDynamic(string url, IResourceHandler handler) {
-			Register(url, () => handler);
-		}
-
-		public void Unregister(string url) {
-			resourceHandlers.TryRemove(url, out _);
-		}
-	}
-}
diff --git a/Browser/Adapters/CefResourceRequestHandler.cs b/Browser/Adapters/CefResourceRequestHandler.cs
deleted file mode 100644
index 71915076..00000000
--- a/Browser/Adapters/CefResourceRequestHandler.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System.Collections.Generic;
-using CefSharp;
-using CefSharp.Handler;
-using TweetDuck.Browser.Handling;
-using TweetLib.Browser.Interfaces;
-using TweetLib.Browser.Request;
-using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
-using ResourceType = TweetLib.Browser.Request.ResourceType;
-
-namespace TweetDuck.Browser.Adapters {
-	sealed class CefResourceRequestHandler : ResourceRequestHandler {
-		private readonly CefResourceHandlerRegistry resourceHandlerRegistry;
-		private readonly IResourceRequestHandler resourceRequestHandler;
-		private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new Dictionary<ulong, IResponseProcessor>();
-
-		public CefResourceRequestHandler(CefResourceHandlerRegistry resourceHandlerRegistry, IResourceRequestHandler resourceRequestHandler) {
-			this.resourceHandlerRegistry = resourceHandlerRegistry;
-			this.resourceRequestHandler = resourceRequestHandler;
-		}
-
-		protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
-			if (request.ResourceType == CefSharp.ResourceType.CspReport) {
-				callback.Dispose();
-				return CefReturnValue.Cancel;
-			}
-
-			if (resourceRequestHandler != null) {
-				var result = resourceRequestHandler.Handle(request.Url, TranslateResourceType(request.ResourceType));
-
-				switch (result) {
-					case RequestHandleResult.Redirect redirect:
-						request.Url = redirect.Url;
-						break;
-
-					case RequestHandleResult.Process process:
-						request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true);
-						responseProcessors[request.Identifier] = process.Processor;
-						break;
-
-					case RequestHandleResult.Cancel _:
-						callback.Dispose();
-						return CefReturnValue.Cancel;
-				}
-			}
-
-			return base.OnBeforeResourceLoad(chromiumWebBrowser, browser, frame, request, callback);
-		}
-
-		protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) {
-			return resourceHandlerRegistry?.GetHandler(request.Url);
-		}
-
-		protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
-			if (responseProcessors.TryGetValue(request.Identifier, out var processor) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)) {
-				return new ResponseFilter(processor, totalBytes);
-			}
-
-			return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
-		}
-
-		protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) {
-			responseProcessors.Remove(request.Identifier);
-			base.OnResourceLoadComplete(chromiumWebBrowser, browser, frame, request, response, status, receivedContentLength);
-		}
-
-		private static ResourceType TranslateResourceType(CefSharp.ResourceType resourceType) {
-			return resourceType switch {
-				CefSharp.ResourceType.MainFrame  => ResourceType.MainFrame,
-				CefSharp.ResourceType.Script     => ResourceType.Script,
-				CefSharp.ResourceType.Stylesheet => ResourceType.Stylesheet,
-				CefSharp.ResourceType.Xhr        => ResourceType.Xhr,
-				CefSharp.ResourceType.Image      => ResourceType.Image,
-				_                                => ResourceType.Unknown
-			};
-		}
-	}
-}
diff --git a/Browser/Adapters/CefSchemeResourceVisitor.cs b/Browser/Adapters/CefSchemeResourceVisitor.cs
deleted file mode 100644
index 1495e861..00000000
--- a/Browser/Adapters/CefSchemeResourceVisitor.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.IO;
-using System.Net;
-using CefSharp;
-using TweetLib.Browser.Interfaces;
-using TweetLib.Browser.Request;
-
-namespace TweetDuck.Browser.Adapters {
-	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(Array.Empty<byte>());
-			handler.StatusCode = (int) status.Code;
-			handler.StatusText = status.Message;
-			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) {
-			return ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
-		}
-	}
-}
diff --git a/Browser/Base/CefBrowserAdapter.cs b/Browser/Base/CefBrowserAdapter.cs
new file mode 100644
index 00000000..456d06a4
--- /dev/null
+++ b/Browser/Base/CefBrowserAdapter.cs
@@ -0,0 +1,20 @@
+using CefSharp;
+using CefSharp.WinForms;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefBrowserAdapter : IBrowserWrapper<IFrame> {
+		public string Url => browser.Address;
+		public IFrame MainFrame => browser.GetMainFrame();
+
+		private readonly ChromiumWebBrowser browser;
+
+		public CefBrowserAdapter(ChromiumWebBrowser browser) {
+			this.browser = browser;
+		}
+
+		public void AddWordToDictionary(string word) {
+			browser.AddWordToDictionary(word);
+		}
+	}
+}
diff --git a/Browser/Base/CefBrowserComponent.cs b/Browser/Base/CefBrowserComponent.cs
new file mode 100644
index 00000000..fd25638f
--- /dev/null
+++ b/Browser/Base/CefBrowserComponent.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using CefSharp;
+using CefSharp.WinForms;
+using TweetDuck.Browser.Handling;
+using TweetDuck.Management;
+using TweetDuck.Utils;
+using TweetLib.Browser.Base;
+using TweetLib.Browser.CEF.Component;
+using TweetLib.Browser.CEF.Data;
+using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefBrowserComponent : BrowserComponent<IFrame> {
+		public delegate ContextMenuBase CreateContextMenu(IContextMenuHandler handler);
+
+		private static readonly CreateContextMenu DefaultContextMenuFactory = handler => new ContextMenuBase(handler);
+
+		public override string CacheFolder => BrowserCache.CacheFolder;
+
+		public ResourceHandlerRegistry<IResourceHandler> ResourceHandlerRegistry { get; } = new ResourceHandlerRegistry<IResourceHandler>(CefResourceHandlerFactory.Instance);
+
+		private readonly ChromiumWebBrowser browser;
+		private readonly bool autoReload;
+
+		private CreateContextMenu createContextMenu;
+
+		public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(new CefBrowserAdapter(browser), CefFrameAdapter.Instance) {
+			this.browser = browser;
+			this.browser.LoadingStateChanged += OnLoadingStateChanged;
+			this.browser.LoadError += OnLoadError;
+			this.browser.FrameLoadStart += OnFrameLoadStart;
+			this.browser.FrameLoadEnd += OnFrameLoadEnd;
+			this.browser.SetupZoomEvents();
+			this.createContextMenu = createContextMenu ?? DefaultContextMenuFactory;
+			this.autoReload = autoReload;
+		}
+
+		public override void Setup(BrowserSetup setup) {
+			var lifeSpanHandler = new CefLifeSpanHandler(PopupHandler.Instance);
+			var requestHandler = new CefRequestHandler(lifeSpanHandler, autoReload);
+
+			browser.DragHandler = new CefDragHandler(requestHandler, this);
+			browser.JsDialogHandler = new CustomJsDialogHandler();
+			browser.LifeSpanHandler = lifeSpanHandler;
+			browser.MenuHandler = createContextMenu(setup.ContextMenuHandler);
+			browser.RequestHandler = requestHandler;
+			browser.ResourceRequestHandlerFactory = new CefResourceRequestHandlerFactory(setup.ResourceRequestHandler, ResourceHandlerRegistry);
+
+			createContextMenu = null;
+		}
+
+		public override void AttachBridgeObject(string name, object bridge) {
+			browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
+			browser.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder);
+		}
+
+		private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
+			base.OnLoadingStateChanged(e.IsLoading);
+		}
+
+		private void OnLoadError(object sender, LoadErrorEventArgs e) {
+			base.OnLoadError(e.FailedUrl, e.ErrorCode, CefErrorCodeAdapter.Instance);
+		}
+
+		private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) {
+			base.OnFrameLoadStart(e.Url, e.Frame);
+		}
+
+		private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
+			base.OnFrameLoadEnd(e.Url, e.Frame);
+		}
+
+		public override void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onError) {
+			Cef.UIThreadTaskFactory.StartNew(() => {
+				try {
+					using IFrame frame = browser.GetMainFrame();
+					var request = frame.CreateRequest(false);
+
+					request.Method = "GET";
+					request.Url = url;
+					request.Flags = UrlRequestFlags.AllowStoredCredentials;
+					request.SetReferrer(Url, ReferrerPolicy.Default);
+
+					var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
+					var client = new CefDownloadRequestClient(fileStream, onSuccess, onError);
+					frame.CreateUrlRequest(request, client);
+				} catch (Exception e) {
+					onError?.Invoke(e);
+				}
+			});
+		}
+	}
+}
diff --git a/Browser/Base/CefByteArrayResourceHandler.cs b/Browser/Base/CefByteArrayResourceHandler.cs
new file mode 100644
index 00000000..059f49dc
--- /dev/null
+++ b/Browser/Base/CefByteArrayResourceHandler.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using CefSharp;
+using CefSharp.Callback;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Logic;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefByteArrayResourceHandler : IResourceHandler {
+		private static readonly ByteArrayResourceHandlerLogic<IResponse>.WriteToOut<Stream> WriteToOut = delegate (Stream dataOut, byte[] dataIn, int position, int length) {
+			dataOut.Write(dataIn, position, length);
+		};
+
+		private ByteArrayResourceHandlerLogic<IResponse> logic;
+
+		public CefByteArrayResourceHandler() {
+			SetResource(new ByteArrayResource(Array.Empty<byte>()));
+		}
+
+		public CefByteArrayResourceHandler(ByteArrayResource resource) {
+			SetResource(resource);
+		}
+
+		public void SetResource(ByteArrayResource resource) {
+			this.logic = new ByteArrayResourceHandlerLogic<IResponse>(resource, CefResponseAdapter.Instance);
+		}
+
+		bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) {
+			return logic.Open(out handleRequest);
+		}
+
+		void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) {
+			logic.GetResponseHeaders(response, out responseLength, out redirectUrl);
+		}
+
+		bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) {
+			return logic.Skip(bytesToSkip, out bytesSkipped, callback);
+		}
+
+		bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) {
+			return logic.Read(WriteToOut, dataOut, out bytesRead, callback);
+		}
+
+		bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) {
+			throw new NotSupportedException();
+		}
+
+		bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) {
+			throw new NotSupportedException();
+		}
+
+		void IResourceHandler.Cancel() {}
+		void IDisposable.Dispose() {}
+	}
+}
diff --git a/Browser/Adapters/CefContextMenuModel.cs b/Browser/Base/CefContextMenuModel.cs
similarity index 90%
rename from Browser/Adapters/CefContextMenuModel.cs
rename to Browser/Base/CefContextMenuModel.cs
index 77f8fc9a..b4dc2551 100644
--- a/Browser/Adapters/CefContextMenuModel.cs
+++ b/Browser/Base/CefContextMenuModel.cs
@@ -1,16 +1,17 @@
 using System;
 using CefSharp;
+using TweetLib.Browser.CEF.Data;
 using TweetLib.Browser.Contexts;
 using TweetLib.Browser.Interfaces;
 using TweetLib.Core.Features.TweetDeck;
 using TweetLib.Core.Features.Twitter;
 
-namespace TweetDuck.Browser.Adapters {
+namespace TweetDuck.Browser.Base {
 	sealed class CefContextMenuModel : IContextMenuBuilder {
 		private readonly IMenuModel model;
-		private readonly CefContextMenuActionRegistry actionRegistry;
+		private readonly ContextMenuActionRegistry<CefMenuCommand> actionRegistry;
 
-		public CefContextMenuModel(IMenuModel model, CefContextMenuActionRegistry actionRegistry) {
+		public CefContextMenuModel(IMenuModel model, ContextMenuActionRegistry<CefMenuCommand> actionRegistry) {
 			this.model = model;
 			this.actionRegistry = actionRegistry;
 		}
diff --git a/Browser/Base/CefDownloadRequestClient.cs b/Browser/Base/CefDownloadRequestClient.cs
new file mode 100644
index 00000000..2a3a0954
--- /dev/null
+++ b/Browser/Base/CefDownloadRequestClient.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using CefSharp;
+using TweetLib.Browser.CEF.Logic;
+using static TweetLib.Browser.CEF.Logic.DownloadRequestClientLogic.RequestStatus;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefDownloadRequestClient : UrlRequestClient {
+		private readonly DownloadRequestClientLogic logic;
+
+		public CefDownloadRequestClient(FileStream fileStream, Action onSuccess, Action<Exception> onError) {
+			this.logic = new DownloadRequestClientLogic(fileStream, onSuccess, onError);
+		}
+
+		protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) {
+			return logic.GetAuthCredentials(callback);
+		}
+
+		protected override void OnDownloadData(IUrlRequest request, Stream data) {
+			logic.OnDownloadData(data);
+		}
+
+		protected override void OnRequestComplete(IUrlRequest request) {
+			logic.OnRequestComplete(request.RequestStatus switch {
+				UrlRequestStatus.Success => Success,
+				UrlRequestStatus.Failed  => Failed,
+				_                        => Unknown
+			});
+		}
+	}
+}
diff --git a/Browser/Base/CefDragDataAdapter.cs b/Browser/Base/CefDragDataAdapter.cs
new file mode 100644
index 00000000..89fdc1c1
--- /dev/null
+++ b/Browser/Base/CefDragDataAdapter.cs
@@ -0,0 +1,26 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefDragDataAdapter : IDragDataAdapter<IDragData> {
+		public static CefDragDataAdapter Instance { get; } = new CefDragDataAdapter();
+
+		private CefDragDataAdapter() {}
+
+		public bool IsLink(IDragData data) {
+			return data.IsLink;
+		}
+
+		public string GetLink(IDragData data) {
+			return data.LinkUrl;
+		}
+
+		public bool IsFragment(IDragData data) {
+			return data.IsFragment;
+		}
+
+		public string GetFragmentAsText(IDragData data) {
+			return data.FragmentText;
+		}
+	}
+}
diff --git a/Browser/Base/CefDragHandler.cs b/Browser/Base/CefDragHandler.cs
new file mode 100644
index 00000000..09b1a679
--- /dev/null
+++ b/Browser/Base/CefDragHandler.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using CefSharp;
+using CefSharp.Enums;
+using TweetLib.Browser.CEF.Logic;
+using TweetLib.Browser.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefDragHandler : IDragHandler {
+		private readonly DragHandlerLogic<IDragData, IRequest> logic;
+
+		public CefDragHandler(CefRequestHandler requestHandler, IScriptExecutor executor) {
+			this.logic = new DragHandlerLogic<IDragData, IRequest>(executor, requestHandler.Logic, CefDragDataAdapter.Instance);
+		}
+
+		public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask) {
+			return logic.OnDragEnter(dragData);
+		}
+
+		public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {}
+	}
+}
diff --git a/Browser/Base/CefErrorCodeAdapter.cs b/Browser/Base/CefErrorCodeAdapter.cs
new file mode 100644
index 00000000..85876246
--- /dev/null
+++ b/Browser/Base/CefErrorCodeAdapter.cs
@@ -0,0 +1,19 @@
+using System;
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefErrorCodeAdapter : IErrorCodeAdapter<CefErrorCode> {
+		public static CefErrorCodeAdapter Instance { get; } = new CefErrorCodeAdapter();
+
+		private CefErrorCodeAdapter() {}
+
+		public bool IsAborted(CefErrorCode errorCode) {
+			return errorCode == CefErrorCode.Aborted;
+		}
+
+		public string GetName(CefErrorCode errorCode) {
+			return Enum.GetName(typeof(CefErrorCode), errorCode);
+		}
+	}
+}
diff --git a/Browser/Base/CefFrameAdapter.cs b/Browser/Base/CefFrameAdapter.cs
new file mode 100644
index 00000000..d78a32ed
--- /dev/null
+++ b/Browser/Base/CefFrameAdapter.cs
@@ -0,0 +1,26 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefFrameAdapter : IFrameAdapter<IFrame> {
+		public static CefFrameAdapter Instance { get; } = new CefFrameAdapter();
+
+		private CefFrameAdapter() {}
+
+		public bool IsValid(IFrame frame) {
+			return frame.IsValid;
+		}
+
+		public bool IsMain(IFrame frame) {
+			return frame.IsMain;
+		}
+
+		public void LoadUrl(IFrame frame, string url) {
+			frame.LoadUrl(url);
+		}
+
+		public void ExecuteJavaScriptAsync(IFrame frame, string script, string identifier, int startLine = 1) {
+			frame.ExecuteJavaScriptAsync(script, identifier, startLine);
+		}
+	}
+}
diff --git a/Browser/Base/CefLifeSpanHandler.cs b/Browser/Base/CefLifeSpanHandler.cs
new file mode 100644
index 00000000..28f5c76b
--- /dev/null
+++ b/Browser/Base/CefLifeSpanHandler.cs
@@ -0,0 +1,34 @@
+using CefSharp;
+using CefSharp.Handler;
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.CEF.Logic;
+using static TweetLib.Browser.CEF.Logic.LifeSpanHandlerLogic.TargetDisposition;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefLifeSpanHandler : LifeSpanHandler {
+		public LifeSpanHandlerLogic Logic { get; }
+
+		public CefLifeSpanHandler(IPopupHandler popupHandler) {
+			this.Logic = new LifeSpanHandlerLogic(popupHandler);
+		}
+
+		protected override bool OnBeforePopup(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) {
+			newBrowser = null;
+			return Logic.OnBeforePopup(targetUrl, ConvertTargetDisposition(targetDisposition));
+		}
+
+		protected override bool DoClose(IWebBrowser chromiumWebBrowser, IBrowser browser) {
+			return Logic.DoClose();
+		}
+
+		public static LifeSpanHandlerLogic.TargetDisposition ConvertTargetDisposition(WindowOpenDisposition targetDisposition) {
+			return targetDisposition switch {
+				WindowOpenDisposition.NewBackgroundTab => NewBackgroundTab,
+				WindowOpenDisposition.NewForegroundTab => NewForegroundTab,
+				WindowOpenDisposition.NewPopup         => NewPopup,
+				WindowOpenDisposition.NewWindow        => NewWindow,
+				_                                      => Other
+			};
+		}
+	}
+}
diff --git a/Browser/Base/CefRequestAdapter.cs b/Browser/Base/CefRequestAdapter.cs
new file mode 100644
index 00000000..2722d6b7
--- /dev/null
+++ b/Browser/Base/CefRequestAdapter.cs
@@ -0,0 +1,46 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+using ResourceType = TweetLib.Browser.Request.ResourceType;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefRequestAdapter : IRequestAdapter<IRequest> {
+		public static CefRequestAdapter Instance { get; } = new CefRequestAdapter();
+
+		private CefRequestAdapter() {}
+
+		public ulong GetIdentifier(IRequest request) {
+			return request.Identifier;
+		}
+
+		public string GetUrl(IRequest request) {
+			return request.Url;
+		}
+
+		public void SetUrl(IRequest request, string url) {
+			request.Url = url;
+		}
+
+		public bool IsTransitionForwardBack(IRequest request) {
+			return request.TransitionType.HasFlag(TransitionType.ForwardBack);
+		}
+
+		public bool IsCspReport(IRequest request) {
+			return request.ResourceType == CefSharp.ResourceType.CspReport;
+		}
+
+		public ResourceType GetResourceType(IRequest request) {
+			return request.ResourceType switch {
+				CefSharp.ResourceType.MainFrame  => ResourceType.MainFrame,
+				CefSharp.ResourceType.Script     => ResourceType.Script,
+				CefSharp.ResourceType.Stylesheet => ResourceType.Stylesheet,
+				CefSharp.ResourceType.Xhr        => ResourceType.Xhr,
+				CefSharp.ResourceType.Image      => ResourceType.Image,
+				_                                => ResourceType.Unknown
+			};
+		}
+
+		public void SetHeader(IRequest request, string header, string value) {
+			request.SetHeaderByName(header, value, overwrite: true);
+		}
+	}
+}
diff --git a/Browser/Base/CefRequestHandler.cs b/Browser/Base/CefRequestHandler.cs
new file mode 100644
index 00000000..070ee897
--- /dev/null
+++ b/Browser/Base/CefRequestHandler.cs
@@ -0,0 +1,30 @@
+using CefSharp;
+using CefSharp.Handler;
+using TweetLib.Browser.CEF.Logic;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefRequestHandler : RequestHandler {
+		public RequestHandlerLogic<IRequest> Logic { get; }
+
+		private readonly bool autoReload;
+
+		public CefRequestHandler(CefLifeSpanHandler lifeSpanHandler, bool autoReload) {
+			this.Logic = new RequestHandlerLogic<IRequest>(CefRequestAdapter.Instance, lifeSpanHandler.Logic);
+			this.autoReload = autoReload;
+		}
+
+		protected override bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) {
+			return Logic.OnBeforeBrowse(request, userGesture);
+		}
+
+		protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) {
+			return Logic.OnOpenUrlFromTab(targetUrl, userGesture, CefLifeSpanHandler.ConvertTargetDisposition(targetDisposition));
+		}
+
+		protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) {
+			if (autoReload) {
+				browser.Reload();
+			}
+		}
+	}
+}
diff --git a/Browser/Base/CefResourceHandlerFactory.cs b/Browser/Base/CefResourceHandlerFactory.cs
new file mode 100644
index 00000000..c087df21
--- /dev/null
+++ b/Browser/Base/CefResourceHandlerFactory.cs
@@ -0,0 +1,19 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefResourceHandlerFactory : IResourceHandlerFactory<IResourceHandler> {
+		public static CefResourceHandlerFactory Instance { get; } = new CefResourceHandlerFactory();
+
+		private CefResourceHandlerFactory() {}
+
+		public IResourceHandler CreateResourceHandler(ByteArrayResource resource) {
+			return new CefByteArrayResourceHandler(resource);
+		}
+
+		public string GetMimeTypeFromExtension(string extension) {
+			return Cef.GetMimeType(extension);
+		}
+	}
+}
diff --git a/Browser/Base/CefResourceRequestHandler.cs b/Browser/Base/CefResourceRequestHandler.cs
new file mode 100644
index 00000000..4925810b
--- /dev/null
+++ b/Browser/Base/CefResourceRequestHandler.cs
@@ -0,0 +1,32 @@
+using CefSharp;
+using CefSharp.Handler;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Logic;
+using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefResourceRequestHandler : ResourceRequestHandler {
+		private readonly ResourceRequestHandlerLogic<IRequest, IResponse, IResourceHandler> logic;
+
+		public CefResourceRequestHandler(ResourceHandlerRegistry<IResourceHandler> resourceHandlerRegistry, IResourceRequestHandler resourceRequestHandler) {
+			this.logic = new ResourceRequestHandlerLogic<IRequest, IResponse, IResourceHandler>(CefRequestAdapter.Instance, CefResponseAdapter.Instance, resourceHandlerRegistry, resourceRequestHandler);
+		}
+
+		protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
+			return logic.OnBeforeResourceLoad(request, callback) ? CefReturnValue.Continue : CefReturnValue.Cancel;
+		}
+
+		protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) {
+			return logic.GetResourceHandler(request);
+		}
+
+		protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
+			var filter = logic.GetResourceResponseFilter(request, response);
+			return filter == null ? null : new CefResponseFilter(filter);
+		}
+
+		protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) {
+			logic.OnResourceLoadComplete(request);
+		}
+	}
+}
diff --git a/Browser/Base/CefResourceRequestHandlerFactory.cs b/Browser/Base/CefResourceRequestHandlerFactory.cs
new file mode 100644
index 00000000..c211d7d8
--- /dev/null
+++ b/Browser/Base/CefResourceRequestHandlerFactory.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+using CefSharp;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Logic;
+using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefResourceRequestHandlerFactory : IResourceRequestHandlerFactory {
+		bool IResourceRequestHandlerFactory.HasHandlers => true;
+
+		private readonly ResourceRequestHandlerFactoryLogic<CefResourceRequestHandler, IResourceHandler, IRequest> logic;
+
+		public CefResourceRequestHandlerFactory(IResourceRequestHandler resourceRequestHandler, ResourceHandlerRegistry<IResourceHandler> registry) {
+			this.logic = new ResourceRequestHandlerFactoryLogic<CefResourceRequestHandler, IResourceHandler, IRequest>(CefRequestAdapter.Instance, new CefResourceRequestHandler(registry, resourceRequestHandler), registry);
+		}
+
+		[SuppressMessage("ReSharper", "RedundantAssignment")]
+		CefSharp.IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) {
+			return logic.GetResourceRequestHandler(request, ref disableDefaultHandling);
+		}
+	}
+}
diff --git a/Browser/Base/CefResponseAdapter.cs b/Browser/Base/CefResponseAdapter.cs
new file mode 100644
index 00000000..1a5ca59b
--- /dev/null
+++ b/Browser/Base/CefResponseAdapter.cs
@@ -0,0 +1,31 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefResponseAdapter : IResponseAdapter<IResponse> {
+		public static CefResponseAdapter Instance { get; } = new CefResponseAdapter();
+
+		private CefResponseAdapter() {}
+
+		public void SetCharset(IResponse response, string charset) {
+			response.Charset = charset;
+		}
+
+		public void SetMimeType(IResponse response, string mimeType) {
+			response.MimeType = mimeType;
+		}
+
+		public void SetStatus(IResponse response, int statusCode, string statusText) {
+			response.StatusCode = statusCode;
+			response.StatusText = statusText;
+		}
+
+		public void SetHeader(IResponse response, string header, string value) {
+			response.SetHeaderByName(header, value, overwrite: true);
+		}
+
+		public string GetHeader(IResponse response, string header) {
+			return response.Headers[header];
+		}
+	}
+}
diff --git a/Browser/Base/CefResponseFilter.cs b/Browser/Base/CefResponseFilter.cs
new file mode 100644
index 00000000..ad2fb042
--- /dev/null
+++ b/Browser/Base/CefResponseFilter.cs
@@ -0,0 +1,28 @@
+using System;
+using System.IO;
+using CefSharp;
+using TweetLib.Browser.CEF.Logic;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefResponseFilter : IResponseFilter {
+		private readonly ResponseFilterLogic logic;
+
+		public CefResponseFilter(ResponseFilterLogic logic) {
+			this.logic = logic;
+		}
+
+		bool IResponseFilter.InitFilter() {
+			return true;
+		}
+
+		FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) {
+			return logic.Filter(dataIn, out dataInRead, dataOut, dataOut.Length, out dataOutWritten) switch {
+				ResponseFilterLogic.FilterStatus.NeedMoreData => FilterStatus.NeedMoreData,
+				ResponseFilterLogic.FilterStatus.Done         => FilterStatus.Done,
+				_                                             => FilterStatus.Error
+			};
+		}
+
+		void IDisposable.Dispose() {}
+	}
+}
diff --git a/Browser/Adapters/CefSchemeHandlerFactory.cs b/Browser/Base/CefSchemeHandlerFactory.cs
similarity index 55%
rename from Browser/Adapters/CefSchemeHandlerFactory.cs
rename to Browser/Base/CefSchemeHandlerFactory.cs
index 12356e1e..322fe6e8 100644
--- a/Browser/Adapters/CefSchemeHandlerFactory.cs
+++ b/Browser/Base/CefSchemeHandlerFactory.cs
@@ -1,9 +1,9 @@
-using System;
 using CefSharp;
 using CefSharp.WinForms;
+using TweetLib.Browser.CEF.Logic;
 using TweetLib.Browser.Interfaces;
 
-namespace TweetDuck.Browser.Adapters {
+namespace TweetDuck.Browser.Base {
 	sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory {
 		public static void Register(CefSettings settings, ICustomSchemeHandler handler) {
 			settings.RegisterScheme(new CefCustomScheme {
@@ -16,14 +16,14 @@ public static void Register(CefSettings settings, ICustomSchemeHandler handler)
 			});
 		}
 
-		private readonly ICustomSchemeHandler handler;
+		private readonly SchemeHandlerFactoryLogic<IRequest, IResourceHandler> logic;
 
 		private CefSchemeHandlerFactory(ICustomSchemeHandler handler) {
-			this.handler = handler;
+			this.logic = new SchemeHandlerFactoryLogic<IRequest, IResourceHandler>(handler, CefRequestAdapter.Instance, CefResourceHandlerFactory.Instance);
 		}
 
-		public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
-			return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(CefSchemeResourceVisitor.Instance) : null;
+		IResourceHandler ISchemeHandlerFactory.Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
+			return logic.Create(request);
 		}
 	}
 }
diff --git a/Browser/FormBrowser.cs b/Browser/FormBrowser.cs
index 28f67dd6..f0106b38 100644
--- a/Browser/FormBrowser.cs
+++ b/Browser/FormBrowser.cs
@@ -47,13 +47,10 @@ public bool IsWaiting {
 
 		public UpdateInstaller UpdateInstaller { get; private set; }
 
-		#pragma warning disable IDE0069 // Disposable fields should be disposed
-		private readonly TweetDeckBrowser browser;
-		private readonly FormNotificationTweet notification;
-		#pragma warning restore IDE0069 // Disposable fields should be disposed
-
 		private readonly ResourceCache resourceCache;
+		private readonly TweetDeckBrowser browser;
 		private readonly ITweetDeckInterface tweetDeckInterface;
+		private readonly FormNotificationTweet notification;
 		private readonly PluginManager plugins;
 		private readonly UpdateChecker updates;
 		private readonly ContextMenu contextMenu;
diff --git a/Browser/Handling/ContextMenuBase.cs b/Browser/Handling/ContextMenuBase.cs
index 05c5d7e5..99cc7edd 100644
--- a/Browser/Handling/ContextMenuBase.cs
+++ b/Browser/Handling/ContextMenuBase.cs
@@ -1,13 +1,14 @@
 using System.Collections.Generic;
 using System.Drawing;
 using CefSharp;
-using TweetDuck.Browser.Adapters;
+using TweetDuck.Browser.Base;
 using TweetDuck.Configuration;
 using TweetDuck.Utils;
+using TweetLib.Browser.CEF.Data;
 using TweetLib.Browser.Contexts;
 
 namespace TweetDuck.Browser.Handling {
-	abstract class ContextMenuBase : IContextMenuHandler {
+	class ContextMenuBase : IContextMenuHandler {
 		private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26500;
 
 		private static readonly HashSet<CefMenuCommand> AllowedCefCommands = new HashSet<CefMenuCommand> {
@@ -31,11 +32,17 @@ abstract class ContextMenuBase : IContextMenuHandler {
 		protected static UserConfig Config => Program.Config.User;
 
 		private readonly TweetLib.Browser.Interfaces.IContextMenuHandler handler;
-		private readonly CefContextMenuActionRegistry actionRegistry;
+		private readonly ContextMenuActionRegistry actionRegistry;
 
-		protected ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) {
+		public ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) {
 			this.handler = handler;
-			this.actionRegistry = new CefContextMenuActionRegistry();
+			this.actionRegistry = new ContextMenuActionRegistry();
+		}
+
+		private sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<CefMenuCommand> {
+			protected override CefMenuCommand NextId(int n) {
+				return CefMenuCommand.UserFirst + 500 + n;
+			}
 		}
 
 		protected virtual Context CreateContext(IContextMenuParams parameters) {
@@ -62,8 +69,13 @@ public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser bro
 			}
 
 			AddSeparator(model);
-			handler.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters));
+			handler?.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters));
 			RemoveSeparatorIfLast(model);
+			AddLastContextMenuItems(model);
+		}
+
+		protected virtual void AddLastContextMenuItems(IMenuModel model) {
+			AddDebugMenuItems(model);
 		}
 
 		public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
@@ -94,16 +106,16 @@ protected static void AddDebugMenuItems(IMenuModel model) {
 			}
 		}
 
-		protected static void RemoveSeparatorIfLast(IMenuModel model) {
-			if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) {
-				model.RemoveAt(model.Count - 1);
-			}
-		}
-
 		protected static void AddSeparator(IMenuModel model) {
 			if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate
 				model.AddSeparator();
 			}
 		}
+
+		private static void RemoveSeparatorIfLast(IMenuModel model) {
+			if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) {
+				model.RemoveAt(model.Count - 1);
+			}
+		}
 	}
 }
diff --git a/Browser/Handling/ContextMenuBrowser.cs b/Browser/Handling/ContextMenuBrowser.cs
index c7fad0fa..5b59d4c6 100644
--- a/Browser/Handling/ContextMenuBrowser.cs
+++ b/Browser/Handling/ContextMenuBrowser.cs
@@ -1,6 +1,6 @@
 using System.Windows.Forms;
 using CefSharp;
-using TweetDuck.Browser.Adapters;
+using TweetDuck.Browser.Base;
 using TweetDuck.Controls;
 using TweetLib.Browser.Contexts;
 using TweetLib.Core.Features.TweetDeck;
@@ -62,6 +62,8 @@ public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser br
 			}
 		}
 
+		protected override void AddLastContextMenuItems(IMenuModel model) {}
+
 		public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
 			if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)) {
 				return true;
diff --git a/Browser/Handling/ContextMenuGuide.cs b/Browser/Handling/ContextMenuGuide.cs
deleted file mode 100644
index cde67b5c..00000000
--- a/Browser/Handling/ContextMenuGuide.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using CefSharp;
-using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
-
-namespace TweetDuck.Browser.Handling {
-	sealed class ContextMenuGuide : ContextMenuBase {
-		public ContextMenuGuide(IContextMenuHandler handler) : base(handler) {}
-
-		public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
-			base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
-			AddDebugMenuItems(model);
-		}
-	}
-}
diff --git a/Browser/Handling/ContextMenuNotification.cs b/Browser/Handling/ContextMenuNotification.cs
index 4be8edae..9ab9a155 100644
--- a/Browser/Handling/ContextMenuNotification.cs
+++ b/Browser/Handling/ContextMenuNotification.cs
@@ -20,7 +20,6 @@ protected override Context CreateContext(IContextMenuParams parameters) {
 
 		public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
 			base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
-			AddDebugMenuItems(model);
 			form.InvokeAsyncSafe(() => form.ContextMenuOpen = true);
 		}
 
diff --git a/Browser/Handling/JavaScriptDialogHandler.cs b/Browser/Handling/CustomJsDialogHandler.cs
similarity index 98%
rename from Browser/Handling/JavaScriptDialogHandler.cs
rename to Browser/Handling/CustomJsDialogHandler.cs
index e7f3baed..c6fd38d9 100644
--- a/Browser/Handling/JavaScriptDialogHandler.cs
+++ b/Browser/Handling/CustomJsDialogHandler.cs
@@ -7,7 +7,7 @@
 using TweetDuck.Utils;
 
 namespace TweetDuck.Browser.Handling {
-	sealed class JavaScriptDialogHandler : IJsDialogHandler {
+	sealed class CustomJsDialogHandler : IJsDialogHandler {
 		private static FormMessage CreateMessageForm(string caption, string text) {
 			MessageBoxIcon icon = MessageBoxIcon.None;
 			int pipe = text.IndexOf('|');
diff --git a/Browser/Handling/CustomLifeSpanHandler.cs b/Browser/Handling/CustomLifeSpanHandler.cs
deleted file mode 100644
index 2b65410c..00000000
--- a/Browser/Handling/CustomLifeSpanHandler.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using CefSharp;
-using CefSharp.Handler;
-using TweetLib.Core;
-using TweetLib.Core.Features.Twitter;
-
-namespace TweetDuck.Browser.Handling {
-	sealed class CustomLifeSpanHandler : LifeSpanHandler {
-		public static bool HandleLinkClick(WindowOpenDisposition targetDisposition, string targetUrl) {
-			switch (targetDisposition) {
-				case WindowOpenDisposition.NewBackgroundTab:
-				case WindowOpenDisposition.NewForegroundTab:
-				case WindowOpenDisposition.NewPopup when !TwitterUrls.IsAllowedPopupUrl(targetUrl):
-				case WindowOpenDisposition.NewWindow:
-					App.SystemHandler.OpenBrowser(targetUrl);
-					return true;
-
-				default:
-					return false;
-			}
-		}
-
-		protected override bool OnBeforePopup(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) {
-			newBrowser = null;
-			return HandleLinkClick(targetDisposition, targetUrl);
-		}
-
-		protected override bool DoClose(IWebBrowser browserControl, IBrowser browser) {
-			return false;
-		}
-	}
-}
diff --git a/Browser/Handling/DownloadRequestClient.cs b/Browser/Handling/DownloadRequestClient.cs
deleted file mode 100644
index 09d4062d..00000000
--- a/Browser/Handling/DownloadRequestClient.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System;
-using System.IO;
-using CefSharp;
-
-namespace TweetDuck.Browser.Handling {
-	sealed class DownloadRequestClient : UrlRequestClient {
-		private readonly FileStream fileStream;
-		private readonly Action onSuccess;
-		private readonly Action<Exception> onError;
-
-		private bool hasFailed;
-
-		public DownloadRequestClient(FileStream fileStream, Action onSuccess, Action<Exception> onError) {
-			this.fileStream = fileStream;
-			this.onSuccess = onSuccess;
-			this.onError = onError;
-		}
-
-		protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) {
-			onError?.Invoke(new Exception("This URL requires authentication."));
-			fileStream.Dispose();
-			hasFailed = true;
-			return false;
-		}
-
-		protected override void OnDownloadData(IUrlRequest request, Stream data) {
-			if (hasFailed) {
-				return;
-			}
-
-			try {
-				data.CopyTo(fileStream);
-			} catch (Exception e) {
-				fileStream.Dispose();
-				onError?.Invoke(e);
-				hasFailed = true;
-			}
-		}
-
-		protected override void OnRequestComplete(IUrlRequest request) {
-			if (hasFailed) {
-				return;
-			}
-
-			bool isEmpty = fileStream.Position == 0;
-			fileStream.Dispose();
-
-			var status = request.RequestStatus;
-			if (status == UrlRequestStatus.Failed) {
-				onError?.Invoke(new Exception("Unknown error."));
-			}
-			else if (status == UrlRequestStatus.Success) {
-				if (isEmpty) {
-					onError?.Invoke(new Exception("File is empty."));
-					return;
-				}
-
-				onSuccess?.Invoke();
-			}
-		}
-	}
-}
diff --git a/Browser/Handling/DragHandlerBrowser.cs b/Browser/Handling/DragHandlerBrowser.cs
deleted file mode 100644
index 85518f87..00000000
--- a/Browser/Handling/DragHandlerBrowser.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Collections.Generic;
-using CefSharp;
-using CefSharp.Enums;
-
-namespace TweetDuck.Browser.Handling {
-	sealed class DragHandlerBrowser : IDragHandler {
-		private readonly RequestHandlerBrowser requestHandler;
-
-		public DragHandlerBrowser(RequestHandlerBrowser requestHandler) {
-			this.requestHandler = requestHandler;
-		}
-
-		public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask) {
-			void TriggerDragStart(string type, string data = null) {
-				browserControl.BrowserCore.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", type, data);
-			}
-
-			requestHandler.BlockNextUserNavUrl = dragData.LinkUrl; // empty if not a link
-
-			if (dragData.IsLink) {
-				TriggerDragStart("link", dragData.LinkUrl);
-			}
-			else if (dragData.IsFragment) {
-				TriggerDragStart("text", dragData.FragmentText.Trim());
-			}
-			else {
-				TriggerDragStart("unknown");
-			}
-
-			return false;
-		}
-
-		public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {}
-	}
-}
diff --git a/Browser/Handling/PopupHandler.cs b/Browser/Handling/PopupHandler.cs
new file mode 100644
index 00000000..1871d072
--- /dev/null
+++ b/Browser/Handling/PopupHandler.cs
@@ -0,0 +1,19 @@
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Core;
+using TweetLib.Core.Features.Twitter;
+
+namespace TweetDuck.Browser.Handling {
+	sealed class PopupHandler : IPopupHandler {
+		public static PopupHandler Instance { get; } = new PopupHandler();
+
+		private PopupHandler() {}
+
+		public bool IsPopupAllowed(string url) {
+			return TwitterUrls.IsAllowedPopupUrl(url);
+		}
+
+		public void OpenExternalBrowser(string url) {
+			App.SystemHandler.OpenBrowser(url);
+		}
+	}
+}
diff --git a/Browser/Handling/RequestHandlerBase.cs b/Browser/Handling/RequestHandlerBase.cs
deleted file mode 100644
index acd06833..00000000
--- a/Browser/Handling/RequestHandlerBase.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using CefSharp;
-using CefSharp.Handler;
-
-namespace TweetDuck.Browser.Handling {
-	class RequestHandlerBase : RequestHandler {
-		private readonly bool autoReload;
-
-		public RequestHandlerBase(bool autoReload) {
-			this.autoReload = autoReload;
-		}
-
-		protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) {
-			return CustomLifeSpanHandler.HandleLinkClick(targetDisposition, targetUrl);
-		}
-
-		protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) {
-			if (autoReload) {
-				browser.Reload();
-			}
-		}
-	}
-}
diff --git a/Browser/Handling/RequestHandlerBrowser.cs b/Browser/Handling/RequestHandlerBrowser.cs
deleted file mode 100644
index 96c3e787..00000000
--- a/Browser/Handling/RequestHandlerBrowser.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using CefSharp;
-using TweetLib.Core.Features.Twitter;
-
-namespace TweetDuck.Browser.Handling {
-	sealed class RequestHandlerBrowser : RequestHandlerBase {
-		public string BlockNextUserNavUrl { get; set; }
-
-		public RequestHandlerBrowser() : base(true) {}
-
-		protected override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) {
-			if (userGesture && request.TransitionType == TransitionType.LinkClicked) {
-				bool block = request.Url == BlockNextUserNavUrl;
-				BlockNextUserNavUrl = string.Empty;
-				return block;
-			}
-			else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)) {
-				return true;
-			}
-
-			return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
-		}
-	}
-}
diff --git a/Browser/Handling/ResourceHandlerNotification.cs b/Browser/Handling/ResourceHandlerNotification.cs
deleted file mode 100644
index 4c3cfedf..00000000
--- a/Browser/Handling/ResourceHandlerNotification.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-using System.Collections.Specialized;
-using System.IO;
-using System.Text;
-using CefSharp;
-using CefSharp.Callback;
-
-namespace TweetDuck.Browser.Handling {
-	sealed class ResourceHandlerNotification : IResourceHandler {
-		private readonly NameValueCollection headers = new NameValueCollection(0);
-		private MemoryStream dataIn;
-
-		public void SetHTML(string html) {
-			dataIn?.Dispose();
-			dataIn = ResourceHandler.GetMemoryStream(html, Encoding.UTF8);
-		}
-
-		public void Dispose() {
-			if (dataIn != null) {
-				dataIn.Dispose();
-				dataIn = null;
-			}
-		}
-
-		bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) {
-			callback.Dispose();
-			handleRequest = true;
-
-			if (dataIn != null) {
-				dataIn.Position = 0;
-			}
-
-			return true;
-		}
-
-		void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) {
-			redirectUrl = null;
-
-			response.MimeType = "text/html";
-			response.StatusCode = 200;
-			response.StatusText = "OK";
-			response.Headers = headers;
-			responseLength = dataIn?.Length ?? 0;
-		}
-
-		bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) {
-			callback?.Dispose(); // TODO unnecessary null check once ReadResponse is removed
-
-			try {
-				byte[] buffer = new byte[Math.Min(dataIn.Length - dataIn.Position, dataOut.Length)];
-				int length = buffer.Length;
-
-				dataIn.Read(buffer, 0, length);
-				dataOut.Write(buffer, 0, length);
-				bytesRead = length;
-			} catch { // catch IOException, possibly NullReferenceException if dataIn is null
-				bytesRead = 0;
-			}
-
-			return bytesRead > 0;
-		}
-
-		bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) {
-			bytesSkipped = -2; // ERR_FAILED
-			callback.Dispose();
-			return false;
-		}
-
-		bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) {
-			return ((IResourceHandler) this).Open(request, out bool _, callback);
-		}
-
-		bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) {
-			return ((IResourceHandler) this).Read(dataOut, out bytesRead, null);
-		}
-
-		void IResourceHandler.Cancel() {}
-	}
-}
diff --git a/Browser/Notification/FormNotificationBase.cs b/Browser/Notification/FormNotificationBase.cs
index 199db797..913dc51a 100644
--- a/Browser/Notification/FormNotificationBase.cs
+++ b/Browser/Notification/FormNotificationBase.cs
@@ -1,11 +1,13 @@
 using System.Drawing;
+using System.Text;
 using System.Windows.Forms;
 using CefSharp.WinForms;
-using TweetDuck.Browser.Adapters;
+using TweetDuck.Browser.Base;
 using TweetDuck.Browser.Handling;
 using TweetDuck.Configuration;
 using TweetDuck.Controls;
 using TweetDuck.Utils;
+using TweetLib.Browser.CEF.Data;
 using TweetLib.Browser.Interfaces;
 using TweetLib.Core.Features.Notifications;
 using TweetLib.Core.Features.Twitter;
@@ -88,14 +90,11 @@ protected virtual FormBorderStyle NotificationBorderStyle {
 
 		private readonly FormBrowser owner;
 
-		protected readonly IBrowserComponent browserComponent;
+		protected readonly ChromiumWebBrowser browser;
+		protected readonly CefBrowserComponent browserComponent;
 		private readonly NotificationBrowser browserImpl;
 
-		#pragma warning disable IDE0069 // Disposable fields should be disposed
-		protected readonly ChromiumWebBrowser browser;
-		#pragma warning restore IDE0069 // Disposable fields should be disposed
-
-		private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification();
+		private readonly CefByteArrayResourceHandler resourceHandler = new CefByteArrayResourceHandler();
 
 		private DesktopNotification currentNotification;
 		private int pauseCounter;
@@ -115,11 +114,10 @@ protected FormNotificationBase(FormBrowser owner, CreateBrowserImplFunc createBr
 			this.owner = owner;
 			this.owner.FormClosed += owner_FormClosed;
 
-			this.browser = new ChromiumWebBrowser(NotificationBrowser.BlankURL) {
-				RequestHandler = new RequestHandlerBase(false)
-			};
-
-			this.browserComponent = new ComponentImpl(browser, this);
+			this.browser = new ChromiumWebBrowser(NotificationBrowser.BlankURL);
+			this.browserComponent = new CefBrowserComponent(browser, handler => new ContextMenuNotification(this, handler), autoReload: false);
+			this.browserComponent.ResourceHandlerRegistry.RegisterStatic(NotificationBrowser.BlankURL, string.Empty);
+			this.browserComponent.ResourceHandlerRegistry.RegisterDynamic(TwitterUrls.TweetDeck, resourceHandler);
 			this.browserImpl = createBrowserImpl(this, browserComponent);
 
 			this.browser.Dock = DockStyle.None;
@@ -139,29 +137,9 @@ protected FormNotificationBase(FormBrowser owner, CreateBrowserImplFunc createBr
 			UpdateTitle();
 		}
 
-		protected sealed class ComponentImpl : CefBrowserComponent {
-			private readonly FormNotificationBase owner;
-
-			public ComponentImpl(ChromiumWebBrowser browser, FormNotificationBase owner) : base(browser) {
-				this.owner = owner;
-			}
-
-			protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) {
-				return new ContextMenuNotification(owner, handler);
-			}
-
-			protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) {
-				var registry = new CefResourceHandlerRegistry();
-				registry.RegisterStatic(NotificationBrowser.BlankURL, string.Empty);
-				registry.RegisterDynamic(TwitterUrls.TweetDeck, owner.resourceHandler);
-				return new CefResourceHandlerFactory(handler, registry);
-			}
-		}
-
 		protected override void Dispose(bool disposing) {
 			if (disposing) {
 				components?.Dispose();
-				resourceHandler.Dispose();
 			}
 
 			base.Dispose(disposing);
@@ -207,7 +185,7 @@ public virtual void ResumeNotification() {
 
 		protected virtual void LoadTweet(DesktopNotification tweet) {
 			currentNotification = tweet;
-			resourceHandler.SetHTML(browserImpl.GetTweetHTML(tweet));
+			resourceHandler.SetResource(new ByteArrayResource(browserImpl.GetTweetHTML(tweet), Encoding.UTF8));
 
 			browser.Load(TwitterUrls.TweetDeck);
 			DisplayTooltip(null);
diff --git a/Browser/Notification/SoundNotification.cs b/Browser/Notification/SoundNotification.cs
index 10b69a7a..cbbb79c5 100644
--- a/Browser/Notification/SoundNotification.cs
+++ b/Browser/Notification/SoundNotification.cs
@@ -1,20 +1,21 @@
 using System.Drawing;
 using System.IO;
 using System.Windows.Forms;
-using TweetDuck.Browser.Adapters;
+using CefSharp;
 using TweetDuck.Controls;
 using TweetDuck.Dialogs;
 using TweetDuck.Dialogs.Settings;
 using TweetDuck.Management;
+using TweetLib.Browser.CEF.Data;
 using TweetLib.Core.Features.TweetDeck;
 
 namespace TweetDuck.Browser.Notification {
 	sealed class SoundNotification : ISoundNotificationHandler {
 		public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
 
-		private readonly CefResourceHandlerRegistry registry;
+		private readonly ResourceHandlerRegistry<IResourceHandler> registry;
 
-		public SoundNotification(CefResourceHandlerRegistry registry) {
+		public SoundNotification(ResourceHandlerRegistry<IResourceHandler> registry) {
 			this.registry = registry;
 		}
 
diff --git a/Browser/TweetDeckBrowser.cs b/Browser/TweetDeckBrowser.cs
index 69b84ae4..08957132 100644
--- a/Browser/TweetDeckBrowser.cs
+++ b/Browser/TweetDeckBrowser.cs
@@ -2,7 +2,7 @@
 using System.Drawing;
 using CefSharp;
 using CefSharp.WinForms;
-using TweetDuck.Browser.Adapters;
+using TweetDuck.Browser.Base;
 using TweetDuck.Browser.Handling;
 using TweetDuck.Browser.Notification;
 using TweetDuck.Configuration;
@@ -11,8 +11,6 @@
 using TweetLib.Core.Features.TweetDeck;
 using TweetLib.Core.Features.Twitter;
 using TweetLib.Core.Systems.Updates;
-using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
-using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
 using TweetDeckBrowserImpl = TweetLib.Core.Features.TweetDeck.TweetDeckBrowser;
 
 namespace TweetDuck.Browser {
@@ -42,24 +40,18 @@ public bool IsTweetDeckWebsite {
 		private readonly ChromiumWebBrowser browser;
 
 		public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDeckInterface tweetDeckInterface, UpdateChecker updateChecker) {
-			RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
-
 			this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck) {
 				DialogHandler = new FileDialogHandler(),
-				DragHandler = new DragHandlerBrowser(requestHandler),
-				KeyboardHandler = new CustomKeyboardHandler(owner),
-				RequestHandler = requestHandler
+				KeyboardHandler = new CustomKeyboardHandler(owner)
 			};
 
 			// ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable
 			this.browser.BrowserSettings.BackgroundColor = (uint) TweetDeckBrowserImpl.BackgroundColor.ToArgb();
 
 			var extraContext = new TweetDeckExtraContext();
-			var resourceHandlerRegistry = new CefResourceHandlerRegistry();
-			var soundNotificationHandler = new SoundNotification(resourceHandlerRegistry);
 
-			this.browserComponent = new ComponentImpl(browser, owner, extraContext, resourceHandlerRegistry);
-			this.browserImpl = new TweetDeckBrowserImpl(browserComponent, tweetDeckInterface, extraContext, soundNotificationHandler, pluginManager, updateChecker);
+			this.browserComponent = new CefBrowserComponent(browser, handler => new ContextMenuBrowser(owner, handler, extraContext));
+			this.browserImpl = new TweetDeckBrowserImpl(browserComponent, tweetDeckInterface, extraContext, new SoundNotification(browserComponent.ResourceHandlerRegistry), pluginManager, updateChecker);
 
 			if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) {
 				browserComponent.PageLoadEnd += (sender, args) => {
@@ -72,26 +64,6 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDe
 			owner.Controls.Add(browser);
 		}
 
-		private sealed class ComponentImpl : CefBrowserComponent {
-			private readonly FormBrowser owner;
-			private readonly TweetDeckExtraContext extraContext;
-			private readonly CefResourceHandlerRegistry registry;
-
-			public ComponentImpl(ChromiumWebBrowser browser, FormBrowser owner, TweetDeckExtraContext extraContext, CefResourceHandlerRegistry registry) : base(browser) {
-				this.owner = owner;
-				this.extraContext = extraContext;
-				this.registry = registry;
-			}
-
-			protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) {
-				return new ContextMenuBrowser(owner, handler, extraContext);
-			}
-
-			protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) {
-				return new CefResourceHandlerFactory(handler, registry);
-			}
-		}
-
 		public void PrepareSize(Size size) {
 			if (!Ready) {
 				browser.Size = size;
diff --git a/Dialogs/FormGuide.cs b/Dialogs/FormGuide.cs
index 6aba986c..94ffb731 100644
--- a/Dialogs/FormGuide.cs
+++ b/Dialogs/FormGuide.cs
@@ -2,12 +2,11 @@
 using System.Windows.Forms;
 using CefSharp.WinForms;
 using TweetDuck.Browser;
-using TweetDuck.Browser.Adapters;
+using TweetDuck.Browser.Base;
 using TweetDuck.Browser.Handling;
 using TweetDuck.Controls;
 using TweetDuck.Management;
 using TweetDuck.Utils;
-using TweetLib.Browser.Interfaces;
 using TweetLib.Core.Features;
 
 namespace TweetDuck.Dialogs {
@@ -31,9 +30,7 @@ public static void Show(string hash = null) {
 			}
 		}
 
-		#pragma warning disable IDE0069 // Disposable fields should be disposed
 		private readonly ChromiumWebBrowser browser;
-		#pragma warning restore IDE0069 // Disposable fields should be disposed
 
 		private FormGuide(string url, Form owner) {
 			InitializeComponent();
@@ -43,13 +40,12 @@ private FormGuide(string url, Form owner) {
 			VisibleChanged += (sender, args) => this.MoveToCenter(owner);
 
 			browser = new ChromiumWebBrowser(url) {
-				KeyboardHandler = new CustomKeyboardHandler(null),
-				RequestHandler = new RequestHandlerBase(true)
+				KeyboardHandler = new CustomKeyboardHandler(null)
 			};
 
 			browser.BrowserSettings.BackgroundColor = (uint) BackColor.ToArgb();
 
-			var browserComponent = new ComponentImpl(browser);
+			var browserComponent = new CefBrowserComponent(browser);
 			var browserImpl = new BaseBrowser(browserComponent);
 
 			BrowserUtils.SetupDockOnLoad(browserComponent, browser);
@@ -62,18 +58,6 @@ private FormGuide(string url, Form owner) {
 			};
 		}
 
-		private sealed class ComponentImpl : CefBrowserComponent {
-			public ComponentImpl(ChromiumWebBrowser browser) : base(browser) {}
-
-			protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) {
-				return new ContextMenuGuide(handler);
-			}
-
-			protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) {
-				return new CefResourceHandlerFactory(handler, null);
-			}
-		}
-
 		protected override void Dispose(bool disposing) {
 			if (disposing) {
 				components?.Dispose();
diff --git a/Dialogs/Settings/DialogSettingsCefArgs.cs b/Dialogs/Settings/DialogSettingsCefArgs.cs
index 5b781cc2..987f6142 100644
--- a/Dialogs/Settings/DialogSettingsCefArgs.cs
+++ b/Dialogs/Settings/DialogSettingsCefArgs.cs
@@ -1,8 +1,8 @@
 using System;
 using System.Windows.Forms;
 using TweetDuck.Controls;
+using TweetLib.Browser.CEF.Utils;
 using TweetLib.Core;
-using TweetLib.Core.Features.Chromium;
 
 namespace TweetDuck.Dialogs.Settings {
 	sealed partial class DialogSettingsCefArgs : Form {
diff --git a/Management/BrowserCache.cs b/Management/BrowserCache.cs
index 018f0bed..f71d9531 100644
--- a/Management/BrowserCache.cs
+++ b/Management/BrowserCache.cs
@@ -3,11 +3,12 @@
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using TweetLib.Browser.CEF.Utils;
 using TweetLib.Core;
 
 namespace TweetDuck.Management {
 	static class BrowserCache {
-		public static string CacheFolder => Path.Combine(App.StoragePath, "Cache");
+		public static string CacheFolder => CefUtils.GetCacheFolder(App.StoragePath);
 
 		private static bool clearOnExit;
 		private static Timer autoClearTimer;
diff --git a/Program.cs b/Program.cs
index baecdef1..5199888e 100644
--- a/Program.cs
+++ b/Program.cs
@@ -5,16 +5,16 @@
 using CefSharp.WinForms;
 using TweetDuck.Application;
 using TweetDuck.Browser;
-using TweetDuck.Browser.Adapters;
+using TweetDuck.Browser.Base;
 using TweetDuck.Browser.Handling;
 using TweetDuck.Configuration;
 using TweetDuck.Dialogs;
 using TweetDuck.Management;
 using TweetDuck.Updates;
 using TweetDuck.Utils;
+using TweetLib.Browser.CEF.Utils;
 using TweetLib.Core;
 using TweetLib.Core.Application;
-using TweetLib.Core.Features.Chromium;
 using TweetLib.Core.Features.Plugins;
 using TweetLib.Core.Features.Plugins.Config;
 using TweetLib.Core.Features.TweetDeck;
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 6a5fa797..6314ecb0 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -66,6 +66,10 @@
       <Project>{eefb1f37-7cad-46bd-8042-66e7b502ab02}</Project>
       <Name>TweetLib.Browser</Name>
     </ProjectReference>
+    <ProjectReference Include="lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj">
+      <Project>{1b7793c6-9002-483e-9bd7-897fe6cd18fb}</Project>
+      <Name>TweetLib.Browser.CEF</Name>
+    </ProjectReference>
     <ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj">
       <Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
       <Name>TweetLib.Core</Name>
@@ -91,29 +95,32 @@
     <Compile Include="Application\FileDialogs.cs" />
     <Compile Include="Application\MessageDialogs.cs" />
     <Compile Include="Application\SystemHandler.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\CefResourceRequestHandler.cs" />
-    <Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
-    <Compile Include="Browser\Adapters\CefSchemeResourceVisitor.cs" />
+    <Compile Include="Browser\Base\CefBrowserAdapter.cs" />
+    <Compile Include="Browser\Base\CefBrowserComponent.cs" />
+    <Compile Include="Browser\Base\CefContextMenuModel.cs" />
+    <Compile Include="Browser\Base\CefDownloadRequestClient.cs" />
+    <Compile Include="Browser\Base\CefDragDataAdapter.cs" />
+    <Compile Include="Browser\Base\CefDragHandler.cs" />
+    <Compile Include="Browser\Base\CefErrorCodeAdapter.cs" />
+    <Compile Include="Browser\Base\CefFrameAdapter.cs" />
+    <Compile Include="Browser\Base\CefLifeSpanHandler.cs" />
+    <Compile Include="Browser\Base\CefRequestAdapter.cs" />
+    <Compile Include="Browser\Base\CefRequestHandler.cs" />
+    <Compile Include="Browser\Base\CefResourceHandlerFactory.cs" />
+    <Compile Include="Browser\Base\CefByteArrayResourceHandler.cs" />
+    <Compile Include="Browser\Base\CefResourceRequestHandlerFactory.cs" />
+    <Compile Include="Browser\Base\CefResourceRequestHandler.cs" />
+    <Compile Include="Browser\Base\CefResponseAdapter.cs" />
+    <Compile Include="Browser\Base\CefResponseFilter.cs" />
+    <Compile Include="Browser\Base\CefSchemeHandlerFactory.cs" />
     <Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
     <Compile Include="Browser\Handling\ContextMenuBase.cs" />
     <Compile Include="Browser\Handling\ContextMenuBrowser.cs" />
-    <Compile Include="Browser\Handling\ContextMenuGuide.cs" />
     <Compile Include="Browser\Handling\ContextMenuNotification.cs" />
     <Compile Include="Browser\Handling\CustomKeyboardHandler.cs" />
-    <Compile Include="Browser\Handling\CustomLifeSpanHandler.cs" />
-    <Compile Include="Browser\Handling\DownloadRequestClient.cs" />
-    <Compile Include="Browser\Handling\DragHandlerBrowser.cs" />
     <Compile Include="Browser\Handling\FileDialogHandler.cs" />
-    <Compile Include="Browser\Handling\JavaScriptDialogHandler.cs" />
-    <Compile Include="Browser\Handling\RequestHandlerBase.cs" />
-    <Compile Include="Browser\Handling\RequestHandlerBrowser.cs" />
-    <Compile Include="Browser\Handling\ResourceHandlerNotification.cs" />
-    <Compile Include="Browser\Handling\ResponseFilter.cs" />
+    <Compile Include="Browser\Handling\CustomJsDialogHandler.cs" />
+    <Compile Include="Browser\Handling\PopupHandler.cs" />
     <Compile Include="Browser\Notification\FormNotificationExample.cs">
       <SubType>Form</SubType>
     </Compile>
diff --git a/TweetDuck.sln b/TweetDuck.sln
index f2a4cde9..1aaf9849 100644
--- a/TweetDuck.sln
+++ b/TweetDuck.sln
@@ -14,10 +14,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetL
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser", "lib\TweetLib.Browser\TweetLib.Browser.csproj", "{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser.CEF", "lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj", "{1B7793C6-9002-483E-9BD7-897FE6CD18FB}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Utils", "lib\TweetLib.Utils\TweetLib.Utils.csproj", "{476B1007-B12C-447F-B855-9886048201D6}"
 EndProject
 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Core", "lib\TweetTest.Core\TweetTest.Core.fsproj", "{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}"
 EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Browser.CEF", "lib\TweetTest.Browser.CEF\TweetTest.Browser.CEF.fsproj", "{651B77C2-3745-4DAA-982C-398C2856E038}"
+EndProject
 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Utils", "lib\TweetTest.Utils\TweetTest.Utils.fsproj", "{07F6D350-B16F-44E2-804D-C1142E1E345F}"
 EndProject
 Global
@@ -50,6 +54,10 @@ Global
 		{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Debug|x86.Build.0 = Debug|x86
 		{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.ActiveCfg = Release|x86
 		{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.Build.0 = Release|x86
+		{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Debug|x86.ActiveCfg = Debug|x86
+		{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Debug|x86.Build.0 = Debug|x86
+		{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Release|x86.ActiveCfg = Release|x86
+		{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Release|x86.Build.0 = Release|x86
 		{476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.ActiveCfg = Debug|x86
 		{476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.Build.0 = Debug|x86
 		{476B1007-B12C-447F-B855-9886048201D6}.Release|x86.ActiveCfg = Release|x86
@@ -58,6 +66,10 @@ Global
 		{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Debug|x86.Build.0 = Debug|x86
 		{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.ActiveCfg = Release|x86
 		{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.Build.0 = Release|x86
+		{651B77C2-3745-4DAA-982C-398C2856E038}.Debug|x86.ActiveCfg = Debug|x86
+		{651B77C2-3745-4DAA-982C-398C2856E038}.Debug|x86.Build.0 = Debug|x86
+		{651B77C2-3745-4DAA-982C-398C2856E038}.Release|x86.ActiveCfg = Release|x86
+		{651B77C2-3745-4DAA-982C-398C2856E038}.Release|x86.Build.0 = Release|x86
 		{07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.ActiveCfg = Debug|x86
 		{07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.Build.0 = Debug|x86
 		{07F6D350-B16F-44E2-804D-C1142E1E345F}.Release|x86.ActiveCfg = Release|x86
diff --git a/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
new file mode 100644
index 00000000..3680e12b
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
@@ -0,0 +1,86 @@
+using System;
+using TweetLib.Browser.Base;
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.Events;
+using TweetLib.Browser.Interfaces;
+using TweetLib.Utils.Static;
+
+namespace TweetLib.Browser.CEF.Component {
+	public abstract class BrowserComponent<TFrame> : IBrowserComponent where TFrame : IDisposable {
+		public bool Ready { get; private set; }
+
+		public string Url => browser.Url;
+		public abstract string CacheFolder { get; }
+
+		public event EventHandler<BrowserLoadedEventArgs>? BrowserLoaded;
+		public event EventHandler<PageLoadEventArgs>? PageLoadStart;
+		public event EventHandler<PageLoadEventArgs>? PageLoadEnd;
+
+		private readonly IBrowserWrapper<TFrame> browser;
+		private readonly IFrameAdapter<TFrame> frameAdapter;
+
+		protected BrowserComponent(IBrowserWrapper<TFrame> browser, IFrameAdapter<TFrame> frameAdapter) {
+			this.browser = browser;
+			this.frameAdapter = frameAdapter;
+		}
+
+		public abstract void Setup(BrowserSetup setup);
+		public abstract void AttachBridgeObject(string name, object bridge);
+		public abstract void DownloadFile(string url, string path, Action? onSuccess, Action<Exception>? onError);
+
+		private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs {
+			private readonly IBrowserWrapper<TFrame> browser;
+
+			public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame> browser) {
+				this.browser = browser;
+			}
+
+			public override void AddDictionaryWords(params string[] words) {
+				foreach (string word in words) {
+					browser.AddWordToDictionary(word);
+				}
+			}
+		}
+
+		protected void OnLoadingStateChanged(bool isLoading) {
+			if (!isLoading && !Ready) {
+				Ready = true;
+				BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser));
+				BrowserLoaded = null;
+			}
+		}
+
+		protected void OnLoadError<T>(string failedUrl, T errorCode, IErrorCodeAdapter<T> errorCodeAdapter) {
+			if (errorCodeAdapter.IsAborted(errorCode)) {
+				return;
+			}
+
+			if (!failedUrl.StartsWithOrdinal("td://resources/error/")) {
+				using TFrame frame = browser.MainFrame;
+
+				if (frameAdapter.IsValid(frame)) {
+					string? errorName = errorCodeAdapter.GetName(errorCode);
+					string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
+					frameAdapter.LoadUrl(frame, "td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle));
+				}
+			}
+		}
+
+		protected void OnFrameLoadStart(string url, TFrame frame) {
+			if (frameAdapter.IsMain(frame)) {
+				PageLoadStart?.Invoke(this, new PageLoadEventArgs(url));
+			}
+		}
+
+		protected void OnFrameLoadEnd(string url, TFrame frame) {
+			if (frameAdapter.IsMain(frame)) {
+				PageLoadEnd?.Invoke(this, new PageLoadEventArgs(url));
+			}
+		}
+
+		public void RunScript(string identifier, string script) {
+			using TFrame frame = browser.MainFrame;
+			frameAdapter.ExecuteJavaScriptAsync(frame, script, identifier, 1);
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs b/lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs
new file mode 100644
index 00000000..fce0f4c7
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs
@@ -0,0 +1,26 @@
+using System.Net;
+using System.Text;
+
+namespace TweetLib.Browser.CEF.Data {
+	public sealed class ByteArrayResource {
+		private const string DefaultMimeType = "text/html";
+		private const HttpStatusCode DefaultStatusCode = HttpStatusCode.OK;
+		private const string DefaultStatusText = "OK";
+
+		internal byte[] Contents { get; }
+		internal int Length { get; }
+		internal string MimeType { get; }
+		internal HttpStatusCode StatusCode { get; }
+		internal string StatusText { get; }
+
+		public ByteArrayResource(byte[] contents, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) {
+			this.Contents = contents;
+			this.Length = contents.Length;
+			this.MimeType = mimeType;
+			this.StatusCode = statusCode;
+			this.StatusText = statusText;
+		}
+
+		public ByteArrayResource(string contents, Encoding encoding, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) : this(encoding.GetBytes(contents), mimeType, statusCode, statusText) {}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs b/lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs
new file mode 100644
index 00000000..d310fb2b
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+
+namespace TweetLib.Browser.CEF.Data {
+	public abstract class ContextMenuActionRegistry<T> {
+		private readonly Dictionary<T, Action> actions = new ();
+
+		protected abstract T NextId(int n);
+
+		public T AddAction(Action action) {
+			T id = NextId(actions.Count);
+			actions[id] = action;
+			return id;
+		}
+
+		public bool Execute(T id) {
+			if (actions.TryGetValue(id, out var action)) {
+				action();
+				return true;
+			}
+
+			return false;
+		}
+
+		public void Clear() {
+			actions.Clear();
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs b/lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs
new file mode 100644
index 00000000..d761d367
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Concurrent;
+using System.Text;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetLib.Browser.CEF.Data {
+	public sealed class ResourceHandlerRegistry<TResourceHandler> where TResourceHandler : class {
+		private readonly IResourceHandlerFactory<TResourceHandler> factory;
+		private readonly ConcurrentDictionary<string, Func<TResourceHandler>> resourceHandlers = new (StringComparer.OrdinalIgnoreCase);
+
+		public ResourceHandlerRegistry(IResourceHandlerFactory<TResourceHandler> factory) {
+			this.factory = factory;
+		}
+
+		internal bool HasHandler(string url) {
+			return resourceHandlers.ContainsKey(url);
+		}
+
+		internal TResourceHandler? GetHandler(string url) {
+			return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null;
+		}
+
+		private void Register(string url, Func<TResourceHandler> factory) {
+			if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
+				throw new ArgumentException("Resource handler URL must be absolute!");
+			}
+
+			resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (_, _) => factory);
+		}
+
+		public void RegisterStatic(string url, byte[] staticData, string mimeType = "text/html") {
+			Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, mimeType)));
+		}
+
+		public void RegisterStatic(string url, string staticData, string mimeType = "text/html") {
+			Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, Encoding.UTF8, mimeType)));
+		}
+
+		public void RegisterDynamic(string url, TResourceHandler handler) {
+			Register(url, () => handler);
+		}
+
+		public void Unregister(string url) {
+			resourceHandlers.TryRemove(url, out _);
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
new file mode 100644
index 00000000..6bbdf2c1
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IBrowserWrapper<TFrame> where TFrame : IDisposable {
+		string Url { get; }
+		TFrame MainFrame { get; }
+
+		void AddWordToDictionary(string word);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs
new file mode 100644
index 00000000..100d7abf
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs
@@ -0,0 +1,9 @@
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IDragDataAdapter<T> {
+		bool IsLink(T data);
+		string GetLink(T data);
+
+		bool IsFragment(T data);
+		string GetFragmentAsText(T data);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs
new file mode 100644
index 00000000..5aec8b40
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs
@@ -0,0 +1,6 @@
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IErrorCodeAdapter<T> {
+		bool IsAborted(T errorCode);
+		string? GetName(T errorCode);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs
new file mode 100644
index 00000000..a93a093e
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs
@@ -0,0 +1,8 @@
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IFrameAdapter<T> {
+		bool IsValid(T frame);
+		bool IsMain(T frame);
+		void LoadUrl(T frame, string url);
+		void ExecuteJavaScriptAsync(T frame, string script, string identifier, int startLine = 1);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs b/lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs
new file mode 100644
index 00000000..b40b7738
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs
@@ -0,0 +1,6 @@
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IPopupHandler {
+		bool IsPopupAllowed(string url);
+		void OpenExternalBrowser(string url);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
new file mode 100644
index 00000000..88393ad7
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
@@ -0,0 +1,19 @@
+using TweetLib.Browser.Request;
+
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IRequestAdapter<T> {
+		ulong GetIdentifier(T request);
+
+		string GetUrl(T request);
+
+		void SetUrl(T request, string url);
+
+		bool IsTransitionForwardBack(T request);
+
+		bool IsCspReport(T request);
+
+		ResourceType GetResourceType(T request);
+
+		void SetHeader(T request, string header, string value);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IResourceHandlerFactory.cs b/lib/TweetLib.Browser.CEF/Interfaces/IResourceHandlerFactory.cs
new file mode 100644
index 00000000..e13e7a2c
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IResourceHandlerFactory.cs
@@ -0,0 +1,8 @@
+using TweetLib.Browser.CEF.Data;
+
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IResourceHandlerFactory<T> {
+		T CreateResourceHandler(ByteArrayResource resource);
+		string GetMimeTypeFromExtension(string extension);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs
new file mode 100644
index 00000000..e1fc1377
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs
@@ -0,0 +1,9 @@
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IResponseAdapter<T> {
+		void SetCharset(T response, string charset);
+		void SetMimeType(T response, string mimeType);
+		void SetStatus(T response, int statusCode, string statusText);
+		void SetHeader(T response, string header, string value);
+		string? GetHeader(T response, string header);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Lib.cs b/lib/TweetLib.Browser.CEF/Lib.cs
new file mode 100644
index 00000000..fde9f468
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Lib.cs
@@ -0,0 +1,5 @@
+using System.Reflection;
+
+[assembly: AssemblyTitle("TweetDuck Browser CEF Library")]
+[assembly: AssemblyDescription("TweetDuck Browser CEF Library")]
+[assembly: AssemblyProduct("TweetLib.Browser.CEF")]
diff --git a/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs
new file mode 100644
index 00000000..268d3e02
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
+	public sealed class ByteArrayResourceHandlerLogic<TResponse> {
+		public delegate void WriteToOut<T>(T dataOut, byte[] dataIn, int position, int length);
+
+		private readonly ByteArrayResource resource;
+		private readonly IResponseAdapter<TResponse> responseAdapter;
+
+		private int position;
+
+		public ByteArrayResourceHandlerLogic(ByteArrayResource resource, IResponseAdapter<TResponse> responseAdapter) {
+			this.resource = resource;
+			this.responseAdapter = responseAdapter;
+		}
+
+		public bool Open(out bool handleRequest) {
+			position = 0;
+			handleRequest = true;
+			return true;
+		}
+
+		public void GetResponseHeaders(TResponse response, out long responseLength, out string? redirectUrl) {
+			responseLength = resource.Length;
+			redirectUrl = null;
+
+			responseAdapter.SetMimeType(response, resource.MimeType);
+			responseAdapter.SetStatus(response, (int) resource.StatusCode, resource.StatusText);
+			responseAdapter.SetCharset(response, "utf-8");
+			responseAdapter.SetHeader(response, "Access-Control-Allow-Origin", "*");
+		}
+
+		public bool Skip(long bytesToSkip, out long bytesSkipped, IDisposable callback) {
+			callback.Dispose();
+
+			position = (int) (position + bytesToSkip);
+			bytesSkipped = bytesToSkip;
+			return true;
+		}
+
+		public bool Read<T>(WriteToOut<T> write, T dataOut, int bytesToRead, out int bytesRead, IDisposable callback) {
+			callback.Dispose();
+
+			if (bytesToRead > 0) {
+				write(dataOut, resource.Contents, position, bytesToRead);
+				position += bytesToRead;
+			}
+
+			bytesRead = bytesToRead;
+			return bytesRead > 0;
+		}
+
+		public bool Read<T>(WriteToOut<T> write, T dataOut, out int bytesRead, IDisposable callback) {
+			return Read(write, dataOut, resource.Length - position, out bytesRead, callback);
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
new file mode 100644
index 00000000..0e9cef28
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class DownloadRequestClientLogic {
+		public enum RequestStatus {
+			Unknown,
+			Success,
+			Failed
+		}
+
+		private readonly FileStream fileStream;
+		private readonly Action? onSuccess;
+		private readonly Action<Exception>? onError;
+
+		private bool hasFailed;
+
+		public DownloadRequestClientLogic(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) {
+			this.fileStream = fileStream;
+			this.onSuccess = onSuccess;
+			this.onError = onError;
+		}
+
+		public bool GetAuthCredentials(IDisposable callback) {
+			callback.Dispose();
+
+			hasFailed = true;
+			fileStream.Dispose();
+			onError?.Invoke(new Exception("This URL requires authentication."));
+
+			return false;
+		}
+
+		public void OnDownloadData(Stream data) {
+			if (hasFailed) {
+				return;
+			}
+
+			try {
+				data.CopyTo(fileStream);
+			} catch (Exception e) {
+				fileStream.Dispose();
+				onError?.Invoke(e);
+				hasFailed = true;
+			}
+		}
+
+		[SuppressMessage("ReSharper", "SwitchStatementMissingSomeEnumCasesNoDefault")]
+		public void OnRequestComplete(RequestStatus status) {
+			if (hasFailed) {
+				return;
+			}
+
+			bool isEmpty = fileStream.Position == 0;
+			fileStream.Dispose();
+
+			switch (status) {
+				case RequestStatus.Failed:
+					onError?.Invoke(new Exception("Unknown error."));
+					break;
+
+				case RequestStatus.Success when isEmpty:
+					onError?.Invoke(new Exception("File is empty."));
+					return;
+
+				case RequestStatus.Success:
+					onSuccess?.Invoke();
+					break;
+			}
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs
new file mode 100644
index 00000000..aff9c3f3
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs
@@ -0,0 +1,37 @@
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class DragHandlerLogic<TDragData, TRequest> {
+		private readonly IScriptExecutor executor;
+		private readonly RequestHandlerLogic<TRequest> requestHandlerLogic;
+		private readonly IDragDataAdapter<TDragData> dragDataAdapter;
+
+		public DragHandlerLogic(IScriptExecutor executor, RequestHandlerLogic<TRequest> requestHandlerLogic, IDragDataAdapter<TDragData> dragDataAdapter) {
+			this.executor = executor;
+			this.requestHandlerLogic = requestHandlerLogic;
+			this.dragDataAdapter = dragDataAdapter;
+		}
+
+		private void TriggerDragStart(string type, string? data = null) {
+			executor.RunFunction("window.TDGF_onGlobalDragStart && window.TDGF_onGlobalDragStart", type, data);
+		}
+
+		public bool OnDragEnter(TDragData dragData) {
+			var link = dragDataAdapter.GetLink(dragData);
+			requestHandlerLogic.BlockNextUserNavUrl = link;
+
+			if (dragDataAdapter.IsLink(dragData)) {
+				TriggerDragStart("link", link);
+			}
+			else if (dragDataAdapter.IsFragment(dragData)) {
+				TriggerDragStart("text", dragDataAdapter.GetFragmentAsText(dragData).Trim());
+			}
+			else {
+				TriggerDragStart("unknown");
+			}
+
+			return false;
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs
new file mode 100644
index 00000000..43ee0cb6
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs
@@ -0,0 +1,37 @@
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class LifeSpanHandlerLogic {
+		public enum TargetDisposition {
+			NewBackgroundTab,
+			NewForegroundTab,
+			NewPopup,
+			NewWindow,
+			Other
+		}
+
+		private readonly IPopupHandler popupHandler;
+
+		public LifeSpanHandlerLogic(IPopupHandler popupHandler) {
+			this.popupHandler = popupHandler;
+		}
+
+		public bool OnBeforePopup(string targetUrl, TargetDisposition targetDisposition) {
+			switch (targetDisposition) {
+				case TargetDisposition.NewBackgroundTab:
+				case TargetDisposition.NewForegroundTab:
+				case TargetDisposition.NewPopup when !popupHandler.IsPopupAllowed(targetUrl):
+				case TargetDisposition.NewWindow:
+					popupHandler.OpenExternalBrowser(targetUrl);
+					return true;
+
+				default:
+					return false;
+			}
+		}
+
+		public bool DoClose() {
+			return false;
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs
new file mode 100644
index 00000000..87d408fa
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs
@@ -0,0 +1,29 @@
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class RequestHandlerLogic<TRequest> {
+		internal string BlockNextUserNavUrl { get; set; } = string.Empty;
+
+		private readonly IRequestAdapter<TRequest> requestAdapter;
+		private readonly LifeSpanHandlerLogic lifeSpanHandlerLogic;
+
+		public RequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, LifeSpanHandlerLogic lifeSpanHandlerLogic) {
+			this.requestAdapter = requestAdapter;
+			this.lifeSpanHandlerLogic = lifeSpanHandlerLogic;
+		}
+
+		private bool ShouldBlockNav(string url) {
+			bool block = url == BlockNextUserNavUrl;
+			BlockNextUserNavUrl = string.Empty;
+			return block;
+		}
+
+		public bool OnBeforeBrowse(TRequest request, bool userGesture) {
+			return requestAdapter.IsTransitionForwardBack(request) || (userGesture && ShouldBlockNav(requestAdapter.GetUrl(request)));
+		}
+
+		public bool OnOpenUrlFromTab(string targetUrl, bool userGesture, LifeSpanHandlerLogic.TargetDisposition targetDisposition) {
+			return (userGesture && ShouldBlockNav(targetUrl)) || lifeSpanHandlerLogic.OnBeforePopup(targetUrl, targetDisposition);
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerFactoryLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerFactoryLogic.cs
new file mode 100644
index 00000000..ef62a806
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerFactoryLogic.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics.CodeAnalysis;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class ResourceRequestHandlerFactoryLogic<TResourceRequestHandler, TResourceHandler, TRequest> where TResourceHandler : class {
+		private readonly IRequestAdapter<TRequest> requestAdapter;
+		private readonly TResourceRequestHandler handler;
+		private readonly ResourceHandlerRegistry<TResourceHandler> registry;
+
+		public ResourceRequestHandlerFactoryLogic(IRequestAdapter<TRequest> requestAdapter, TResourceRequestHandler handler, ResourceHandlerRegistry<TResourceHandler> registry) {
+			this.handler = handler;
+			this.registry = registry;
+			this.requestAdapter = requestAdapter;
+		}
+
+		[SuppressMessage("ReSharper", "RedundantAssignment")]
+		public TResourceRequestHandler GetResourceRequestHandler(TRequest request, ref bool disableDefaultHandling) {
+			disableDefaultHandling = registry.HasHandler(requestAdapter.GetUrl(request));
+			return handler;
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerLogic.cs
new file mode 100644
index 00000000..7c1bb524
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerLogic.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.Interfaces;
+using TweetLib.Browser.Request;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class ResourceRequestHandlerLogic<TRequest, TResponse, TResourceHandler> where TResourceHandler : class {
+		private readonly IRequestAdapter<TRequest> requestAdapter;
+		private readonly IResponseAdapter<TResponse> responseAdapter;
+		private readonly ResourceHandlerRegistry<TResourceHandler> resourceHandlerRegistry;
+		private readonly IResourceRequestHandler? resourceRequestHandler;
+
+		private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new ();
+
+		public ResourceRequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, IResponseAdapter<TResponse> responseAdapter, ResourceHandlerRegistry<TResourceHandler> resourceHandlerRegistry, IResourceRequestHandler? resourceRequestHandler) {
+			this.requestAdapter = requestAdapter;
+			this.responseAdapter = responseAdapter;
+			this.resourceHandlerRegistry = resourceHandlerRegistry;
+			this.resourceRequestHandler = resourceRequestHandler;
+		}
+
+		public bool OnBeforeResourceLoad(TRequest request, IDisposable callback) {
+			if (requestAdapter.IsCspReport(request)) {
+				callback.Dispose();
+				return false;
+			}
+
+			if (resourceRequestHandler != null) {
+				var result = resourceRequestHandler.Handle(requestAdapter.GetUrl(request), requestAdapter.GetResourceType(request));
+
+				switch (result) {
+					case RequestHandleResult.Redirect redirect:
+						requestAdapter.SetUrl(request, redirect.Url);
+						break;
+
+					case RequestHandleResult.Process process:
+						requestAdapter.SetHeader(request, "Accept-Encoding", "identity");
+						responseProcessors[requestAdapter.GetIdentifier(request)] = process.Processor;
+						break;
+
+					case RequestHandleResult.Cancel:
+						callback.Dispose();
+						return false;
+				}
+			}
+
+			return true;
+		}
+
+		public TResourceHandler? GetResourceHandler(TRequest request) {
+			return resourceHandlerRegistry.GetHandler(requestAdapter.GetUrl(request));
+		}
+
+		public ResponseFilterLogic? GetResourceResponseFilter(TRequest request, TResponse response) {
+			if (responseProcessors.TryGetValue(requestAdapter.GetIdentifier(request), out var processor) && int.TryParse(responseAdapter.GetHeader(response, "Content-Length"), out int totalBytes)) {
+				return new ResponseFilterLogic(processor, totalBytes);
+			}
+
+			return null;
+		}
+
+		public void OnResourceLoadComplete(TRequest request) {
+			responseProcessors.Remove(requestAdapter.GetIdentifier(request));
+		}
+	}
+}
diff --git a/Browser/Handling/ResponseFilter.cs b/lib/TweetLib.Browser.CEF/Logic/ResponseFilterLogic.cs
similarity index 71%
rename from Browser/Handling/ResponseFilter.cs
rename to lib/TweetLib.Browser.CEF/Logic/ResponseFilterLogic.cs
index 351d3285..0f075a41 100644
--- a/Browser/Handling/ResponseFilter.cs
+++ b/lib/TweetLib.Browser.CEF/Logic/ResponseFilterLogic.cs
@@ -1,10 +1,14 @@
 using System;
 using System.IO;
-using CefSharp;
 using TweetLib.Browser.Interfaces;
 
-namespace TweetDuck.Browser.Handling {
-	sealed class ResponseFilter : IResponseFilter {
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class ResponseFilterLogic {
+		public enum FilterStatus {
+			NeedMoreData,
+			Done
+		}
+
 		private enum State {
 			Reading,
 			Writing,
@@ -17,26 +21,21 @@ private enum State {
 		private State state;
 		private int offset;
 
-		public ResponseFilter(IResponseProcessor processor, int totalBytes) {
+		internal ResponseFilterLogic(IResponseProcessor processor, int totalBytes) {
 			this.processor = processor;
 			this.responseData = new byte[totalBytes];
 			this.state = State.Reading;
 		}
 
-		public bool InitFilter() {
-			return true;
-		}
-
-		FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) {
+		public FilterStatus Filter(Stream? dataIn, out long dataInRead, Stream dataOut, long dataOutLength, out long dataOutWritten) {
 			int responseLength = responseData.Length;
 
 			if (state == State.Reading) {
 				int bytesToRead = Math.Min(responseLength - offset, (int) Math.Min(dataIn?.Length ?? 0, int.MaxValue));
+				int bytesRead = dataIn?.Read(responseData, offset, bytesToRead) ?? 0;
 
-				dataIn?.Read(responseData, offset, bytesToRead);
-				offset += bytesToRead;
-
-				dataInRead = bytesToRead;
+				offset += bytesRead;
+				dataInRead = bytesRead;
 				dataOutWritten = 0;
 
 				if (offset >= responseLength) {
@@ -48,7 +47,7 @@ FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream d
 				return FilterStatus.NeedMoreData;
 			}
 			else if (state == State.Writing) {
-				int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOut.Length, int.MaxValue));
+				int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOutLength, int.MaxValue));
 
 				if (bytesToWrite > 0) {
 					dataOut.Write(responseData, offset, bytesToWrite);
@@ -70,7 +69,5 @@ FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream d
 				throw new InvalidOperationException("This resource filter cannot be reused.");
 			}
 		}
-
-		public void Dispose() {}
 	}
 }
diff --git a/lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs b/lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs
new file mode 100644
index 00000000..fccd7e7a
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs
@@ -0,0 +1,21 @@
+using System;
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public sealed class SchemeHandlerFactoryLogic<TRequest, TResourceHandler> where TResourceHandler : class {
+		private readonly ICustomSchemeHandler handler;
+		private readonly IRequestAdapter<TRequest> requestAdapter;
+		private readonly ISchemeResourceVisitor<TResourceHandler> resourceVisitor;
+
+		public SchemeHandlerFactoryLogic(ICustomSchemeHandler handler, IRequestAdapter<TRequest> requestAdapter, IResourceHandlerFactory<TResourceHandler> resourceHandlerFactory) {
+			this.handler = handler;
+			this.requestAdapter = requestAdapter;
+			this.resourceVisitor = new SchemeResourceVisitor<TResourceHandler>(resourceHandlerFactory);
+		}
+
+		public TResourceHandler? Create(TRequest request) {
+			return Uri.TryCreate(requestAdapter.GetUrl(request), UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(resourceVisitor) : null;
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs b/lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs
new file mode 100644
index 00000000..4ed41243
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Net;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.Interfaces;
+using TweetLib.Browser.Request;
+
+namespace TweetLib.Browser.CEF.Logic {
+	internal abstract class SchemeResourceVisitor {
+		protected static readonly SchemeResource.Status FileIsEmpty = new (HttpStatusCode.NoContent, "File is empty.");
+	}
+
+	internal sealed class SchemeResourceVisitor<TResourceHandler> : SchemeResourceVisitor, ISchemeResourceVisitor<TResourceHandler> {
+		private readonly IResourceHandlerFactory<TResourceHandler> factory;
+
+		public SchemeResourceVisitor(IResourceHandlerFactory<TResourceHandler> factory) {
+			this.factory = factory;
+		}
+
+		public TResourceHandler Status(SchemeResource.Status status) {
+			return factory.CreateResourceHandler(new ByteArrayResource(Array.Empty<byte>(), statusCode: status.Code, statusText: status.Message));
+		}
+
+		public TResourceHandler File(SchemeResource.File file) {
+			byte[] contents = file.Contents;
+			return contents.Length == 0 ? Status(FileIsEmpty) : factory.CreateResourceHandler(new ByteArrayResource(contents, factory.GetMimeTypeFromExtension(file.Extension)));
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj b/lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj
new file mode 100644
index 00000000..0c7d1bc3
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <Platforms>x86</Platforms>
+    <LangVersion>9</LangVersion>
+    <Nullable>enable</Nullable>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <Compile Include="..\..\Version.cs" Link="Version.cs" />
+  </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\TweetLib.Browser\TweetLib.Browser.csproj" />
+    <ProjectReference Include="..\TweetLib.Utils\TweetLib.Utils.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/TweetLib.Core/Features/Chromium/CefUtils.cs b/lib/TweetLib.Browser.CEF/Utils/CefUtils.cs
similarity index 83%
rename from lib/TweetLib.Core/Features/Chromium/CefUtils.cs
rename to lib/TweetLib.Browser.CEF/Utils/CefUtils.cs
index 66c3a54f..e2c7f14f 100644
--- a/lib/TweetLib.Core/Features/Chromium/CefUtils.cs
+++ b/lib/TweetLib.Browser.CEF/Utils/CefUtils.cs
@@ -1,8 +1,13 @@
+using System.IO;
 using System.Text.RegularExpressions;
 using TweetLib.Utils.Collections;
 
-namespace TweetLib.Core.Features.Chromium {
+namespace TweetLib.Browser.CEF.Utils {
 	public static class CefUtils {
+		public static string GetCacheFolder(string storagePath) {
+			return Path.Combine(storagePath, "Cache");
+		}
+
 		public static CommandLineArgs ParseCommandLineArguments(string argumentString) {
 			CommandLineArgs args = new CommandLineArgs();
 
diff --git a/lib/TweetTest.Browser.CEF/TweetTest.Browser.CEF.fsproj b/lib/TweetTest.Browser.CEF/TweetTest.Browser.CEF.fsproj
new file mode 100644
index 00000000..2ebd9ce2
--- /dev/null
+++ b/lib/TweetTest.Browser.CEF/TweetTest.Browser.CEF.fsproj
@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  
+  <PropertyGroup>
+    <TargetFramework>net472</TargetFramework>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="3.1.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj" />
+  </ItemGroup>
+  
+  <ItemGroup>
+    <Compile Include="Utils\TestCefUtils.fs" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/TweetTest.Core/Features/Chromium/TestCefUtils.fs b/lib/TweetTest.Browser.CEF/Utils/TestCefUtils.fs
similarity index 94%
rename from lib/TweetTest.Core/Features/Chromium/TestCefUtils.fs
rename to lib/TweetTest.Browser.CEF/Utils/TestCefUtils.fs
index 4fe99e7e..4721bcb7 100644
--- a/lib/TweetTest.Core/Features/Chromium/TestCefUtils.fs
+++ b/lib/TweetTest.Browser.CEF/Utils/TestCefUtils.fs
@@ -1,6 +1,6 @@
-namespace TweetTest.Core.Features.Chromium.CefUtils
+namespace TweetTest.Browser.CEF.Utils.CefUtils
 
-open TweetLib.Core.Features.Chromium
+open TweetLib.Browser.CEF.Utils
 open Xunit
 
 
diff --git a/lib/TweetTest.Core/TweetTest.Core.fsproj b/lib/TweetTest.Core/TweetTest.Core.fsproj
index bba0c85b..1810c097 100644
--- a/lib/TweetTest.Core/TweetTest.Core.fsproj
+++ b/lib/TweetTest.Core/TweetTest.Core.fsproj
@@ -22,7 +22,6 @@
   </ItemGroup>
   
   <ItemGroup>
-    <Compile Include="Features\Chromium\TestCefUtils.fs" />
     <Compile Include="Features\Twitter\TestTwitterUrls.fs" />
   </ItemGroup>