1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-23 12:15:48 +02:00

Work on abstracting CEF conventions and logic into a separate library

This commit is contained in:
chylex 2022-01-23 12:00:26 +01:00
parent 51d2ec92ca
commit c9fd4634ab
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
74 changed files with 1371 additions and 755 deletions
Browser
Dialogs
Management
Program.csTweetDuck.csprojTweetDuck.sln
lib

View File

@ -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);
}
});
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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 _);
}
}
}

View File

@ -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
};
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
});
}
}
}

View File

@ -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() {}
}
}

View File

@ -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;
}

View File

@ -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
});
}
}
}

View File

@ -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;
}
}
}

View File

@ -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) {}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
};
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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];
}
}
}

View File

@ -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() {}
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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('|');

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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) {}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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() {}
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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();

View File

@ -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 {

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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) {}
}
}

View File

@ -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();
}
}
}

View File

@ -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 _);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
namespace TweetLib.Browser.CEF.Interfaces {
public interface IErrorCodeAdapter<T> {
bool IsAborted(T errorCode);
string? GetName(T errorCode);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
namespace TweetLib.Browser.CEF.Interfaces {
public interface IPopupHandler {
bool IsPopupAllowed(string url);
void OpenExternalBrowser(string url);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
using System.Reflection;
[assembly: AssemblyTitle("TweetDuck Browser CEF Library")]
[assembly: AssemblyDescription("TweetDuck Browser CEF Library")]
[assembly: AssemblyProduct("TweetLib.Browser.CEF")]

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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() {}
}
}

View File

@ -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;
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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>

View File

@ -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();

View File

@ -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>

View File

@ -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

View File

@ -22,7 +22,6 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Features\Chromium\TestCefUtils.fs" />
<Compile Include="Features\Twitter\TestTwitterUrls.fs" />
</ItemGroup>