diff --git a/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
index 3680e12b..3a16447f 100644
--- a/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
+++ b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
@@ -1,12 +1,14 @@
 using System;
+using System.IO;
 using TweetLib.Browser.Base;
+using TweetLib.Browser.CEF.Data;
 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 abstract class BrowserComponent<TFrame, TRequest> : IBrowserComponent where TFrame : IDisposable {
 		public bool Ready { get; private set; }
 
 		public string Url => browser.Url;
@@ -16,22 +18,25 @@ public abstract class BrowserComponent<TFrame> : IBrowserComponent where TFrame
 		public event EventHandler<PageLoadEventArgs>? PageLoadStart;
 		public event EventHandler<PageLoadEventArgs>? PageLoadEnd;
 
-		private readonly IBrowserWrapper<TFrame> browser;
+		private readonly IBrowserWrapper<TFrame, TRequest> browser;
+		private readonly ICefAdapter cefAdapter;
 		private readonly IFrameAdapter<TFrame> frameAdapter;
+		private readonly IRequestAdapter<TRequest> requestAdapter;
 
-		protected BrowserComponent(IBrowserWrapper<TFrame> browser, IFrameAdapter<TFrame> frameAdapter) {
+		protected BrowserComponent(IBrowserWrapper<TFrame, TRequest> browser, ICefAdapter cefAdapter, IFrameAdapter<TFrame> frameAdapter, IRequestAdapter<TRequest> requestAdapter) {
 			this.browser = browser;
+			this.cefAdapter = cefAdapter;
 			this.frameAdapter = frameAdapter;
+			this.requestAdapter = requestAdapter;
 		}
 
 		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;
+			private readonly IBrowserWrapper<TFrame, TRequest> browser;
 
-			public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame> browser) {
+			public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame, TRequest> browser) {
 				this.browser = browser;
 			}
 
@@ -82,5 +87,25 @@ public void RunScript(string identifier, string script) {
 			using TFrame frame = browser.MainFrame;
 			frameAdapter.ExecuteJavaScriptAsync(frame, script, identifier, 1);
 		}
+
+		public void DownloadFile(string url, string path, Action? onSuccess, Action<Exception>? onError) {
+			cefAdapter.RunOnUiThread(() => {
+				var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
+
+				try {
+					var request = browser.CreateGetRequest();
+					requestAdapter.SetUrl(request, url);
+					requestAdapter.SetMethod(request, "GET");
+					requestAdapter.SetReferrer(request, Url);
+					requestAdapter.SetAllowStoredCredentials(request);
+
+					using TFrame frame = browser.MainFrame;
+					browser.RequestDownload(frame, request, new DownloadCallbacks(fileStream, onSuccess, onError));
+				} catch (Exception e) {
+					fileStream.Dispose();
+					onError?.Invoke(e);
+				}
+			});
+		}
 	}
 }
diff --git a/lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs b/lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
new file mode 100644
index 00000000..241aecd1
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+
+namespace TweetLib.Browser.CEF.Data {
+	public sealed class DownloadCallbacks {
+		internal bool HasData { get; private set; }
+
+		private readonly FileStream fileStream;
+		private readonly Action? onSuccess;
+		private readonly Action<Exception>? onError;
+
+		internal DownloadCallbacks(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) {
+			this.fileStream = fileStream;
+			this.onSuccess = onSuccess;
+			this.onError = onError;
+		}
+
+		internal void OnData(Stream data) {
+			data.CopyTo(fileStream);
+			HasData |= fileStream.Position > 0;
+		}
+
+		internal void OnSuccess() {
+			fileStream.Dispose();
+			onSuccess?.Invoke();
+		}
+
+		internal void OnError(Exception e) {
+			fileStream.Dispose();
+			onError?.Invoke(e);
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
index 6bbdf2c1..616b2e57 100644
--- a/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
@@ -1,10 +1,13 @@
 using System;
+using TweetLib.Browser.CEF.Data;
 
 namespace TweetLib.Browser.CEF.Interfaces {
-	public interface IBrowserWrapper<TFrame> where TFrame : IDisposable {
+	public interface IBrowserWrapper<TFrame, TRequest> where TFrame : IDisposable {
 		string Url { get; }
 		TFrame MainFrame { get; }
 
 		void AddWordToDictionary(string word);
+		TRequest CreateGetRequest();
+		void RequestDownload(TFrame frame, TRequest request, DownloadCallbacks callbacks);
 	}
 }
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
new file mode 100644
index 00000000..74c4cf39
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
@@ -0,0 +1,7 @@
+using System;
+
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface ICefAdapter {
+		void RunOnUiThread(Action action);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
new file mode 100644
index 00000000..87ab1929
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
@@ -0,0 +1,16 @@
+namespace TweetLib.Browser.CEF.Interfaces {
+	public interface IMenuModelAdapter<T> {
+		int GetItemCount(T model);
+
+		void AddCommand(T model, int command, string name);
+		int GetCommandAt(T model, int index);
+
+		void AddCheckCommand(T model, int command, string name);
+		void SetChecked(T model, int command, bool isChecked);
+
+		void AddSeparator(T model);
+		bool IsSeparatorAt(T model, int index);
+
+		void RemoveAt(T model, int index);
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
index 88393ad7..04fb54d9 100644
--- a/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
+++ b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
@@ -8,6 +8,8 @@ public interface IRequestAdapter<T> {
 
 		void SetUrl(T request, string url);
 
+		void SetMethod(T request, string method);
+
 		bool IsTransitionForwardBack(T request);
 
 		bool IsCspReport(T request);
@@ -15,5 +17,9 @@ public interface IRequestAdapter<T> {
 		ResourceType GetResourceType(T request);
 
 		void SetHeader(T request, string header, string value);
+
+		void SetReferrer(T request, string referrer);
+
+		void SetAllowStoredCredentials(T request);
 	}
 }
diff --git a/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs
index 268d3e02..71cbda94 100644
--- a/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs
+++ b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs
@@ -4,10 +4,12 @@
 using TweetLib.Browser.CEF.Interfaces;
 
 namespace TweetLib.Browser.CEF.Logic {
-	[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
-	public sealed class ByteArrayResourceHandlerLogic<TResponse> {
+	public abstract class ByteArrayResourceHandlerLogic {
 		public delegate void WriteToOut<T>(T dataOut, byte[] dataIn, int position, int length);
+	}
 
+	[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
+	public sealed class ByteArrayResourceHandlerLogic<TResponse> : ByteArrayResourceHandlerLogic {
 		private readonly ByteArrayResource resource;
 		private readonly IResponseAdapter<TResponse> responseAdapter;
 
diff --git a/lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
new file mode 100644
index 00000000..62ba5310
--- /dev/null
+++ b/lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using TweetLib.Browser.CEF.Data;
+using TweetLib.Browser.CEF.Interfaces;
+using TweetLib.Browser.Contexts;
+using TweetLib.Browser.Interfaces;
+
+namespace TweetLib.Browser.CEF.Logic {
+	public abstract class ContextMenuLogic {
+		protected const int CommandCustomFirst = 220;
+		protected const int CommandCustomLast = 250;
+
+		protected static readonly HashSet<int> AllowedCommands = new () {
+			-1,  // NotFound
+			110, // Undo
+			111, // Redo
+			112, // Cut
+			113, // Copy
+			114, // Paste
+			115, // Delete
+			116, // SelectAll
+			200, // SpellCheckSuggestion0
+			201, // SpellCheckSuggestion1
+			202, // SpellCheckSuggestion2
+			203, // SpellCheckSuggestion3
+			204, // SpellCheckSuggestion4
+			205, // SpellCheckNoSuggestions
+			206  // AddToDictionary
+		};
+
+		protected sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<int> {
+			private const int CommandUserFirst = 26500;
+
+			protected override int NextId(int n) {
+				return CommandUserFirst + 500 + n;
+			}
+		}
+	}
+
+	public sealed class ContextMenuLogic<TModel> : ContextMenuLogic {
+		private readonly IContextMenuHandler? handler;
+		private readonly IMenuModelAdapter<TModel> modelAdapter;
+		private readonly ContextMenuActionRegistry actionRegistry;
+
+		public ContextMenuLogic(IContextMenuHandler? handler, IMenuModelAdapter<TModel> modelAdapter) {
+			this.handler = handler;
+			this.modelAdapter = modelAdapter;
+			this.actionRegistry = new ContextMenuActionRegistry();
+		}
+
+		private sealed class ContextMenuBuilder : IContextMenuBuilder {
+			private readonly IMenuModelAdapter<TModel> modelAdapter;
+			private readonly ContextMenuActionRegistry actionRegistry;
+			private readonly TModel model;
+
+			public ContextMenuBuilder(IMenuModelAdapter<TModel> modelAdapter, ContextMenuActionRegistry actionRegistry, TModel model) {
+				this.model = model;
+				this.actionRegistry = actionRegistry;
+				this.modelAdapter = modelAdapter;
+			}
+
+			public void AddAction(string name, Action action) {
+				var id = actionRegistry.AddAction(action);
+				modelAdapter.AddCommand(model, id, name);
+			}
+
+			public void AddActionWithCheck(string name, bool isChecked, Action action) {
+				var id = actionRegistry.AddAction(action);
+				modelAdapter.AddCheckCommand(model, id, name);
+				modelAdapter.SetChecked(model, id, isChecked);
+			}
+
+			public void AddSeparator() {
+				int count = modelAdapter.GetItemCount(model);
+				if (count > 0 && !modelAdapter.IsSeparatorAt(model, count - 1)) { // do not add separators if there is nothing to separate
+					modelAdapter.AddSeparator(model);
+				}
+			}
+
+			public void RemoveSeparatorIfLast(TModel model) {
+				int count = modelAdapter.GetItemCount(model);
+				if (count > 0 && modelAdapter.IsSeparatorAt(model, count - 1)) {
+					modelAdapter.RemoveAt(model, count - 1);
+				}
+			}
+		}
+
+		public void OnBeforeContextMenu(TModel model, Context context) {
+			for (int i = modelAdapter.GetItemCount(model) - 1; i >= 0; i--) {
+				int command = modelAdapter.GetCommandAt(model, i);
+
+				if (!(AllowedCommands.Contains(command) || command is >= CommandCustomFirst and <= CommandCustomLast)) {
+					modelAdapter.RemoveAt(model, i);
+				}
+			}
+
+			for (int i = modelAdapter.GetItemCount(model) - 2; i >= 0; i--) {
+				if (modelAdapter.IsSeparatorAt(model, i) && modelAdapter.IsSeparatorAt(model, i + 1)) {
+					modelAdapter.RemoveAt(model, i);
+				}
+			}
+
+			if (modelAdapter.GetItemCount(model) > 0 && modelAdapter.IsSeparatorAt(model, 0)) {
+				modelAdapter.RemoveAt(model, 0);
+			}
+
+			var builder = new ContextMenuBuilder(modelAdapter, actionRegistry, model);
+			builder.AddSeparator();
+			handler?.Show(builder, context);
+			builder.RemoveSeparatorIfLast(model);
+		}
+
+		public bool OnContextMenuCommand(int commandId) {
+			return actionRegistry.Execute(commandId);
+		}
+
+		public void OnContextMenuDismissed() {
+			actionRegistry.Clear();
+		}
+
+		public bool RunContextMenu() {
+			return false;
+		}
+	}
+}
diff --git a/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
index 0e9cef28..d1995e49 100644
--- a/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
+++ b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using TweetLib.Browser.CEF.Data;
 
 namespace TweetLib.Browser.CEF.Logic {
 	public sealed class DownloadRequestClientLogic {
@@ -10,24 +11,18 @@ public enum RequestStatus {
 			Failed
 		}
 
-		private readonly FileStream fileStream;
-		private readonly Action? onSuccess;
-		private readonly Action<Exception>? onError;
-
+		private readonly DownloadCallbacks callbacks;
 		private bool hasFailed;
 
-		public DownloadRequestClientLogic(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) {
-			this.fileStream = fileStream;
-			this.onSuccess = onSuccess;
-			this.onError = onError;
+		public DownloadRequestClientLogic(DownloadCallbacks callbacks) {
+			this.callbacks = callbacks;
 		}
 
 		public bool GetAuthCredentials(IDisposable callback) {
 			callback.Dispose();
 
 			hasFailed = true;
-			fileStream.Dispose();
-			onError?.Invoke(new Exception("This URL requires authentication."));
+			callbacks.OnError(new Exception("This URL requires authentication."));
 
 			return false;
 		}
@@ -38,10 +33,9 @@ public void OnDownloadData(Stream data) {
 			}
 
 			try {
-				data.CopyTo(fileStream);
+				callbacks.OnData(data);
 			} catch (Exception e) {
-				fileStream.Dispose();
-				onError?.Invoke(e);
+				callbacks.OnError(e);
 				hasFailed = true;
 			}
 		}
@@ -52,20 +46,17 @@ public void OnRequestComplete(RequestStatus status) {
 				return;
 			}
 
-			bool isEmpty = fileStream.Position == 0;
-			fileStream.Dispose();
-
 			switch (status) {
-				case RequestStatus.Failed:
-					onError?.Invoke(new Exception("Unknown error."));
+				case RequestStatus.Success when callbacks.HasData:
+					callbacks.OnSuccess();
 					break;
 
-				case RequestStatus.Success when isEmpty:
-					onError?.Invoke(new Exception("File is empty."));
-					return;
-
 				case RequestStatus.Success:
-					onSuccess?.Invoke();
+					callbacks.OnError(new Exception("File is empty."));
+					break;
+
+				default:
+					callbacks.OnError(new Exception("Unknown error."));
 					break;
 			}
 		}
diff --git a/windows/TweetDuck/Browser/Base/CefAdapter.cs b/windows/TweetDuck/Browser/Base/CefAdapter.cs
new file mode 100644
index 00000000..06aa6dc8
--- /dev/null
+++ b/windows/TweetDuck/Browser/Base/CefAdapter.cs
@@ -0,0 +1,15 @@
+using System;
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefAdapter : ICefAdapter {
+		public static CefAdapter Instance { get; } = new CefAdapter();
+
+		private CefAdapter() {}
+
+		public void RunOnUiThread(Action action) {
+			Cef.UIThreadTaskFactory.StartNew(action);
+		}
+	}
+}
diff --git a/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs b/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs
index 456d06a4..abccada1 100644
--- a/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs
+++ b/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs
@@ -1,9 +1,10 @@
 using CefSharp;
 using CefSharp.WinForms;
+using TweetLib.Browser.CEF.Data;
 using TweetLib.Browser.CEF.Interfaces;
 
 namespace TweetDuck.Browser.Base {
-	sealed class CefBrowserAdapter : IBrowserWrapper<IFrame> {
+	sealed class CefBrowserAdapter : IBrowserWrapper<IFrame, IRequest> {
 		public string Url => browser.Address;
 		public IFrame MainFrame => browser.GetMainFrame();
 
@@ -16,5 +17,14 @@ public CefBrowserAdapter(ChromiumWebBrowser browser) {
 		public void AddWordToDictionary(string word) {
 			browser.AddWordToDictionary(word);
 		}
+
+		public IRequest CreateGetRequest() {
+			using var frame = MainFrame;
+			return frame.CreateRequest(false);
+		}
+
+		public void RequestDownload(IFrame frame, IRequest request, DownloadCallbacks callbacks) {
+			frame.CreateUrlRequest(request, new CefDownloadRequestClient(callbacks));
+		}
 	}
 }
diff --git a/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs b/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs
index cd1d2592..d8f7c4c7 100644
--- a/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs
+++ b/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs
@@ -1,5 +1,3 @@
-using System;
-using System.IO;
 using CefSharp;
 using CefSharp.WinForms;
 using TweetDuck.Browser.Handling;
@@ -11,7 +9,7 @@
 using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
 
 namespace TweetDuck.Browser.Base {
-	sealed class CefBrowserComponent : BrowserComponent<IFrame> {
+	sealed class CefBrowserComponent : BrowserComponent<IFrame, IRequest> {
 		public delegate ContextMenuBase CreateContextMenu(IContextMenuHandler handler);
 
 		private static readonly CreateContextMenu DefaultContextMenuFactory = handler => new ContextMenuBase(handler);
@@ -25,7 +23,7 @@ sealed class CefBrowserComponent : BrowserComponent<IFrame> {
 
 		private CreateContextMenu createContextMenu;
 
-		public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(new CefBrowserAdapter(browser), CefFrameAdapter.Instance) {
+		public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(new CefBrowserAdapter(browser), CefAdapter.Instance, CefFrameAdapter.Instance, CefRequestAdapter.Instance) {
 			this.browser = browser;
 			this.browser.LoadingStateChanged += OnLoadingStateChanged;
 			this.browser.LoadError += OnLoadError;
@@ -70,26 +68,5 @@ private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) {
 		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(() => {
-				var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
-
-				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);
-
-					frame.CreateUrlRequest(request, new CefDownloadRequestClient(fileStream, onSuccess, onError));
-				} catch (Exception e) {
-					fileStream.Dispose();
-					onError?.Invoke(e);
-				}
-			});
-		}
 	}
 }
diff --git a/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs b/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs
index 059f49dc..d7874b30 100644
--- a/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs
+++ b/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs
@@ -7,7 +7,7 @@
 
 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) {
+		private static readonly ByteArrayResourceHandlerLogic.WriteToOut<Stream> WriteToOut = delegate (Stream dataOut, byte[] dataIn, int position, int length) {
 			dataOut.Write(dataIn, position, length);
 		};
 
diff --git a/windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs b/windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs
new file mode 100644
index 00000000..620fd5f4
--- /dev/null
+++ b/windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs
@@ -0,0 +1,31 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Logic;
+using TweetLib.Browser.Contexts;
+
+namespace TweetDuck.Browser.Base {
+	abstract class CefContextMenuHandler : IContextMenuHandler {
+		private readonly ContextMenuLogic<IMenuModel> logic;
+
+		protected CefContextMenuHandler(TweetLib.Browser.Interfaces.IContextMenuHandler handler) {
+			this.logic = new ContextMenuLogic<IMenuModel>(handler, CefMenuModelAdapter.Instance);
+		}
+
+		protected abstract Context CreateContext(IContextMenuParams parameters);
+
+		public virtual void OnBeforeContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
+			logic.OnBeforeContextMenu(model, CreateContext(parameters));
+		}
+
+		public virtual bool OnContextMenuCommand(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
+			return logic.OnContextMenuCommand((int) commandId);
+		}
+
+		public virtual void OnContextMenuDismissed(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame) {
+			logic.OnContextMenuDismissed();
+		}
+
+		public bool RunContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) {
+			return logic.RunContextMenu();
+		}
+	}
+}
diff --git a/windows/TweetDuck/Browser/Base/CefContextMenuModel.cs b/windows/TweetDuck/Browser/Base/CefContextMenuModel.cs
deleted file mode 100644
index b4dc2551..00000000
--- a/windows/TweetDuck/Browser/Base/CefContextMenuModel.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-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.Base {
-	sealed class CefContextMenuModel : IContextMenuBuilder {
-		private readonly IMenuModel model;
-		private readonly ContextMenuActionRegistry<CefMenuCommand> actionRegistry;
-
-		public CefContextMenuModel(IMenuModel model, ContextMenuActionRegistry<CefMenuCommand> actionRegistry) {
-			this.model = model;
-			this.actionRegistry = actionRegistry;
-		}
-
-		public void AddAction(string name, Action action) {
-			var id = actionRegistry.AddAction(action);
-			model.AddItem(id, name);
-		}
-
-		public void AddActionWithCheck(string name, bool isChecked, Action action) {
-			var id = actionRegistry.AddAction(action);
-			model.AddCheckItem(id, name);
-			model.SetChecked(id, isChecked);
-		}
-
-		public void AddSeparator() {
-			if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate
-				model.AddSeparator();
-			}
-		}
-
-		public static Context CreateContext(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) {
-			var context = new Context();
-			var flags = parameters.TypeFlags;
-
-			var tweet = extraContext?.Tweet;
-			if (tweet != null && !flags.HasFlag(ContextMenuType.Editable)) {
-				context.Tweet = tweet;
-			}
-
-			context.Link = GetLink(parameters, extraContext);
-			context.Media = GetMedia(parameters, extraContext, imageQuality);
-
-			if (flags.HasFlag(ContextMenuType.Selection)) {
-				context.Selection = new Selection(parameters.SelectionText, flags.HasFlag(ContextMenuType.Editable));
-			}
-
-			return context;
-		}
-
-		private static Link? GetLink(IContextMenuParams parameters, TweetDeckExtraContext extraContext) {
-			var link = extraContext?.Link;
-			if (link != null) {
-				return link;
-			}
-
-			if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && extraContext?.Media == null) {
-				return new Link(parameters.LinkUrl, parameters.UnfilteredLinkUrl);
-			}
-
-			return null;
-		}
-
-		private static Media? GetMedia(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) {
-			var media = extraContext?.Media;
-			if (media != null) {
-				return media;
-			}
-
-			if (parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) {
-				return new Media(Media.Type.Image, TwitterUrls.GetMediaLink(parameters.SourceUrl, imageQuality));
-			}
-
-			return null;
-		}
-	}
-}
diff --git a/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs b/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs
index 2a3a0954..ef97f03b 100644
--- a/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs
+++ b/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs
@@ -1,6 +1,6 @@
-using System;
 using System.IO;
 using CefSharp;
+using TweetLib.Browser.CEF.Data;
 using TweetLib.Browser.CEF.Logic;
 using static TweetLib.Browser.CEF.Logic.DownloadRequestClientLogic.RequestStatus;
 
@@ -8,8 +8,8 @@ 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);
+		public CefDownloadRequestClient(DownloadCallbacks callbacks) {
+			this.logic = new DownloadRequestClientLogic(callbacks);
 		}
 
 		protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) {
diff --git a/windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs b/windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs
new file mode 100644
index 00000000..b38a5fd7
--- /dev/null
+++ b/windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs
@@ -0,0 +1,42 @@
+using CefSharp;
+using TweetLib.Browser.CEF.Interfaces;
+
+namespace TweetDuck.Browser.Base {
+	sealed class CefMenuModelAdapter : IMenuModelAdapter<IMenuModel> {
+		public static CefMenuModelAdapter Instance { get; } = new CefMenuModelAdapter();
+
+		private CefMenuModelAdapter() {}
+
+		public int GetItemCount(IMenuModel model) {
+			return model.Count;
+		}
+
+		public void AddCommand(IMenuModel model, int command, string name) {
+			model.AddItem((CefMenuCommand) command, name);
+		}
+
+		public int GetCommandAt(IMenuModel model, int index) {
+			return (int) model.GetCommandIdAt(index);
+		}
+
+		public void AddCheckCommand(IMenuModel model, int command, string name) {
+			model.AddCheckItem((CefMenuCommand) command, name);
+		}
+
+		public void SetChecked(IMenuModel model, int command, bool isChecked) {
+			model.SetChecked((CefMenuCommand) command, isChecked);
+		}
+
+		public void AddSeparator(IMenuModel model) {
+			model.AddSeparator();
+		}
+
+		public bool IsSeparatorAt(IMenuModel model, int index) {
+			return model.GetTypeAt(index) == MenuItemType.Separator;
+		}
+
+		public void RemoveAt(IMenuModel model, int index) {
+			model.RemoveAt(index);
+		}
+	}
+}
diff --git a/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs b/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs
index 2722d6b7..ff0361b0 100644
--- a/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs
+++ b/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs
@@ -20,6 +20,10 @@ public void SetUrl(IRequest request, string url) {
 			request.Url = url;
 		}
 
+		public void SetMethod(IRequest request, string method) {
+			request.Method = method;
+		}
+
 		public bool IsTransitionForwardBack(IRequest request) {
 			return request.TransitionType.HasFlag(TransitionType.ForwardBack);
 		}
@@ -42,5 +46,13 @@ public ResourceType GetResourceType(IRequest request) {
 		public void SetHeader(IRequest request, string header, string value) {
 			request.SetHeaderByName(header, value, overwrite: true);
 		}
+
+		public void SetReferrer(IRequest request, string referrer) {
+			request.SetReferrer(referrer, ReferrerPolicy.Default);
+		}
+
+		public void SetAllowStoredCredentials(IRequest request) {
+			request.Flags |= UrlRequestFlags.AllowStoredCredentials;
+		}
 	}
 }
diff --git a/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs b/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs
index 4925810b..6ac55e5b 100644
--- a/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs
+++ b/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs
@@ -21,8 +21,7 @@ protected override IResourceHandler GetResourceHandler(IWebBrowser browserContro
 		}
 
 		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);
+			return CefResponseFilter.Create(logic.GetResourceResponseFilter(request, response));
 		}
 
 		protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) {
diff --git a/windows/TweetDuck/Browser/Base/CefResponseFilter.cs b/windows/TweetDuck/Browser/Base/CefResponseFilter.cs
index ad2fb042..466359b4 100644
--- a/windows/TweetDuck/Browser/Base/CefResponseFilter.cs
+++ b/windows/TweetDuck/Browser/Base/CefResponseFilter.cs
@@ -5,9 +5,13 @@
 
 namespace TweetDuck.Browser.Base {
 	sealed class CefResponseFilter : IResponseFilter {
+		public static CefResponseFilter Create(ResponseFilterLogic logic) {
+			return logic == null ? null : new CefResponseFilter(logic);
+		}
+
 		private readonly ResponseFilterLogic logic;
 
-		public CefResponseFilter(ResponseFilterLogic logic) {
+		private CefResponseFilter(ResponseFilterLogic logic) {
 			this.logic = logic;
 		}
 
diff --git a/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs b/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs
index 99cc7edd..85fb4ed7 100644
--- a/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs
+++ b/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs
@@ -1,76 +1,27 @@
-using System.Collections.Generic;
-using System.Drawing;
+using System.Drawing;
 using CefSharp;
 using TweetDuck.Browser.Base;
 using TweetDuck.Configuration;
 using TweetDuck.Utils;
-using TweetLib.Browser.CEF.Data;
 using TweetLib.Browser.Contexts;
+using TweetLib.Core.Features.TweetDeck;
+using TweetLib.Core.Features.Twitter;
+using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
 
 namespace TweetDuck.Browser.Handling {
-	class ContextMenuBase : IContextMenuHandler {
+	class ContextMenuBase : CefContextMenuHandler {
 		private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26500;
 
-		private static readonly HashSet<CefMenuCommand> AllowedCefCommands = new HashSet<CefMenuCommand> {
-			CefMenuCommand.NotFound,
-			CefMenuCommand.Undo,
-			CefMenuCommand.Redo,
-			CefMenuCommand.Cut,
-			CefMenuCommand.Copy,
-			CefMenuCommand.Paste,
-			CefMenuCommand.Delete,
-			CefMenuCommand.SelectAll,
-			CefMenuCommand.SpellCheckSuggestion0,
-			CefMenuCommand.SpellCheckSuggestion1,
-			CefMenuCommand.SpellCheckSuggestion2,
-			CefMenuCommand.SpellCheckSuggestion3,
-			CefMenuCommand.SpellCheckSuggestion4,
-			CefMenuCommand.SpellCheckNoSuggestions,
-			CefMenuCommand.AddToDictionary
-		};
-
 		protected static UserConfig Config => Program.Config.User;
 
-		private readonly TweetLib.Browser.Interfaces.IContextMenuHandler handler;
-		private readonly ContextMenuActionRegistry actionRegistry;
+		public ContextMenuBase(IContextMenuHandler handler) : base(handler) {}
 
-		public ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) {
-			this.handler = handler;
-			this.actionRegistry = new ContextMenuActionRegistry();
+		protected override Context CreateContext(IContextMenuParams parameters) {
+			return CreateContext(parameters, null, Config.TwitterImageQuality);
 		}
 
-		private sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<CefMenuCommand> {
-			protected override CefMenuCommand NextId(int n) {
-				return CefMenuCommand.UserFirst + 500 + n;
-			}
-		}
-
-		protected virtual Context CreateContext(IContextMenuParams parameters) {
-			return CefContextMenuModel.CreateContext(parameters, null, Config.TwitterImageQuality);
-		}
-
-		public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
-			for (int i = model.Count - 1; i >= 0; i--) {
-				CefMenuCommand command = model.GetCommandIdAt(i);
-
-				if (!AllowedCefCommands.Contains(command) && !(command >= CefMenuCommand.CustomFirst && command <= CefMenuCommand.CustomLast)) {
-					model.RemoveAt(i);
-				}
-			}
-
-			for (int i = model.Count - 2; i >= 0; i--) {
-				if (model.GetTypeAt(i) == MenuItemType.Separator && model.GetTypeAt(i + 1) == MenuItemType.Separator) {
-					model.RemoveAt(i);
-				}
-			}
-
-			if (model.Count > 0 && model.GetTypeAt(0) == MenuItemType.Separator) {
-				model.RemoveAt(0);
-			}
-
-			AddSeparator(model);
-			handler?.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters));
-			RemoveSeparatorIfLast(model);
+		public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
+			base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
 			AddLastContextMenuItems(model);
 		}
 
@@ -78,8 +29,8 @@ protected virtual void AddLastContextMenuItems(IMenuModel model) {
 			AddDebugMenuItems(model);
 		}
 
-		public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
-			if (actionRegistry.Execute(commandId)) {
+		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;
 			}
 
@@ -91,14 +42,6 @@ public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser br
 			return false;
 		}
 
-		public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
-			actionRegistry.Clear();
-		}
-
-		public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) {
-			return false;
-		}
-
 		protected static void AddDebugMenuItems(IMenuModel model) {
 			if (Config.DevToolsInContextMenu) {
 				AddSeparator(model);
@@ -107,15 +50,54 @@ protected static void AddDebugMenuItems(IMenuModel model) {
 		}
 
 		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
+			if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) {
 				model.AddSeparator();
 			}
 		}
 
-		private static void RemoveSeparatorIfLast(IMenuModel model) {
-			if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) {
-				model.RemoveAt(model.Count - 1);
+		protected static Context CreateContext(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) {
+			var context = new Context();
+			var flags = parameters.TypeFlags;
+
+			var tweet = extraContext?.Tweet;
+			if (tweet != null && !flags.HasFlag(ContextMenuType.Editable)) {
+				context.Tweet = tweet;
 			}
+
+			context.Link = GetLink(parameters, extraContext);
+			context.Media = GetMedia(parameters, extraContext, imageQuality);
+
+			if (flags.HasFlag(ContextMenuType.Selection)) {
+				context.Selection = new Selection(parameters.SelectionText, flags.HasFlag(ContextMenuType.Editable));
+			}
+
+			return context;
+		}
+
+		private static Link? GetLink(IContextMenuParams parameters, TweetDeckExtraContext extraContext) {
+			var link = extraContext?.Link;
+			if (link != null) {
+				return link;
+			}
+
+			if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && extraContext?.Media == null) {
+				return new Link(parameters.LinkUrl, parameters.UnfilteredLinkUrl);
+			}
+
+			return null;
+		}
+
+		private static Media? GetMedia(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) {
+			var media = extraContext?.Media;
+			if (media != null) {
+				return media;
+			}
+
+			if (parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) {
+				return new Media(Media.Type.Image, TwitterUrls.GetMediaLink(parameters.SourceUrl, imageQuality));
+			}
+
+			return null;
 		}
 	}
 }
diff --git a/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs b/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs
index 5b59d4c6..50c420cf 100644
--- a/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs
+++ b/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs
@@ -1,6 +1,5 @@
 using System.Windows.Forms;
 using CefSharp;
-using TweetDuck.Browser.Base;
 using TweetDuck.Controls;
 using TweetLib.Browser.Contexts;
 using TweetLib.Core.Features.TweetDeck;
@@ -31,7 +30,7 @@ public ContextMenuBrowser(FormBrowser form, IContextMenuHandler handler, TweetDe
 		}
 
 		protected override Context CreateContext(IContextMenuParams parameters) {
-			return CefContextMenuModel.CreateContext(parameters, extraContext, Config.TwitterImageQuality);
+			return CreateContext(parameters, extraContext, Config.TwitterImageQuality);
 		}
 
 		public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
diff --git a/windows/TweetDuck/TweetDuck.csproj b/windows/TweetDuck/TweetDuck.csproj
index db4db178..6a4a0cab 100644
--- a/windows/TweetDuck/TweetDuck.csproj
+++ b/windows/TweetDuck/TweetDuck.csproj
@@ -95,15 +95,17 @@
     <Compile Include="Application\FileDialogs.cs" />
     <Compile Include="Application\MessageDialogs.cs" />
     <Compile Include="Application\SystemHandler.cs" />
+    <Compile Include="Browser\Base\CefAdapter.cs" />
     <Compile Include="Browser\Base\CefBrowserAdapter.cs" />
     <Compile Include="Browser\Base\CefBrowserComponent.cs" />
-    <Compile Include="Browser\Base\CefContextMenuModel.cs" />
+    <Compile Include="Browser\Base\CefContextMenuHandler.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\CefMenuModelAdapter.cs" />
     <Compile Include="Browser\Base\CefRequestAdapter.cs" />
     <Compile Include="Browser\Base\CefRequestHandler.cs" />
     <Compile Include="Browser\Base\CefResourceHandlerFactory.cs" />