mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 10:32:10 +02:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
b58c8f65fe
|
|||
2c69289785
|
|||
dc0fc06673
|
|||
3114b489b6
|
|||
8e5934bd84
|
|||
a2129b957e
|
|||
61cd632df6
|
|||
712bcd5a6f
|
|||
dd47201d7b
|
|||
2af864f337
|
|||
acafbc3706
|
|||
b815ae4b11
|
|||
45a3a7499f
|
|||
09fac63ffc
|
|||
dd6776fef4
|
|||
cd02a03e8a
|
|||
933e0e54df
|
|||
c4aa62fc3a
|
|||
ad30021d6d
|
|||
7c8b43adfe
|
|||
3aace0b399
|
|||
0a9c84feec
|
|||
d5ae698855
|
|||
26e6a09d5a
|
|||
57fcff3824
|
|||
2a7aec199f
|
|||
1b01c38fda
|
|||
c9fd4634ab
|
|||
51d2ec92ca
|
|||
12ec8baf5c
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,8 +10,8 @@ bld/*
|
|||||||
!bld/Resources
|
!bld/Resources
|
||||||
|
|
||||||
# Rider
|
# Rider
|
||||||
.idea/.idea.TweetDuck/.idea/dictionaries
|
**/.idea/dictionaries
|
||||||
.idea/.idea.TweetDuck/.idea/misc.xml
|
**/.idea/misc.xml
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.suo
|
*.suo
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="TweetDuck" type="DotNetProject" factoryName=".NET Project">
|
<configuration default="false" name="TweetDuck" type="DotNetProject" factoryName=".NET Project">
|
||||||
<option name="EXE_PATH" value="$PROJECT_DIR$/bin/x86/Debug/TweetDuck.exe" />
|
<option name="EXE_PATH" value="$PROJECT_DIR$/windows/TweetDuck/bin/x86/Debug/TweetDuck.exe" />
|
||||||
<option name="PROGRAM_PARAMETERS" value="-datafolder TweetDuckDebug -nogdpr" />
|
<option name="PROGRAM_PARAMETERS" value="-datafolder TweetDuckDebug -nogdpr" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/bin/x86/Debug" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/windows/TweetDuck/bin/x86/Debug" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/TweetDuck.csproj" />
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/windows/TweetDuck/TweetDuck.csproj" />
|
||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CefSharp;
|
|
||||||
using TweetLib.Browser.Contexts;
|
|
||||||
using TweetLib.Browser.Interfaces;
|
|
||||||
using TweetLib.Core.Features.TweetDeck;
|
|
||||||
using TweetLib.Core.Features.Twitter;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Adapters {
|
|
||||||
sealed class CefContextMenuModel : IContextMenuBuilder {
|
|
||||||
private readonly IMenuModel model;
|
|
||||||
private readonly CefContextMenuActionRegistry actionRegistry;
|
|
||||||
|
|
||||||
public CefContextMenuModel(IMenuModel model, CefContextMenuActionRegistry 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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 _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CefSharp;
|
|
||||||
using CefSharp.WinForms;
|
|
||||||
using TweetLib.Browser.Interfaces;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Adapters {
|
|
||||||
sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory {
|
|
||||||
public static void Register(CefSettings settings, ICustomSchemeHandler handler) {
|
|
||||||
settings.RegisterScheme(new CefCustomScheme {
|
|
||||||
SchemeName = handler.Protocol,
|
|
||||||
IsStandard = false,
|
|
||||||
IsSecure = true,
|
|
||||||
IsCorsEnabled = true,
|
|
||||||
IsCSPBypassing = true,
|
|
||||||
SchemeHandlerFactory = new CefSchemeHandlerFactory(handler)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ICustomSchemeHandler handler;
|
|
||||||
|
|
||||||
private CefSchemeHandlerFactory(ICustomSchemeHandler handler) {
|
|
||||||
this.handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
|
|
||||||
return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(CefSchemeResourceVisitor.Instance) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,38 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using CefSharp;
|
|
||||||
using TweetLib.Browser.Interfaces;
|
|
||||||
using TweetLib.Browser.Request;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Adapters {
|
|
||||||
sealed class CefSchemeResourceVisitor : ISchemeResourceVisitor<IResourceHandler> {
|
|
||||||
public static CefSchemeResourceVisitor Instance { get; } = new CefSchemeResourceVisitor();
|
|
||||||
|
|
||||||
private static readonly SchemeResource.Status FileIsEmpty = new SchemeResource.Status(HttpStatusCode.NoContent, "File is empty.");
|
|
||||||
|
|
||||||
private CefSchemeResourceVisitor() {}
|
|
||||||
|
|
||||||
public IResourceHandler Status(SchemeResource.Status status) {
|
|
||||||
var handler = CreateHandler(Array.Empty<byte>());
|
|
||||||
handler.StatusCode = (int) status.Code;
|
|
||||||
handler.StatusText = status.Message;
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IResourceHandler File(SchemeResource.File file) {
|
|
||||||
byte[] contents = file.Contents;
|
|
||||||
if (contents.Length == 0) {
|
|
||||||
return Status(FileIsEmpty); // FromByteArray crashes CEF internals with no contents
|
|
||||||
}
|
|
||||||
|
|
||||||
var handler = CreateHandler(contents);
|
|
||||||
handler.MimeType = Cef.GetMimeType(file.Extension);
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ResourceHandler CreateHandler(byte[] bytes) {
|
|
||||||
return ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,109 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Drawing;
|
|
||||||
using CefSharp;
|
|
||||||
using TweetDuck.Browser.Adapters;
|
|
||||||
using TweetDuck.Configuration;
|
|
||||||
using TweetDuck.Utils;
|
|
||||||
using TweetLib.Browser.Contexts;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
|
||||||
abstract class ContextMenuBase : IContextMenuHandler {
|
|
||||||
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 CefContextMenuActionRegistry actionRegistry;
|
|
||||||
|
|
||||||
protected ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) {
|
|
||||||
this.handler = handler;
|
|
||||||
this.actionRegistry = new CefContextMenuActionRegistry();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
|
|
||||||
if (actionRegistry.Execute(commandId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commandId == MenuOpenDevTools) {
|
|
||||||
browserControl.OpenDevToolsCustom(new Point(parameters.XCoord, parameters.YCoord));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
model.AddItem(MenuOpenDevTools, "Open dev tools");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
using CefSharp;
|
|
||||||
using CefSharp.Handler;
|
|
||||||
using TweetLib.Core;
|
|
||||||
using TweetLib.Utils.Static;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
|
||||||
sealed class CustomLifeSpanHandler : LifeSpanHandler {
|
|
||||||
private static bool IsPopupAllowed(string url) {
|
|
||||||
return url.StartsWithOrdinal("https://twitter.com/teams/authorize?") ||
|
|
||||||
url.StartsWithOrdinal("https://accounts.google.com/") ||
|
|
||||||
url.StartsWithOrdinal("https://appleid.apple.com/");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool HandleLinkClick(WindowOpenDisposition targetDisposition, string targetUrl) {
|
|
||||||
switch (targetDisposition) {
|
|
||||||
case WindowOpenDisposition.NewBackgroundTab:
|
|
||||||
case WindowOpenDisposition.NewForegroundTab:
|
|
||||||
case WindowOpenDisposition.NewPopup when !IsPopupAllowed(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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) {}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,68 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using CefSharp;
|
|
||||||
using TweetLib.Core;
|
|
||||||
using TweetLib.Utils.Static;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
|
||||||
sealed class FileDialogHandler : IDialogHandler {
|
|
||||||
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback) {
|
|
||||||
if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple) {
|
|
||||||
string allFilters = string.Join(";", acceptFilters.SelectMany(ParseFileType).Where(filter => !string.IsNullOrEmpty(filter)).Select(filter => "*" + filter));
|
|
||||||
|
|
||||||
using OpenFileDialog dialog = new OpenFileDialog {
|
|
||||||
AutoUpgradeEnabled = true,
|
|
||||||
DereferenceLinks = true,
|
|
||||||
Multiselect = mode == CefFileDialogMode.OpenMultiple,
|
|
||||||
Title = "Open Files",
|
|
||||||
Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (dialog.ShowDialog() == DialogResult.OK) {
|
|
||||||
string ext = Path.GetExtension(dialog.FileName)?.ToLower();
|
|
||||||
callback.Continue(acceptFilters.FindIndex(filter => ParseFileType(filter).Contains(ext)), dialog.FileNames.ToList());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.Dispose();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback.Dispose();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> ParseFileType(string type) {
|
|
||||||
if (string.IsNullOrEmpty(type)) {
|
|
||||||
return StringUtils.EmptyArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type[0] == '.') {
|
|
||||||
return new string[] { type };
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] extensions = type switch {
|
|
||||||
"image/jpeg" => new string[] { ".jpg", ".jpeg" },
|
|
||||||
"image/png" => new string[] { ".png" },
|
|
||||||
"image/gif" => new string[] { ".gif" },
|
|
||||||
"image/webp" => new string[] { ".webp" },
|
|
||||||
"video/mp4" => new string[] { ".mp4" },
|
|
||||||
"video/quicktime" => new string[] { ".mov", ".qt" },
|
|
||||||
_ => StringUtils.EmptyArray
|
|
||||||
};
|
|
||||||
|
|
||||||
if (extensions.Length == 0) {
|
|
||||||
App.Logger.Warn("Unknown file type: " + type);
|
|
||||||
Debugger.Break();
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,96 +0,0 @@
|
|||||||
using System.Drawing;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using CefSharp;
|
|
||||||
using CefSharp.WinForms;
|
|
||||||
using TweetDuck.Controls;
|
|
||||||
using TweetDuck.Dialogs;
|
|
||||||
using TweetDuck.Utils;
|
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
|
||||||
sealed class JavaScriptDialogHandler : IJsDialogHandler {
|
|
||||||
private static FormMessage CreateMessageForm(string caption, string text) {
|
|
||||||
MessageBoxIcon icon = MessageBoxIcon.None;
|
|
||||||
int pipe = text.IndexOf('|');
|
|
||||||
|
|
||||||
if (pipe != -1) {
|
|
||||||
icon = text.Substring(0, pipe) switch {
|
|
||||||
"error" => MessageBoxIcon.Error,
|
|
||||||
"warning" => MessageBoxIcon.Warning,
|
|
||||||
"info" => MessageBoxIcon.Information,
|
|
||||||
"question" => MessageBoxIcon.Question,
|
|
||||||
_ => MessageBoxIcon.None
|
|
||||||
};
|
|
||||||
|
|
||||||
if (icon != MessageBoxIcon.None) {
|
|
||||||
text = text.Substring(pipe + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FormMessage(caption, text, icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IJsDialogHandler.OnJSDialog(IWebBrowser browserControl, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage) {
|
|
||||||
var control = (ChromiumWebBrowser) browserControl;
|
|
||||||
|
|
||||||
control.InvokeSafe(() => {
|
|
||||||
FormMessage form;
|
|
||||||
TextBox input = null;
|
|
||||||
|
|
||||||
if (dialogType == CefJsDialogType.Alert) {
|
|
||||||
form = CreateMessageForm("Browser Message", messageText);
|
|
||||||
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
|
|
||||||
}
|
|
||||||
else if (dialogType == CefJsDialogType.Confirm) {
|
|
||||||
form = CreateMessageForm("Browser Confirmation", messageText);
|
|
||||||
form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel);
|
|
||||||
form.AddButton(FormMessage.Yes, ControlType.Focused);
|
|
||||||
}
|
|
||||||
else if (dialogType == CefJsDialogType.Prompt) {
|
|
||||||
form = CreateMessageForm("Browser Prompt", messageText);
|
|
||||||
form.AddButton(FormMessage.Cancel, DialogResult.Cancel, ControlType.Cancel);
|
|
||||||
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
|
|
||||||
|
|
||||||
float dpiScale = form.GetDPIScale();
|
|
||||||
int inputPad = form.HasIcon ? 43 : 0;
|
|
||||||
|
|
||||||
input = new TextBox {
|
|
||||||
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
|
|
||||||
Font = SystemFonts.MessageBoxFont,
|
|
||||||
Location = new Point(BrowserUtils.Scale(22 + inputPad, dpiScale), form.ActionPanelY - BrowserUtils.Scale(46, dpiScale)),
|
|
||||||
Size = new Size(form.ClientSize.Width - BrowserUtils.Scale(44 + inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale))
|
|
||||||
};
|
|
||||||
|
|
||||||
form.Controls.Add(input);
|
|
||||||
form.ActiveControl = input;
|
|
||||||
form.Height += input.Size.Height + input.Margin.Vertical;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback.Continue(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool success = form.ShowDialog() == DialogResult.OK;
|
|
||||||
|
|
||||||
if (input == null) {
|
|
||||||
callback.Continue(success);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback.Continue(success, input.Text);
|
|
||||||
input.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
form.Dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IJsDialogHandler.OnBeforeUnloadDialog(IWebBrowser browserControl, IBrowser browser, string messageText, bool isReload, IJsDialogCallback callback) {
|
|
||||||
callback.Dispose();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IJsDialogHandler.OnResetDialogState(IWebBrowser browserControl, IBrowser browser) {}
|
|
||||||
void IJsDialogHandler.OnDialogClosed(IWebBrowser browserControl, IBrowser browser) {}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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() {}
|
|
||||||
}
|
|
||||||
}
|
|
249
README.md
249
README.md
@@ -1,56 +1,211 @@
|
|||||||
# Support
|
|
||||||
|
|
||||||
[Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) | [Support via Ko-fi](https://ko-fi.com/chylex) | [Support via Patreon](https://www.patreon.com/chylex)
|
[Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) | [Support via Ko-fi](https://ko-fi.com/chylex) | [Support via Patreon](https://www.patreon.com/chylex)
|
||||||
|
|
||||||
# Build Instructions
|
# Table of Contents
|
||||||
|
|
||||||
### Setup
|
1. [Installation](#installation)
|
||||||
|
2. [Source Code](#source-code)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
+ [Editors](#editors)
|
||||||
|
+ [Installers](#installers)
|
||||||
|
* [Solution Overview](#solution-overview)
|
||||||
|
+ [Core Libraries](#core-libraries)
|
||||||
|
- [TweetLib.Core](#tweetlibcore)
|
||||||
|
- [TweetLib.Browser](#tweetlibbrowser)
|
||||||
|
- [TweetLib.Browser.CEF](#tweetlibbrowsercef)
|
||||||
|
+ [Windows Projects](#windows-projects)
|
||||||
|
- [TweetDuck](#tweetduck)
|
||||||
|
- [TweetDuck.Browser](#tweetduckbrowser)
|
||||||
|
- [TweetDuck.Video](#tweetduckvideo)
|
||||||
|
- [TweetImpl.CefSharp](#tweetimplcefsharp)
|
||||||
|
+ [Linux Projects](#linux-projects)
|
||||||
|
- [TweetDuck](#tweetduck-1)
|
||||||
|
- [TweetImpl.CefGlue](#tweetimplcefglue)
|
||||||
|
+ [Miscellaneous](#miscellaneous)
|
||||||
|
- [TweetLib.Communication](#tweetlibcommunication)
|
||||||
|
- [TweetLib.Utils](#tweetlibutils)
|
||||||
|
- [TweetTest.*](#tweettest)
|
||||||
|
3. [Development (Windows)](#development-windows)
|
||||||
|
* [Building](#building)
|
||||||
|
* [Debugging](#debugging)
|
||||||
|
* [Release](#release)
|
||||||
|
+ [Installers](#installers-1)
|
||||||
|
4. [Development (Linux)](#development-linux)
|
||||||
|
* [Building](#building-1)
|
||||||
|
* [Release](#release-1)
|
||||||
|
|
||||||
The program can be built using Visual Studio 2019. Before opening the solution, please make sure you have the following workloads and components installed (optional components that are not listed can be deselected to save space):
|
# Installation
|
||||||
|
|
||||||
|
Download links and system requirements are on the [official website](https://tweetduck.chylex.com).
|
||||||
|
|
||||||
|
# Source Code
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Building TweetDuck for Windows requires at minimum [Visual Studio 2019](https://visualstudio.microsoft.com/downloads) and Windows 7. Before opening the solution, open Visual Studio Installer and make sure you have the following Visual Studio workloads and components installed:
|
||||||
* **.NET desktop development**
|
* **.NET desktop development**
|
||||||
* .NET Framework 4.7.2 SDK
|
* .NET Framework 4.7.2 targeting pack
|
||||||
* F# desktop language support
|
* F# desktop language support
|
||||||
* **Desktop development with C++**
|
* **Desktop development with C++**
|
||||||
* MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.20)
|
* MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.20 / Latest)
|
||||||
|
|
||||||
After opening the solution, right-click the solution and select **Restore NuGet Packages**, or manually run this command in the **Package Manager Console**:
|
In the **Installation details** panel, you can expand the workloads you selected, and uncheck any components that are not listed above to save space.
|
||||||
```
|
|
||||||
PM> Install-Package CefSharp.WinForms -Version 67.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug
|
Building TweetDuck for Linux requires [.NET 5](https://docs.microsoft.com/en-us/dotnet/core/install/linux). The Linux project has its own solution file in the `linux/` folder.
|
||||||
|
|
||||||
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
|
### Editors
|
||||||
|
|
||||||
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads.
|
For editing code, I recommend either:
|
||||||
|
|
||||||
### Release
|
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) for C# / F# + [VS Code](https://code.visualstudio.com/) for the rest (free when using the Community edition of Visual Studio)
|
||||||
|
* [Rider](https://www.jetbrains.com/rider/) for all languages (paid)
|
||||||
|
|
||||||
Open **Batch Build**, tick all `Release` configurations with `x86` platform, and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the [Troubleshooting](#troubleshooting) section.
|
Icons and logos were designed in [Affinity Designer](https://affinity.serif.com/en-us/designer/) (paid). The original design projects are in the `resources/Design/` folder (`.afdesign` extension).
|
||||||
|
|
||||||
After the build succeeds, the `bin/x86/Release` folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#installers) section for automated installer generation.
|
|
||||||
|
|
||||||
The `Release` configuration omits debug symbols and other unnecessary files, and it will automatically generate the [update installer](#installers) if the environment is setup correctly. You can modify this behavior by opening `TweetDuck.csproj`, and editing the `<Target Name="AfterBuild" Condition="$(ConfigurationName) == Release">` section.
|
|
||||||
|
|
||||||
If you decide to publicly release a custom version, please make it clear that it is not an official release of TweetDuck. There are many references to the official website and this repository, especially in the update system, so search for `chylex.com` and `github.com` in all files and replace them appropriately.
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
#### Error: The command (...) exited with code 1
|
|
||||||
- This indicates a failed post-build event, open the **Output** tab for logs
|
|
||||||
- Determine if there was an IO error from the `rmdir` commands, the custom MSBuild targets near the end of the [.csproj file](https://github.com/chylex/TweetDuck/blob/master/TweetDuck.csproj), or in the **PostBuild.fsx** script (`Encountered an error while running PostBuild`)
|
|
||||||
- Some files are checked for invalid characters:
|
|
||||||
- `Resources/Plugins/emoji-keyboard/emoji-ordering.txt` line endings must be LF (line feed); any CR (carriage return) in the file will cause a failed build, and you will need to ensure correct line endings in your text editor
|
|
||||||
|
|
||||||
### Installers
|
### Installers
|
||||||
|
|
||||||
TweetDuck uses **Inno Setup** for installers and updates. First, download and install [InnoSetup 5.6.1](http://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) (with Preprocessor support) and the [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list).
|
> If you don't want to build installers using the existing foundations, you can skip this section.
|
||||||
|
|
||||||
Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer and Visual Studio for the change to take place.
|
Official Windows installers are built using [InnoSetup](https://jrsoftware.org/isinfo.php) and [Inno Download Plugin](https://mitrichsoftware.wordpress.com/inno-setup-tools/inno-download-plugin/), specifically:
|
||||||
|
* [InnoSetup 5.6.1](https://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) with Preprocessor support
|
||||||
|
* [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list)
|
||||||
|
|
||||||
Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio first!
|
When installing InnoSetup, you can choose to include Inno Script Studio which I recommend for editing and testing installer configuration files in the `bld` folder (`.iss` extension).
|
||||||
|
|
||||||
After the window closes, three installers will be generated inside the `bld/Output` folder:
|
Scripts for building installers require the `PATH` environment variable to include the InnoSetup installation folder. You can either edit `PATH` manually, or use a program like [Rapid Environment Editor](https://www.rapidee.com/en/about) to simplify the process. For example, this is the installation folder I added to `PATH` under **User variables**:
|
||||||
|
* `C:\Program Files (x86)\Inno Setup 5`
|
||||||
|
|
||||||
|
You may need to restart Visual Studio after changing `PATH` for the change to take place.
|
||||||
|
|
||||||
|
## Solution Overview
|
||||||
|
|
||||||
|
Open the solution file `TweetDuck.sln` (or `linux/TweetDuck.Linux.sln`) in an IDE, and use the **Restore NuGet Packages** option in your IDE to install dependencies.
|
||||||
|
|
||||||
|
On Windows, TweetDuck uses the [CefSharp](https://github.com/cefsharp/CefSharp/) library for the browser component, and Windows Forms for the GUI.
|
||||||
|
|
||||||
|
On Linux, TweetDuck uses the [ChromiumGtk](https://github.com/lunixo/ChromiumGtk) library, which combines [CefGlue](https://gitlab.com/xiliumhq/chromiumembedded/cefglue) for the browser component and [GtkSharp](https://github.com/GtkSharp/GtkSharp) for the GUI.
|
||||||
|
|
||||||
|
The solution contains several C# projects for executables and libraries, and F# projects for automated tests.
|
||||||
|
|
||||||
|
Projects are organized into folders:
|
||||||
|
* Windows projects are in the `windows/` folder, and target `.NET Framework 4.7.2` + `C# 8.0`
|
||||||
|
* Linux projects are in the `linux/` folder, and target `.NET 5` + `C#`
|
||||||
|
* Libraries (`TweetLib.*`) are in the `lib/` folder, and target `.NET Standard 2.0` + `C# 9.0`
|
||||||
|
* Tests (`TweetTest.*`) are also in the `lib/` folder, and target `.NET Framework 4.7.2` + `F#`
|
||||||
|
|
||||||
|
Here are a few things to keep in mind:
|
||||||
|
* Executable projects have their entry points in `Program.cs`
|
||||||
|
* Library projects targeting `.NET Standard` have their assembly information in `Lib.cs`
|
||||||
|
* All non-test projects include a link to the `Version.cs` file in the root of the repository, which allows changing the version of all executables and library files in one place
|
||||||
|
|
||||||
|
Web resource files (HTML, CSS, JS) are in the `Resources/` folder:
|
||||||
|
* `Resources/Content/` contains all the core features of TweetDuck injected into the browser components
|
||||||
|
* `Resources/Guide/` contains the official TweetDuck guide that opens as a popup
|
||||||
|
* `Resources/Plugins/` contains all official plugins, and a `.debug` plugin for testing
|
||||||
|
|
||||||
|
These resource folders are linked as part of the `TweetLib.Core` project so they can be edited directly within an IDE. Alternatively, you can edit them using [VS Code](https://code.visualstudio.com/) by opening the workspace file `Resources/..code-workspace`.
|
||||||
|
|
||||||
|
### Core Libraries
|
||||||
|
|
||||||
|
#### TweetLib.Core
|
||||||
|
|
||||||
|
This library contains the core TweetDuck application and browser logic. It is built around simple dependency injection that makes it independent of any concrete OS, GUI framework, or browser implementation.
|
||||||
|
|
||||||
|
To simplify porting to other systems, it is not necessary to implement all interfaces, but some functionality will be missing (for ex. if clipboard-related interfaces are not implemented, then context menus will not contain options to copy text or images to clipboard).
|
||||||
|
|
||||||
|
#### TweetLib.Browser
|
||||||
|
|
||||||
|
This library provides a zero-dependency abstraction of browser components and logic. It defines interfaces, events, and container objects that are used by the `TweetLib.Core` library to describe how a browser should behave, while making as few assumptions about the actual browser implementation as possible.
|
||||||
|
|
||||||
|
#### TweetLib.Browser.CEF
|
||||||
|
|
||||||
|
This library is a partial implementation of `TweetLib.Browser` based on [CEF](https://bitbucket.org/chromiumembedded/cef/) interfaces and conventions.
|
||||||
|
|
||||||
|
While `TweetLib.Browser` is highly generic, most browser libraries are likely to be using some form of [CEF](https://bitbucket.org/chromiumembedded/cef/), so this library significantly reduces the amount of work required to swap between browser libraries that are based on [CEF](https://bitbucket.org/chromiumembedded/cef/).
|
||||||
|
|
||||||
|
### Windows Projects
|
||||||
|
|
||||||
|
#### TweetDuck
|
||||||
|
|
||||||
|
Main Windows executable. It has a dependency on [CefSharp](https://github.com/cefsharp/CefSharp/) and Windows Forms. Here you will find the entry point that bootstraps the main application, as well as code for GUIs and Windows-specific functionality.
|
||||||
|
|
||||||
|
#### TweetDuck.Browser
|
||||||
|
|
||||||
|
Windows executable that hosts various Chromium processes. It depends on two specific DLLs from the [CefSharp](https://github.com/cefsharp/CefSharp/) package. After updating [CefSharp](https://github.com/cefsharp/CefSharp/), run the `windows/TweetDuck/Resources/PostCefUpdate.ps1` PowerShell script to update these dependencies to the new version.
|
||||||
|
|
||||||
|
#### TweetDuck.Video
|
||||||
|
|
||||||
|
Windows executable that hosts a video player, which is based on the WMPLib ActiveX component responsible for integrating Windows Media Player into .NET Framework.
|
||||||
|
|
||||||
|
By default, [CefSharp](https://github.com/cefsharp/CefSharp/) is not built with support for H.264 video playback due to software patent nonsense, and even though TweetDuck could be moved entirely to Europe where MPEG LA's patent means nothing, it would require building a custom version of Chromium which requires too many resources. Instead, when a Twitter video played, TweetDuck launches this video player process, which uses Windows Media Player to play H.264 videos.
|
||||||
|
|
||||||
|
#### TweetImpl.CefSharp
|
||||||
|
|
||||||
|
Windows library that implements `TweetLib.Browser.CEF` using the [CefSharp](https://github.com/cefsharp/CefSharp/) library and Windows Forms.
|
||||||
|
|
||||||
|
### Linux Projects
|
||||||
|
|
||||||
|
#### TweetDuck
|
||||||
|
|
||||||
|
Main Linux executable. It has a transitive dependency on [ChromiumGtk](https://github.com/lunixo/ChromiumGtk). Here you will find the entry point that bootstraps the main application, as well as code for GUIs and Linux-specific functionality.
|
||||||
|
|
||||||
|
#### TweetImpl.CefGlue
|
||||||
|
|
||||||
|
Linux library that implements `TweetLib.Browser.CEF` using [ChromiumGtk](https://github.com/lunixo/ChromiumGtk), which is based on [CefGlue](https://gitlab.com/xiliumhq/chromiumembedded/cefglue) and [GtkSharp](https://github.com/GtkSharp/GtkSharp).
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
#### TweetLib.Communication
|
||||||
|
|
||||||
|
This library provides a `DuplexPipe` class for two-way communication between processes.
|
||||||
|
|
||||||
|
#### TweetLib.Utils
|
||||||
|
|
||||||
|
This library contains various utilities that fill some very specific holes in the .NET standard library.
|
||||||
|
|
||||||
|
#### TweetTest.*
|
||||||
|
|
||||||
|
These are F# projects with automated tests.
|
||||||
|
|
||||||
|
# Development (Windows)
|
||||||
|
|
||||||
|
When developing with [Rider](https://www.jetbrains.com/rider/), it must be configured to use MSBuild from Visual Studio, and the `DevEnvDir` property must be set to the full path to the `Common7\IDE` folder which is inside Visual Studio's installation folder. You can set both in **File | Settings | Build, Execution, Deployment | Toolset and Build**:
|
||||||
|
|
||||||
|
1. Click the `MSBuild version` drop-down, and select the path that includes the Visual Studio installation folder.
|
||||||
|
2. Click the Edit button next to `MSBuild global properties`.
|
||||||
|
3. Add a new property named `DevEnvDir`, and set its value to the full path to `Common7\IDE`. For example:
|
||||||
|
- `VS 2019 Community` - `C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE`
|
||||||
|
- `VS 2022 Community` - `C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE`
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
The `windows/TweetDuck/TweetDuck.csproj` project file has several tasks (targets) that run before and after a build:
|
||||||
|
* `PreBuildEvent` runs a PowerShell script that kills `TweetDuck.Browser` processes, in case they got stuck
|
||||||
|
* `CopyResources` copies resource files into the build folder, and patches and validates them using the `PostBuild.ps1` PowerShell script
|
||||||
|
* `FinalizeDebug` copies a debug plugin (`Resources/Plugins/.debug`) into the build folder (Debug only)
|
||||||
|
* `FinalizeRelease` prepares the build folder for publishing, and if InnoSetup is installed, regenerates the [update installer](#installers-1) (Release only)
|
||||||
|
|
||||||
|
If the build fails, usually with an error like `The command (...) exited with code 1`, open the **Output** tab for detailed logs. A possible cause is the `PostBuild.ps1` script's file validation:
|
||||||
|
* `Resources/Plugins/emoji-keyboard/emoji-ordering.txt` line endings must be LF (line feed); if the file contains any CR (carriage return) characters, the build will fail
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
|
||||||
|
|
||||||
|
While debugging, opening the main menu and clicking **Reload browser** automatically applies all changes to HTML/CSS/JS files in the `Resources/` folder. This allows editing and testing resource files without restarting the program, but it will cause a short delay between browser reloads.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
Open **Batch Build**, tick all `Release` configurations with `x86` platform, and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the end of the [Building](#building) section.
|
||||||
|
|
||||||
|
If the build succeeds, the `windows/TweetDuck/bin/x86/Release` folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#installers-1) section for automated installer generation.
|
||||||
|
|
||||||
|
If you decide to publicly release a custom version, please change all references to the TweetDuck name, website, and other links such as the issue tracker. The source files contain several constants and references to the official website and this repository, so don't forget to search all files for `chylex.com` and `github.com` in all files and replace them appropriately.
|
||||||
|
|
||||||
|
### Installers
|
||||||
|
|
||||||
|
If you have all the requirements for building [installers](#installers), you can generate them by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to create a [release build](#release) in Visual Studio first!
|
||||||
|
|
||||||
|
After the window closes, three installers will be generated inside the `bld/Output/` folder:
|
||||||
* **TweetDuck.exe**
|
* **TweetDuck.exe**
|
||||||
* This is the main installer that creates entries in the Start Menu & Programs and Features, and an optional desktop icon
|
* This is the main installer that creates entries in the Start Menu & Programs and Features, and an optional desktop icon
|
||||||
* **TweetDuck.Update.exe**
|
* **TweetDuck.Update.exe**
|
||||||
@@ -60,12 +215,28 @@ After the window closes, three installers will be generated inside the `bld/Outp
|
|||||||
* This is a portable installer that does not need administrator privileges
|
* This is a portable installer that does not need administrator privileges
|
||||||
* It automatically creates a `makeportable` file in the program folder, which forces TweetDuck to run in portable mode
|
* It automatically creates a `makeportable` file in the program folder, which forces TweetDuck to run in portable mode
|
||||||
|
|
||||||
The installers are built for GitHub Releases, where the main and portable installers can download and install Visual C++ if it's missing, and the update installer will download and apply the full installer when needed. If you plan to distribute your own installers via GitHub, you can change the variables in the installer files (`.iss`) and in the update system to point to your repository, and use the power of the existing update system.
|
If you plan to distribute your own installers, you can change the variables in the `.iss` installer files and in the update system to point to your own repository, and use the power of the existing update system.
|
||||||
|
|
||||||
#### Notes
|
|
||||||
|
|
||||||
> When opening **Batch Build**, you will also see `x64` and `AnyCPU` configurations. These are visible due to what I consider a Visual Studio bug, and will not work without significant changes to the project. Manually running the `Resources/PostCefUpdate.ps1` PowerShell script modifies the downloaded CefSharp packages, and removes the invalid configurations.
|
|
||||||
|
|
||||||
> There is a small chance running `GEN INSTALLERS.bat` immediately shows a resource error. If that happens, close the console window (which terminates all Inno Setup processes and leaves corrupted installers in the output folder), and run it again.
|
> There is a small chance running `GEN INSTALLERS.bat` immediately shows a resource error. If that happens, close the console window (which terminates all Inno Setup processes and leaves corrupted installers in the output folder), and run it again.
|
||||||
|
|
||||||
> Running `GEN INSTALLERS.bat` uses about 400 MB of RAM due to high compression. You can lower this to about 140 MB by opening `gen_full.iss` and `gen_port.iss`, and changing `LZMADictionarySize=15360` to `LZMADictionarySize=4096`.
|
> Running `GEN INSTALLERS.bat` uses about 400 MB of RAM due to high compression. You can lower this to about 140 MB by opening `gen_full.iss` and `gen_port.iss`, and changing `LZMADictionarySize=15360` to `LZMADictionarySize=4096`.
|
||||||
|
|
||||||
|
# Development (Linux)
|
||||||
|
|
||||||
|
Unfortunately the development experience on Linux is terrible, likely due to mixed C# and native code. The .NET debugger seems to crash the moment it enters native code, so the only way to run the app is without the debugger attached. If any C# code throws an exception, it will crash the whole application with no usable stack trace or error message. Please let me know if you find a way to make this better.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
The `linux/TweetDuck/TweetDuck.csproj` project file has several tasks (targets) that run after a build:
|
||||||
|
|
||||||
|
* `CopyResources` copies resource files into the build folder, and patches and validates them using the `build.sh` Bash script
|
||||||
|
* `FinalizeDebug` copies a debug plugin (`Resources/Plugins/.debug`) into the build folder (Debug only)
|
||||||
|
* `FinalizeRelease` prepares the build folder for publishing (Release only)
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
To change the application version before a release, search for the `<Version>` tag in every `.csproj` file in the `linux/` folder and modify it.
|
||||||
|
|
||||||
|
To build the application, execute the `linux/publish.sh` Bash script. This will build the Release configuration for the `linux-x64` runtime platform, and create a tarball in the `linux/bld/` folder.
|
||||||
|
|
||||||
|
If you decide to publicly release a custom version, please change all references to the TweetDuck name, website, and other links such as the issue tracker. The source files contain several constants and references to the official website and this repository, so don't forget to search all files for `chylex.com` and `github.com` in all files and replace them appropriately.
|
||||||
|
@@ -2,11 +2,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.28729.10
|
VisualStudioVersion = 16.0.28729.10
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck", "TweetDuck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck", "windows\TweetDuck\TweetDuck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Browser", "subprocess\TweetDuck.Browser.csproj", "{B10B0017-819E-4F71-870F-8256B36A26AA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Browser", "windows\TweetDuck.Browser\TweetDuck.Browser.csproj", "{B10B0017-819E-4F71-870F-8256B36A26AA}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "video\TweetDuck.Video.csproj", "{278B2D11-402D-44B6-B6A1-8FA67DB65565}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "windows\TweetDuck.Video\TweetDuck.Video.csproj", "{278B2D11-402D-44B6-B6A1-8FA67DB65565}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetImpl.CefSharp", "windows\TweetImpl.CefSharp\TweetImpl.CefSharp.csproj", "{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -14,10 +16,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetL
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser", "lib\TweetLib.Browser\TweetLib.Browser.csproj", "{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser", "lib\TweetLib.Browser\TweetLib.Browser.csproj", "{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}"
|
||||||
EndProject
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Utils", "lib\TweetLib.Utils\TweetLib.Utils.csproj", "{476B1007-B12C-447F-B855-9886048201D6}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Core", "lib\TweetTest.Core\TweetTest.Core.fsproj", "{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Core", "lib\TweetTest.Core\TweetTest.Core.fsproj", "{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}"
|
||||||
EndProject
|
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}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Utils", "lib\TweetTest.Utils\TweetTest.Utils.fsproj", "{07F6D350-B16F-44E2-804D-C1142E1E345F}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@@ -38,6 +44,10 @@ Global
|
|||||||
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.Build.0 = Debug|x86
|
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.Build.0 = Debug|x86
|
||||||
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.ActiveCfg = Release|x86
|
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.ActiveCfg = Release|x86
|
||||||
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.Build.0 = Release|x86
|
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.Build.0 = Release|x86
|
||||||
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Debug|x86.Build.0 = Debug|x86
|
||||||
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.Build.0 = Release|x86
|
||||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
|
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
|
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
|
||||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
|
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
|
||||||
@@ -50,6 +60,10 @@ Global
|
|||||||
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Debug|x86.Build.0 = Debug|x86
|
{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.ActiveCfg = Release|x86
|
||||||
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.Build.0 = 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.ActiveCfg = Debug|x86
|
||||||
{476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.Build.0 = Debug|x86
|
{476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.Build.0 = Debug|x86
|
||||||
{476B1007-B12C-447F-B855-9886048201D6}.Release|x86.ActiveCfg = Release|x86
|
{476B1007-B12C-447F-B855-9886048201D6}.Release|x86.ActiveCfg = Release|x86
|
||||||
@@ -58,6 +72,10 @@ Global
|
|||||||
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Debug|x86.Build.0 = Debug|x86
|
{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.ActiveCfg = Release|x86
|
||||||
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.Build.0 = 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.ActiveCfg = Debug|x86
|
||||||
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.Build.0 = Debug|x86
|
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.Build.0 = Debug|x86
|
||||||
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Release|x86.ActiveCfg = Release|x86
|
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
@@ -6,6 +6,6 @@ using TweetDuck;
|
|||||||
|
|
||||||
namespace TweetDuck {
|
namespace TweetDuck {
|
||||||
internal static class Version {
|
internal static class Version {
|
||||||
public const string Tag = "1.21.2";
|
public const string Tag = "1.22.1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
#define MyAppShortURL "https://td.chylex.com"
|
#define MyAppShortURL "https://td.chylex.com"
|
||||||
#define MyAppExeName "TweetDuck.exe"
|
#define MyAppExeName "TweetDuck.exe"
|
||||||
|
|
||||||
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
|
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
||||||
@@ -43,8 +43,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
|||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
#define MyAppShortURL "https://td.chylex.com"
|
#define MyAppShortURL "https://td.chylex.com"
|
||||||
#define MyAppExeName "TweetDuck.exe"
|
#define MyAppExeName "TweetDuck.exe"
|
||||||
|
|
||||||
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
|
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
||||||
@@ -40,8 +40,8 @@ MinVersion=0,6.1
|
|||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
|
||||||
|
@@ -8,8 +8,8 @@
|
|||||||
#define MyAppExeName "TweetDuck.exe"
|
#define MyAppExeName "TweetDuck.exe"
|
||||||
|
|
||||||
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
||||||
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
|
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
||||||
#define CefVersion GetFileVersion("..\bin\x86\Release\libcef.dll")
|
#define CefVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\libcef.dll")
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{{#MyAppID}}
|
AppId={{{#MyAppID}}
|
||||||
@@ -43,12 +43,13 @@ MinVersion=0,6.1
|
|||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\bin\x86\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\x86\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\bin\x86\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\x86\Release\TweetImpl.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\bin\x86\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\x86\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\bin\x86\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\x86\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
Source: "..\bin\x86\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\x86\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
Source: "..\windows\TweetDuck\bin\x86\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||||
|
111
lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
Normal file
111
lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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, TRequest> : 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, TRequest> browser;
|
||||||
|
private readonly ICefAdapter cefAdapter;
|
||||||
|
private readonly IFrameAdapter<TFrame> frameAdapter;
|
||||||
|
private readonly IRequestAdapter<TRequest> requestAdapter;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs {
|
||||||
|
private readonly IBrowserWrapper<TFrame, TRequest> browser;
|
||||||
|
|
||||||
|
public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame, TRequest> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs
Normal file
26
lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs
Normal 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) {}
|
||||||
|
}
|
||||||
|
}
|
29
lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs
Normal file
29
lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Data {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
Normal file
33
lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs
Normal file
47
lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs
Normal 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 _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs
Normal file
7
lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Dialogs {
|
||||||
|
public enum FileDialogType {
|
||||||
|
Open,
|
||||||
|
OpenMultiple,
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
}
|
8
lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs
Normal file
8
lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Dialogs {
|
||||||
|
public enum JsDialogType {
|
||||||
|
Alert,
|
||||||
|
Confirm,
|
||||||
|
Prompt,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
}
|
9
lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs
Normal file
9
lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Dialogs {
|
||||||
|
public enum MessageDialogType {
|
||||||
|
None,
|
||||||
|
Question,
|
||||||
|
Information,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
}
|
13
lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
Normal file
13
lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using TweetLib.Browser.CEF.Data;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
7
lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
Normal file
7
lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface ICefAdapter {
|
||||||
|
void RunOnUiThread(Action action);
|
||||||
|
}
|
||||||
|
}
|
9
lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs
Normal file
9
lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
6
lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs
Normal file
6
lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface IErrorCodeAdapter<T> {
|
||||||
|
bool IsAborted(T errorCode);
|
||||||
|
string? GetName(T errorCode);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface IFileDialogCallbackAdapter<T> {
|
||||||
|
void Continue(T callback, int selectedAcceptFilter, string[] filePaths);
|
||||||
|
void Cancel(T callback);
|
||||||
|
void Dispose(T callback);
|
||||||
|
}
|
||||||
|
}
|
9
lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs
Normal file
9
lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using TweetLib.Utils.Dialogs;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface IFileDialogOpener {
|
||||||
|
void OpenFile(string title, bool multiple, List<FileDialogFilter> filters, Action<string[]> onAccepted, Action onCancelled);
|
||||||
|
}
|
||||||
|
}
|
8
lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs
Normal file
8
lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface IJsDialogCallbackAdapter<T> {
|
||||||
|
void Continue(T callback, bool success, string? userInput = null);
|
||||||
|
void Dispose(T callback);
|
||||||
|
}
|
||||||
|
}
|
10
lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs
Normal file
10
lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using TweetLib.Browser.CEF.Dialogs;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface IJsDialogOpener {
|
||||||
|
void Alert(MessageDialogType type, string title, string message, Action<bool> callback);
|
||||||
|
void Confirm(MessageDialogType type, string title, string message, Action<bool> callback);
|
||||||
|
void Prompt(MessageDialogType type, string title, string message, Action<bool, string> callback);
|
||||||
|
}
|
||||||
|
}
|
16
lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
Normal file
16
lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
6
lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs
Normal file
6
lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TweetLib.Browser.CEF.Interfaces {
|
||||||
|
public interface IPopupHandler {
|
||||||
|
bool IsPopupAllowed(string url);
|
||||||
|
void OpenExternalBrowser(string url);
|
||||||
|
}
|
||||||
|
}
|
25
lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
Normal file
25
lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
void SetMethod(T request, string method);
|
||||||
|
|
||||||
|
bool IsTransitionForwardBack(T request);
|
||||||
|
|
||||||
|
bool IsCspReport(T request);
|
||||||
|
|
||||||
|
ResourceType GetResourceType(T request);
|
||||||
|
|
||||||
|
void SetHeader(T request, string header, string value);
|
||||||
|
|
||||||
|
void SetReferrer(T request, string referrer);
|
||||||
|
|
||||||
|
void SetAllowStoredCredentials(T request);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
9
lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs
Normal file
9
lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
5
lib/TweetLib.Browser.CEF/Lib.cs
Normal file
5
lib/TweetLib.Browser.CEF/Lib.cs
Normal 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")]
|
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using TweetLib.Browser.CEF.Data;
|
||||||
|
using TweetLib.Browser.CEF.Interfaces;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Logic {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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, long maxBytesToRead, out int bytesRead, IDisposable callback) {
|
||||||
|
callback.Dispose();
|
||||||
|
|
||||||
|
if (maxBytesToRead == 0) {
|
||||||
|
bytesRead = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int bytesToRead = (int) Math.Min(maxBytesToRead, resource.Length - position);
|
||||||
|
|
||||||
|
write(dataOut, resource.Contents, position, bytesToRead);
|
||||||
|
position += bytesToRead;
|
||||||
|
bytesRead = bytesToRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesRead > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
Normal file
125
lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs
Normal file
75
lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using TweetLib.Browser.CEF.Dialogs;
|
||||||
|
using TweetLib.Browser.CEF.Interfaces;
|
||||||
|
using TweetLib.Utils.Dialogs;
|
||||||
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Logic {
|
||||||
|
public sealed class DialogHandlerLogic<TCallback> {
|
||||||
|
private readonly IFileDialogOpener fileDialogOpener;
|
||||||
|
private readonly IFileDialogCallbackAdapter<TCallback> callbackAdapter;
|
||||||
|
|
||||||
|
public DialogHandlerLogic(IFileDialogOpener fileDialogOpener, IFileDialogCallbackAdapter<TCallback> callbackAdapter) {
|
||||||
|
this.fileDialogOpener = fileDialogOpener;
|
||||||
|
this.callbackAdapter = callbackAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnFileDialog(FileDialogType type, IEnumerable<string> acceptFilters, TCallback callback) {
|
||||||
|
if (type is FileDialogType.Open or FileDialogType.OpenMultiple) {
|
||||||
|
var multiple = type == FileDialogType.OpenMultiple;
|
||||||
|
var supportedExtensions = acceptFilters.SelectMany(ParseFileType).Where(static filter => !string.IsNullOrEmpty(filter)).ToArray();
|
||||||
|
|
||||||
|
var filters = new List<FileDialogFilter> {
|
||||||
|
new ("All Supported Formats", supportedExtensions),
|
||||||
|
new ("All Files", ".*")
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDialogOpener.OpenFile("Open Files", multiple, filters, files => {
|
||||||
|
string ext = Path.GetExtension(files[0])!.ToLower();
|
||||||
|
callbackAdapter.Continue(callback, Array.FindIndex(supportedExtensions, filter => ParseFileType(filter).Contains(ext)), files);
|
||||||
|
callbackAdapter.Dispose(callback);
|
||||||
|
}, () => {
|
||||||
|
callbackAdapter.Cancel(callback);
|
||||||
|
callbackAdapter.Dispose(callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callbackAdapter.Dispose(callback);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ParseFileType(string type) {
|
||||||
|
if (string.IsNullOrEmpty(type)) {
|
||||||
|
return StringUtils.EmptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type[0] == '.') {
|
||||||
|
return new string[] { type };
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] extensions = type switch {
|
||||||
|
"image/jpeg" => new string[] { ".jpg", ".jpeg" },
|
||||||
|
"image/png" => new string[] { ".png" },
|
||||||
|
"image/gif" => new string[] { ".gif" },
|
||||||
|
"image/webp" => new string[] { ".webp" },
|
||||||
|
"video/mp4" => new string[] { ".mp4" },
|
||||||
|
"video/quicktime" => new string[] { ".mov", ".qt" },
|
||||||
|
_ => StringUtils.EmptyArray
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extensions.Length == 0) {
|
||||||
|
Debug.WriteLine("Unknown file type: " + type);
|
||||||
|
Debugger.Break();
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
Normal file
64
lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using TweetLib.Browser.CEF.Data;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Logic {
|
||||||
|
public sealed class DownloadRequestClientLogic {
|
||||||
|
public enum RequestStatus {
|
||||||
|
Unknown,
|
||||||
|
Success,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly DownloadCallbacks callbacks;
|
||||||
|
private bool hasFailed;
|
||||||
|
|
||||||
|
public DownloadRequestClientLogic(DownloadCallbacks callbacks) {
|
||||||
|
this.callbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool GetAuthCredentials(IDisposable callback) {
|
||||||
|
callback.Dispose();
|
||||||
|
|
||||||
|
hasFailed = true;
|
||||||
|
callbacks.OnError(new Exception("This URL requires authentication."));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDownloadData(Stream data) {
|
||||||
|
if (hasFailed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
callbacks.OnData(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
callbacks.OnError(e);
|
||||||
|
hasFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "SwitchStatementMissingSomeEnumCasesNoDefault")]
|
||||||
|
public void OnRequestComplete(RequestStatus status) {
|
||||||
|
if (hasFailed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case RequestStatus.Success when callbacks.HasData:
|
||||||
|
callbacks.OnSuccess();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RequestStatus.Success:
|
||||||
|
callbacks.OnError(new Exception("File is empty."));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callbacks.OnError(new Exception("Unknown error."));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs
Normal file
37
lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs
Normal file
71
lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using TweetLib.Browser.CEF.Dialogs;
|
||||||
|
using TweetLib.Browser.CEF.Interfaces;
|
||||||
|
|
||||||
|
namespace TweetLib.Browser.CEF.Logic {
|
||||||
|
public sealed class JsDialogHandlerLogic<TCallback> {
|
||||||
|
private static (MessageDialogType, string) GetMessageDialogProperties(string text) {
|
||||||
|
MessageDialogType type = MessageDialogType.None;
|
||||||
|
|
||||||
|
int pipe = text.IndexOf('|');
|
||||||
|
if (pipe != -1) {
|
||||||
|
type = text.Substring(0, pipe) switch {
|
||||||
|
"error" => MessageDialogType.Error,
|
||||||
|
"warning" => MessageDialogType.Warning,
|
||||||
|
"info" => MessageDialogType.Information,
|
||||||
|
"question" => MessageDialogType.Question,
|
||||||
|
_ => MessageDialogType.None
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type != MessageDialogType.None) {
|
||||||
|
text = text.Substring(pipe + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (type, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IJsDialogOpener jsDialogOpener;
|
||||||
|
private readonly IJsDialogCallbackAdapter<TCallback> callbackAdapter;
|
||||||
|
|
||||||
|
public JsDialogHandlerLogic(IJsDialogOpener jsDialogOpener, IJsDialogCallbackAdapter<TCallback> callbackAdapter) {
|
||||||
|
this.jsDialogOpener = jsDialogOpener;
|
||||||
|
this.callbackAdapter = callbackAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnJSDialog(JsDialogType dialogType, string messageText, TCallback callback, out bool suppressMessage) {
|
||||||
|
suppressMessage = false;
|
||||||
|
|
||||||
|
var (type, text) = GetMessageDialogProperties(messageText);
|
||||||
|
|
||||||
|
if (dialogType == JsDialogType.Alert) {
|
||||||
|
jsDialogOpener.Alert(type, "Browser Message", text, success => {
|
||||||
|
callbackAdapter.Continue(callback, success);
|
||||||
|
callbackAdapter.Dispose(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (dialogType == JsDialogType.Confirm) {
|
||||||
|
jsDialogOpener.Confirm(type, "Browser Confirmation", text, success => {
|
||||||
|
callbackAdapter.Continue(callback, success);
|
||||||
|
callbackAdapter.Dispose(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (dialogType == JsDialogType.Prompt) {
|
||||||
|
jsDialogOpener.Prompt(type, "Browser Prompt", text, (success, input) => {
|
||||||
|
callbackAdapter.Continue(callback, success, input);
|
||||||
|
callbackAdapter.Dispose(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnBeforeUnloadDialog(IDisposable callback) {
|
||||||
|
callback.Dispose();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs
Normal file
37
lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs
Normal file
29
lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
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));
|
||||||
|
|
||||||
|
if (result is RequestHandleResult.Redirect redirect) {
|
||||||
|
requestAdapter.SetUrl(request, redirect.Url);
|
||||||
|
}
|
||||||
|
else if (result is RequestHandleResult.Process process) {
|
||||||
|
requestAdapter.SetHeader(request, "Accept-Encoding", "identity");
|
||||||
|
responseProcessors[requestAdapter.GetIdentifier(request)] = process.Processor;
|
||||||
|
}
|
||||||
|
else if (result == 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using CefSharp;
|
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
namespace TweetLib.Browser.CEF.Logic {
|
||||||
sealed class ResponseFilter : IResponseFilter {
|
public sealed class ResponseFilterLogic {
|
||||||
|
public enum FilterStatus {
|
||||||
|
NeedMoreData,
|
||||||
|
Done
|
||||||
|
}
|
||||||
|
|
||||||
private enum State {
|
private enum State {
|
||||||
Reading,
|
Reading,
|
||||||
Writing,
|
Writing,
|
||||||
@@ -17,26 +21,21 @@ namespace TweetDuck.Browser.Handling {
|
|||||||
private State state;
|
private State state;
|
||||||
private int offset;
|
private int offset;
|
||||||
|
|
||||||
public ResponseFilter(IResponseProcessor processor, int totalBytes) {
|
internal ResponseFilterLogic(IResponseProcessor processor, int totalBytes) {
|
||||||
this.processor = processor;
|
this.processor = processor;
|
||||||
this.responseData = new byte[totalBytes];
|
this.responseData = new byte[totalBytes];
|
||||||
this.state = State.Reading;
|
this.state = State.Reading;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool InitFilter() {
|
public FilterStatus Filter(Stream? dataIn, out long dataInRead, Stream dataOut, long dataOutLength, out long dataOutWritten) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) {
|
|
||||||
int responseLength = responseData.Length;
|
int responseLength = responseData.Length;
|
||||||
|
|
||||||
if (state == State.Reading) {
|
if (state == State.Reading) {
|
||||||
int bytesToRead = Math.Min(responseLength - offset, (int) Math.Min(dataIn?.Length ?? 0, int.MaxValue));
|
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 += bytesRead;
|
||||||
offset += bytesToRead;
|
dataInRead = bytesRead;
|
||||||
|
|
||||||
dataInRead = bytesToRead;
|
|
||||||
dataOutWritten = 0;
|
dataOutWritten = 0;
|
||||||
|
|
||||||
if (offset >= responseLength) {
|
if (offset >= responseLength) {
|
||||||
@@ -48,7 +47,7 @@ namespace TweetDuck.Browser.Handling {
|
|||||||
return FilterStatus.NeedMoreData;
|
return FilterStatus.NeedMoreData;
|
||||||
}
|
}
|
||||||
else if (state == State.Writing) {
|
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) {
|
if (bytesToWrite > 0) {
|
||||||
dataOut.Write(responseData, offset, bytesToWrite);
|
dataOut.Write(responseData, offset, bytesToWrite);
|
||||||
@@ -67,10 +66,8 @@ namespace TweetDuck.Browser.Handling {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new InvalidOperationException("This resource filter cannot be reused.");
|
throw new InvalidOperationException("This resource filter cannot be reused!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
21
lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs
Normal file
21
lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs
Normal file
29
lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs
Normal 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 {
|
||||||
|
abstract class SchemeResourceVisitor {
|
||||||
|
protected static readonly SchemeResource.Status FileIsEmpty = new (HttpStatusCode.NoContent, "File is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj
Normal file
21
lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
<Platforms>x86;x64</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>
|
@@ -1,8 +1,13 @@
|
|||||||
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using TweetLib.Utils.Collections;
|
using TweetLib.Utils.Collections;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Chromium {
|
namespace TweetLib.Browser.CEF.Utils {
|
||||||
public static class CefUtils {
|
public static class CefUtils {
|
||||||
|
public static string GetCacheFolder(string storagePath) {
|
||||||
|
return Path.Combine(storagePath, "Cache");
|
||||||
|
}
|
||||||
|
|
||||||
public static CommandLineArgs ParseCommandLineArguments(string argumentString) {
|
public static CommandLineArgs ParseCommandLineArguments(string argumentString) {
|
||||||
CommandLineArgs args = new CommandLineArgs();
|
CommandLineArgs args = new CommandLineArgs();
|
||||||
|
|
@@ -3,7 +3,7 @@ using System.Linq;
|
|||||||
using TweetLib.Utils.Globalization;
|
using TweetLib.Utils.Globalization;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Chromium {
|
namespace TweetLib.Browser.CEF.Utils {
|
||||||
public static class SpellCheck {
|
public static class SpellCheck {
|
||||||
// https://cs.chromium.org/chromium/src/third_party/hunspell_dictionaries/
|
// https://cs.chromium.org/chromium/src/third_party/hunspell_dictionaries/
|
||||||
public static IEnumerable<Language> SupportedLanguages { get; } = new List<string> {
|
public static IEnumerable<Language> SupportedLanguages { get; } = new List<string> {
|
@@ -1,8 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace TweetLib.Browser.Interfaces {
|
|
||||||
public interface IResourceProvider<T> {
|
|
||||||
T Status(HttpStatusCode code, string message);
|
|
||||||
T File(byte[] contents, string extension);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +1,9 @@
|
|||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
|
|
||||||
namespace TweetLib.Browser.Request {
|
namespace TweetLib.Browser.Request {
|
||||||
public abstract class RequestHandleResult {
|
public class RequestHandleResult {
|
||||||
|
public static RequestHandleResult Cancel { get; } = new ();
|
||||||
|
|
||||||
private RequestHandleResult() {}
|
private RequestHandleResult() {}
|
||||||
|
|
||||||
public sealed class Redirect : RequestHandleResult {
|
public sealed class Redirect : RequestHandleResult {
|
||||||
@@ -19,11 +21,5 @@ namespace TweetLib.Browser.Request {
|
|||||||
Processor = processor;
|
Processor = processor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class Cancel : RequestHandleResult {
|
|
||||||
public static Cancel Instance { get; } = new ();
|
|
||||||
|
|
||||||
private Cancel() {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,18 +2,17 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using TweetLib.Browser.Request;
|
|
||||||
using IOFile = System.IO.File;
|
using IOFile = System.IO.File;
|
||||||
|
|
||||||
namespace TweetLib.Core.Resources {
|
namespace TweetLib.Browser.Request {
|
||||||
public sealed class ResourceCache {
|
public sealed class ResourceCache {
|
||||||
private readonly Dictionary<string, SchemeResource> cache = new ();
|
private readonly Dictionary<string, SchemeResource> cache = new ();
|
||||||
|
|
||||||
public void ClearCache() {
|
public void Clear() {
|
||||||
cache.Clear();
|
cache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal SchemeResource ReadFile(string path) {
|
public SchemeResource ReadFile(string path) {
|
||||||
string key = new Uri(path).LocalPath;
|
string key = new Uri(path).LocalPath;
|
||||||
|
|
||||||
if (cache.TryGetValue(key, out var cachedResource)) {
|
if (cache.TryGetValue(key, out var cachedResource)) {
|
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<Platforms>x86</Platforms>
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
<Platforms>x86;x64</Platforms>
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>9</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using TweetLib.Browser.Request;
|
||||||
using TweetLib.Core.Application;
|
using TweetLib.Core.Application;
|
||||||
using TweetLib.Core.Features;
|
using TweetLib.Core.Features;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
using TweetLib.Core.Resources;
|
|
||||||
using TweetLib.Core.Systems.Configuration;
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using TweetLib.Core.Systems.Logging;
|
using TweetLib.Core.Systems.Logging;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
@@ -17,22 +17,23 @@ namespace TweetLib.Core {
|
|||||||
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
|
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
public static readonly bool IsPortable = Setup.IsPortable;
|
public static readonly bool IsPortable = Setup.IsPortable;
|
||||||
|
|
||||||
public static readonly string ResourcesPath = Path.Combine(ProgramPath, "resources");
|
internal static readonly string ResourcesPath = Path.Combine(ProgramPath, "resources");
|
||||||
public static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
internal static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
||||||
public static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
|
internal static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
|
||||||
|
|
||||||
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
|
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
|
||||||
|
public static readonly string LogoPath = Path.Combine(ResourcesPath, "images/logo.png");
|
||||||
|
|
||||||
public static Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
|
public static Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
|
||||||
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
|
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
|
||||||
|
|
||||||
public static IAppErrorHandler ErrorHandler { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
|
public static IAppErrorHandler ErrorHandler { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
|
||||||
public static IAppSystemHandler SystemHandler { get; } = Validate(Builder.SystemHandler, nameof(Builder.SystemHandler));
|
public static IAppSystemHandler SystemHandler { get; } = Validate(Builder.SystemHandler, nameof(Builder.SystemHandler));
|
||||||
public static IAppMessageDialogs MessageDialogs { get; } = Validate(Builder.MessageDialogs, nameof(Builder.MessageDialogs));
|
|
||||||
public static IAppFileDialogs? FileDialogs { get; } = Builder.FileDialogs;
|
internal static IAppMessageDialogs MessageDialogs { get; } = Validate(Builder.MessageDialogs, nameof(Builder.MessageDialogs));
|
||||||
|
internal static IAppFileDialogs? FileDialogs { get; } = Builder.FileDialogs;
|
||||||
|
|
||||||
internal static IAppUserConfiguration UserConfiguration => ConfigManager.User;
|
internal static IAppUserConfiguration UserConfiguration => ConfigManager.User;
|
||||||
internal static IAppSystemConfiguration SystemConfiguration => ConfigManager.System;
|
|
||||||
|
|
||||||
private static string GetDataFolder() {
|
private static string GetDataFolder() {
|
||||||
string? custom = Setup.CustomDataFolder;
|
string? custom = Setup.CustomDataFolder;
|
||||||
@@ -75,7 +76,7 @@ namespace TweetLib.Core {
|
|||||||
|
|
||||||
WebUtils.DefaultUserAgent = Lib.BrandName + " " + Version.Tag;
|
WebUtils.DefaultUserAgent = Lib.BrandName + " " + Version.Tag;
|
||||||
|
|
||||||
if (SystemConfiguration.UseSystemProxyForAllConnections) {
|
if (ConfigManager.System.UseSystemProxyForAllConnections) {
|
||||||
WebUtils.EnableSystemProxy();
|
WebUtils.EnableSystemProxy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,20 +92,20 @@ namespace TweetLib.Core {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
|
|
||||||
private static AppBuilder Builder => AppBuilder.Instance ?? throw new InvalidOperationException("App is initializing too early");
|
private static AppBuilder Builder => AppBuilder.Instance ?? throw new InvalidOperationException("App is initializing too early!");
|
||||||
|
|
||||||
private static bool isInitialized = false;
|
private static bool isInitialized = false;
|
||||||
|
|
||||||
internal static void Initialize() {
|
internal static void Initialize() {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
throw new InvalidOperationException("App is already initialized");
|
throw new InvalidOperationException("App is already initialized!");
|
||||||
}
|
}
|
||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T Validate<T>(T? obj, string name) where T : class {
|
private static T Validate<T>(T? obj, string name) where T : class {
|
||||||
return obj ?? throw new InvalidOperationException("Missing property " + name + " on the provided App");
|
return obj ?? throw new InvalidOperationException("Missing property " + name + " on the provided App.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using TweetLib.Core.Systems.Dialogs;
|
using TweetLib.Utils.Dialogs;
|
||||||
|
|
||||||
namespace TweetLib.Core.Application {
|
namespace TweetLib.Core.Application {
|
||||||
public interface IAppFileDialogs {
|
public interface IAppFileDialogs {
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
using TweetLib.Core.Systems.Dialogs;
|
|
||||||
|
|
||||||
namespace TweetLib.Core.Application {
|
namespace TweetLib.Core.Application {
|
||||||
public interface IAppMessageDialogs {
|
public interface IAppMessageDialogs {
|
||||||
void Information(string caption, string text, string buttonAccept = Dialogs.OK);
|
void Information(string caption, string text);
|
||||||
void Error(string caption, string text, string buttonAccept = Dialogs.OK);
|
void Error(string caption, string text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
using TweetLib.Browser.Request;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
using TweetLib.Core.Resources;
|
|
||||||
using TweetLib.Core.Systems.Configuration;
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetLib.Core.Application {
|
namespace TweetLib.Core.Application {
|
||||||
|
@@ -4,7 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
using TweetLib.Core.Systems.Dialogs;
|
using TweetLib.Utils.Dialogs;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features {
|
namespace TweetLib.Core.Features {
|
||||||
|
@@ -4,7 +4,7 @@ using TweetLib.Core.Features.Notifications;
|
|||||||
namespace TweetLib.Core.Features {
|
namespace TweetLib.Core.Features {
|
||||||
public interface ICommonInterface {
|
public interface ICommonInterface {
|
||||||
void Alert(string type, string contents);
|
void Alert(string type, string contents);
|
||||||
void DisplayTooltip(string text);
|
void DisplayTooltip(string? text);
|
||||||
void FixClipboard();
|
void FixClipboard();
|
||||||
int GetIdleSeconds();
|
int GetIdleSeconds();
|
||||||
void OnSoundNotification();
|
void OnSoundNotification();
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using TweetLib.Utils.Data;
|
using TweetLib.Utils.Data;
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
return 2000 + Math.Max(1000, value * characters);
|
return 2000 + Math.Max(1000, value * characters);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GenerateHtml(string bodyClasses, string? headLayout, string? customStyles, IEnumerable<InjectedString> injections, string[] scripts) { // TODO
|
internal string GenerateHtml(string bodyClasses, string? headLayout, string? customStyles, IEnumerable<InjectedString> injections, string[] scripts) { // TODO
|
||||||
headLayout ??= DefaultHeadLayout;
|
headLayout ??= DefaultHeadLayout;
|
||||||
customStyles ??= string.Empty;
|
customStyles ??= string.Empty;
|
||||||
|
|
||||||
@@ -71,12 +72,7 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
build.Append("<tweetduck-script-placeholder></body></html>");
|
build.Append("<tweetduck-script-placeholder></body></html>");
|
||||||
|
|
||||||
string result = build.ToString();
|
string result = build.ToString();
|
||||||
|
return injections.Aggregate(result, static (current, injection) => injection.InjectInto(current)).Replace("<tweetduck-script-placeholder>", GenerateScripts(scripts));
|
||||||
foreach (var injection in injections) {
|
|
||||||
result = injection.InjectInto(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Replace("<tweetduck-script-placeholder>", GenerateScripts(scripts));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GenerateScripts(string[] scripts) {
|
private string GenerateScripts(string[] scripts) {
|
||||||
|
@@ -3,7 +3,7 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
bool FreezeTimer { get; set; }
|
bool FreezeTimer { get; set; }
|
||||||
bool IsHovered { get; }
|
bool IsHovered { get; }
|
||||||
|
|
||||||
void DisplayTooltip(string text);
|
void DisplayTooltip(string? text);
|
||||||
void FinishCurrentNotification();
|
void FinishCurrentNotification();
|
||||||
void ShowTweetDetail();
|
void ShowTweetDetail();
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
this.i = notificationInterface;
|
this.i = notificationInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisplayTooltip(string text) {
|
public void DisplayTooltip(string? text) {
|
||||||
i.DisplayTooltip(text);
|
i.DisplayTooltip(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static string? FontSize { get; private set; }
|
public static string? FontSize { get; private set; }
|
||||||
public static string? HeadLayout { get; private set; }
|
private static string? HeadLayout { get; set; }
|
||||||
|
|
||||||
private NotificationBrowser(IBrowserComponent browserComponent, Func<NotificationBrowser, BrowserSetup> setup) : base(browserComponent, setup) {}
|
private NotificationBrowser(IBrowserComponent browserComponent, Func<NotificationBrowser, BrowserSetup> setup) : base(browserComponent, setup) {}
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ namespace TweetLib.Core.Features.Plugins.Config {
|
|||||||
public sealed class PluginConfig : IConfigObject<PluginConfig> {
|
public sealed class PluginConfig : IConfigObject<PluginConfig> {
|
||||||
internal IEnumerable<string> DisabledPlugins => disabled;
|
internal IEnumerable<string> DisabledPlugins => disabled;
|
||||||
|
|
||||||
public event EventHandler<PluginChangedStateEventArgs>? PluginChangedState;
|
internal event EventHandler<PluginChangedStateEventArgs>? PluginChangedState;
|
||||||
|
|
||||||
private readonly HashSet<string> defaultDisabled;
|
private readonly HashSet<string> defaultDisabled;
|
||||||
private readonly HashSet<string> disabled;
|
private readonly HashSet<string> disabled;
|
||||||
|
@@ -6,12 +6,11 @@ using TweetLib.Core.Systems.Configuration;
|
|||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins.Config {
|
namespace TweetLib.Core.Features.Plugins.Config {
|
||||||
sealed class PluginConfigInstance : IConfigInstance {
|
sealed class PluginConfigInstance : IConfigInstance {
|
||||||
public PluginConfig Instance { get; }
|
private readonly PluginConfig instance;
|
||||||
|
|
||||||
private readonly string filename;
|
private readonly string filename;
|
||||||
|
|
||||||
public PluginConfigInstance(string filename, PluginConfig instance) {
|
public PluginConfigInstance(string filename, PluginConfig instance) {
|
||||||
this.Instance = instance;
|
this.instance = instance;
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ namespace TweetLib.Core.Features.Plugins.Config {
|
|||||||
newDisabled.Add(line);
|
newDisabled.Add(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance.Reset(newDisabled);
|
instance.Reset(newDisabled);
|
||||||
}
|
}
|
||||||
} catch (FileNotFoundException) {
|
} catch (FileNotFoundException) {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -43,7 +42,7 @@ namespace TweetLib.Core.Features.Plugins.Config {
|
|||||||
using var writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8);
|
using var writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8);
|
||||||
writer.WriteLine("#Disabled");
|
writer.WriteLine("#Disabled");
|
||||||
|
|
||||||
foreach (string identifier in Instance.DisabledPlugins) {
|
foreach (string identifier in instance.DisabledPlugins) {
|
||||||
writer.WriteLine(identifier);
|
writer.WriteLine(identifier);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -58,7 +57,7 @@ namespace TweetLib.Core.Features.Plugins.Config {
|
|||||||
public void Reset() {
|
public void Reset() {
|
||||||
try {
|
try {
|
||||||
File.Delete(filename);
|
File.Delete(filename);
|
||||||
Instance.ResetToDefault();
|
instance.ResetToDefault();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
OnException("Could not delete the plugin configuration file.", e);
|
OnException("Could not delete the plugin configuration file.", e);
|
||||||
return;
|
return;
|
||||||
|
@@ -2,12 +2,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins.Enums {
|
namespace TweetLib.Core.Features.Plugins.Enums {
|
||||||
public enum PluginEnvironment {
|
enum PluginEnvironment {
|
||||||
Browser,
|
Browser,
|
||||||
Notification
|
Notification
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class PluginEnvironments {
|
static class PluginEnvironments {
|
||||||
public static IEnumerable<PluginEnvironment> All { get; } = new PluginEnvironment[] {
|
public static IEnumerable<PluginEnvironment> All { get; } = new PluginEnvironment[] {
|
||||||
PluginEnvironment.Browser,
|
PluginEnvironment.Browser,
|
||||||
PluginEnvironment.Notification
|
PluginEnvironment.Notification
|
||||||
|
@@ -7,7 +7,7 @@ namespace TweetLib.Core.Features.Plugins.Enums {
|
|||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class PluginGroups {
|
static class PluginGroups {
|
||||||
public static IEnumerable<PluginGroup> All { get; } = new PluginGroup[] {
|
public static IEnumerable<PluginGroup> All { get; } = new PluginGroup[] {
|
||||||
PluginGroup.Official,
|
PluginGroup.Official,
|
||||||
PluginGroup.Custom
|
PluginGroup.Custom
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins.Events {
|
namespace TweetLib.Core.Features.Plugins.Events {
|
||||||
public sealed class PluginChangedStateEventArgs : EventArgs {
|
internal sealed class PluginChangedStateEventArgs : EventArgs {
|
||||||
public Plugin Plugin { get; }
|
public Plugin Plugin { get; }
|
||||||
public bool IsEnabled { get; }
|
public bool IsEnabled { get; }
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins.Events {
|
namespace TweetLib.Core.Features.Plugins.Events {
|
||||||
public sealed class PluginErrorEventArgs : EventArgs {
|
internal sealed class PluginErrorEventArgs : EventArgs {
|
||||||
public bool HasErrors => Errors.Count > 0;
|
public bool HasErrors => Errors.Count > 0;
|
||||||
|
|
||||||
public IList<string> Errors { get; }
|
public IList<string> Errors { get; }
|
||||||
|
@@ -7,7 +7,7 @@ using TweetLib.Utils.Static;
|
|||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins {
|
namespace TweetLib.Core.Features.Plugins {
|
||||||
public sealed class Plugin {
|
public sealed class Plugin {
|
||||||
private static readonly Version AppVersion = new (TweetDuck.Version.Tag);
|
public static Version LibVersion { get; } = new (TweetDuck.Version.Tag);
|
||||||
|
|
||||||
public string Identifier { get; }
|
public string Identifier { get; }
|
||||||
public PluginGroup Group { get; }
|
public PluginGroup Group { get; }
|
||||||
@@ -17,32 +17,33 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
public string Author { get; }
|
public string Author { get; }
|
||||||
public string Version { get; }
|
public string Version { get; }
|
||||||
public string Website { get; }
|
public string Website { get; }
|
||||||
public string ConfigFile { get; }
|
|
||||||
public string ConfigDefault { get; }
|
|
||||||
public Version RequiredVersion { get; }
|
public Version RequiredVersion { get; }
|
||||||
|
|
||||||
public bool CanRun { get; }
|
public bool CanRun { get; }
|
||||||
|
|
||||||
public bool HasConfig {
|
internal bool HasConfig {
|
||||||
get => ConfigFile.Length > 0 && GetFullPathIfSafe(PluginFolder.Data, ConfigFile).Length > 0;
|
get => configFile.Length > 0 && GetFullPathIfSafe(PluginFolder.Data, configFile).Length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ConfigPath {
|
internal string ConfigPath {
|
||||||
get => HasConfig ? Path.Combine(GetPluginFolder(PluginFolder.Data), ConfigFile) : string.Empty;
|
get => HasConfig ? Path.Combine(GetPluginFolder(PluginFolder.Data), configFile) : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasDefaultConfig {
|
private bool HasDefaultConfig {
|
||||||
get => ConfigDefault.Length > 0 && GetFullPathIfSafe(PluginFolder.Root, ConfigDefault).Length > 0;
|
get => configDefault.Length > 0 && GetFullPathIfSafe(PluginFolder.Root, configDefault).Length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DefaultConfigPath {
|
private string DefaultConfigPath {
|
||||||
get => HasDefaultConfig ? Path.Combine(GetPluginFolder(PluginFolder.Root), ConfigDefault) : string.Empty;
|
get => HasDefaultConfig ? Path.Combine(GetPluginFolder(PluginFolder.Root), configDefault) : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly string pathRoot;
|
private readonly string pathRoot;
|
||||||
private readonly string pathData;
|
private readonly string pathData;
|
||||||
private readonly ISet<PluginEnvironment> environments;
|
private readonly ISet<PluginEnvironment> environments;
|
||||||
|
|
||||||
|
private readonly string configFile;
|
||||||
|
private readonly string configDefault;
|
||||||
|
|
||||||
private Plugin(PluginGroup group, string identifier, string pathRoot, string pathData, Builder builder) {
|
private Plugin(PluginGroup group, string identifier, string pathRoot, string pathData, Builder builder) {
|
||||||
this.pathRoot = pathRoot;
|
this.pathRoot = pathRoot;
|
||||||
this.pathData = pathData;
|
this.pathData = pathData;
|
||||||
@@ -56,18 +57,18 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
this.Author = builder.Author;
|
this.Author = builder.Author;
|
||||||
this.Version = builder.Version;
|
this.Version = builder.Version;
|
||||||
this.Website = builder.Website;
|
this.Website = builder.Website;
|
||||||
this.ConfigFile = builder.ConfigFile;
|
this.configFile = builder.ConfigFile;
|
||||||
this.ConfigDefault = builder.ConfigDefault;
|
this.configDefault = builder.ConfigDefault;
|
||||||
this.RequiredVersion = builder.RequiredVersion;
|
this.RequiredVersion = builder.RequiredVersion;
|
||||||
|
|
||||||
this.CanRun = AppVersion >= RequiredVersion;
|
this.CanRun = LibVersion >= RequiredVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasEnvironment(PluginEnvironment environment) {
|
internal bool HasEnvironment(PluginEnvironment environment) {
|
||||||
return environments.Contains(environment);
|
return environments.Contains(environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetScriptPath(PluginEnvironment environment) {
|
internal string GetScriptPath(PluginEnvironment environment) {
|
||||||
return environments.Contains(environment) ? Path.Combine(pathRoot, environment.GetPluginScriptFile()) : string.Empty;
|
return environments.Contains(environment) ? Path.Combine(pathRoot, environment.GetPluginScriptFile()) : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetFullPathIfSafe(PluginFolder folder, string relativePath) {
|
internal string GetFullPathIfSafe(PluginFolder folder, string relativePath) {
|
||||||
string rootFolder = GetPluginFolder(folder);
|
string rootFolder = GetPluginFolder(folder);
|
||||||
return FileUtils.ResolveRelativePathSafely(rootFolder, relativePath);
|
return FileUtils.ResolveRelativePathSafely(rootFolder, relativePath);
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
|
|
||||||
// Builder
|
// Builder
|
||||||
|
|
||||||
public sealed class Builder {
|
internal sealed class Builder {
|
||||||
private static readonly Version DefaultRequiredVersion = new (0, 0, 0, 0);
|
private static readonly Version DefaultRequiredVersion = new (0, 0, 0, 0);
|
||||||
|
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
@@ -132,19 +133,19 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
Plugin plugin = new Plugin(group, identifier, pathRoot, pathData, this);
|
Plugin plugin = new Plugin(group, identifier, pathRoot, pathData, this);
|
||||||
|
|
||||||
if (plugin.Name.Length == 0) {
|
if (plugin.Name.Length == 0) {
|
||||||
throw new InvalidOperationException("Plugin is missing a name in the .meta file");
|
throw new InvalidOperationException("Plugin is missing a name in the .meta file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PluginEnvironments.All.Any(plugin.HasEnvironment)) {
|
if (!PluginEnvironments.All.Any(plugin.HasEnvironment)) {
|
||||||
throw new InvalidOperationException("Plugin has no script files");
|
throw new InvalidOperationException("Plugin has no script files.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.Group == PluginGroup.Official) {
|
if (plugin.Group == PluginGroup.Official) {
|
||||||
if (plugin.RequiredVersion != AppVersion) {
|
if (plugin.RequiredVersion != LibVersion) {
|
||||||
throw new InvalidOperationException("Plugin is not supported in this version of TweetDuck, this may indicate a failed update or an unsupported plugin that was not removed automatically");
|
throw new InvalidOperationException("Plugin is not supported in this version of TweetDuck, this may indicate a failed update or an unsupported plugin that was not removed automatically.");
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(plugin.Version)) {
|
else if (!string.IsNullOrEmpty(plugin.Version)) {
|
||||||
throw new InvalidOperationException("Official plugins cannot have a version identifier");
|
throw new InvalidOperationException("Official plugins cannot have a version identifier.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ using TweetLib.Core.Features.Plugins.Enums;
|
|||||||
using TweetLib.Utils.Data;
|
using TweetLib.Utils.Data;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins {
|
namespace TweetLib.Core.Features.Plugins {
|
||||||
internal static class PluginLoader {
|
static class PluginLoader {
|
||||||
private static readonly string[] EndTag = { "[END]" };
|
private static readonly string[] EndTag = { "[END]" };
|
||||||
|
|
||||||
public static IEnumerable<Result<Plugin>> AllInFolder(string pluginFolder, string pluginDataFolder, PluginGroup group) {
|
public static IEnumerable<Result<Plugin>> AllInFolder(string pluginFolder, string pluginDataFolder, PluginGroup group) {
|
||||||
@@ -38,7 +38,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group) {
|
private static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group) {
|
||||||
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
||||||
|
|
||||||
foreach (var environment in Directory.EnumerateFiles(pathRoot, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).Select(EnvironmentFromFileName)) {
|
foreach (var environment in Directory.EnumerateFiles(pathRoot, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).Select(EnvironmentFromFileName)) {
|
||||||
@@ -48,7 +48,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
string metaFile = Path.Combine(pathRoot, ".meta");
|
string metaFile = Path.Combine(pathRoot, ".meta");
|
||||||
|
|
||||||
if (!File.Exists(metaFile)) {
|
if (!File.Exists(metaFile)) {
|
||||||
throw new ArgumentException("Plugin is missing a .meta file");
|
throw new ArgumentException("Plugin is missing a .meta file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string? currentTag = null;
|
string? currentTag = null;
|
||||||
|
@@ -18,15 +18,15 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
public string PluginFolder { get; }
|
public string PluginFolder { get; }
|
||||||
public string PluginDataFolder { get; }
|
public string PluginDataFolder { get; }
|
||||||
|
|
||||||
public event EventHandler<PluginErrorEventArgs>? Reloaded;
|
internal event EventHandler<PluginErrorEventArgs>? Reloaded;
|
||||||
public event EventHandler<PluginErrorEventArgs>? Executed;
|
internal event EventHandler<PluginErrorEventArgs>? Executed;
|
||||||
|
|
||||||
internal readonly PluginBridge bridge;
|
internal readonly PluginBridge bridge;
|
||||||
private IScriptExecutor? browserExecutor;
|
private IScriptExecutor? browserExecutor;
|
||||||
|
|
||||||
private readonly HashSet<Plugin> plugins = new ();
|
private readonly HashSet<Plugin> plugins = new ();
|
||||||
|
|
||||||
public PluginManager(PluginConfig config, string pluginFolder, string pluginDataFolder) {
|
internal PluginManager(PluginConfig config, string pluginFolder, string pluginDataFolder) {
|
||||||
this.Config = config;
|
this.Config = config;
|
||||||
this.Config.PluginChangedState += Config_PluginChangedState;
|
this.Config.PluginChangedState += Config_PluginChangedState;
|
||||||
this.PluginFolder = pluginFolder;
|
this.PluginFolder = pluginFolder;
|
||||||
|
@@ -4,7 +4,6 @@ using System.Net;
|
|||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
using TweetLib.Browser.Request;
|
using TweetLib.Browser.Request;
|
||||||
using TweetLib.Core.Features.Plugins.Enums;
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
using TweetLib.Core.Resources;
|
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins {
|
namespace TweetLib.Core.Features.Plugins {
|
||||||
public sealed class PluginSchemeHandler : ICustomSchemeHandler {
|
public sealed class PluginSchemeHandler : ICustomSchemeHandler {
|
||||||
|
@@ -3,7 +3,7 @@ using TweetLib.Core.Features.Plugins.Config;
|
|||||||
using TweetLib.Core.Features.Plugins.Enums;
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins {
|
namespace TweetLib.Core.Features.Plugins {
|
||||||
internal static class PluginScriptGenerator {
|
static class PluginScriptGenerator {
|
||||||
public static string GenerateConfig(PluginConfig config) {
|
public static string GenerateConfig(PluginConfig config) {
|
||||||
return "window.TD_PLUGINS_DISABLE = [" + string.Join(",", config.DisabledPlugins.Select(static id => '"' + id + '"')) + "]";
|
return "window.TD_PLUGINS_DISABLE = [" + string.Join(",", config.DisabledPlugins.Select(static id => '"' + id + '"')) + "]";
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,8 @@ using TweetLib.Core.Application;
|
|||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features {
|
namespace TweetLib.Core.Features {
|
||||||
public static class PropertyObjectScript {
|
static class PropertyObjectScript {
|
||||||
public enum Environment {
|
internal enum Environment {
|
||||||
Browser,
|
Browser,
|
||||||
Notification
|
Notification
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,7 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
NotificationBrowser.SetNotificationLayout(fontSize, headLayout);
|
NotificationBrowser.SetNotificationLayout(fontSize, headLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisplayTooltip(string text) {
|
public void DisplayTooltip(string? text) {
|
||||||
i.DisplayTooltip(text);
|
i.DisplayTooltip(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
using TweetLib.Browser.Base;
|
using TweetLib.Browser.Base;
|
||||||
using TweetLib.Browser.Contexts;
|
using TweetLib.Browser.Contexts;
|
||||||
using TweetLib.Browser.Events;
|
using TweetLib.Browser.Events;
|
||||||
@@ -16,6 +17,8 @@ using Version = TweetDuck.Version;
|
|||||||
|
|
||||||
namespace TweetLib.Core.Features.TweetDeck {
|
namespace TweetLib.Core.Features.TweetDeck {
|
||||||
public sealed class TweetDeckBrowser : BaseBrowser<TweetDeckBrowser> {
|
public sealed class TweetDeckBrowser : BaseBrowser<TweetDeckBrowser> {
|
||||||
|
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153);
|
||||||
|
|
||||||
private const string NamespaceTweetDeck = "tweetdeck";
|
private const string NamespaceTweetDeck = "tweetdeck";
|
||||||
private const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
|
private const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
|
||||||
|
|
||||||
@@ -244,10 +247,10 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
return new RequestHandleResult.Process(VendorScriptProcessor.Instance);
|
return new RequestHandleResult.Process(VendorScriptProcessor.Instance);
|
||||||
|
|
||||||
case ResourceType.Script when url.Contains("analytics."):
|
case ResourceType.Script when url.Contains("analytics."):
|
||||||
return RequestHandleResult.Cancel.Instance;
|
return RequestHandleResult.Cancel;
|
||||||
|
|
||||||
case ResourceType.Xhr when url.Contains(UrlVersionCheck):
|
case ResourceType.Xhr when url.Contains(UrlVersionCheck):
|
||||||
return RequestHandleResult.Cancel.Instance;
|
return RequestHandleResult.Cancel;
|
||||||
|
|
||||||
case ResourceType.Xhr when url.Contains("://api.twitter.com/") && url.Contains("include_entities=1") && !url.Contains("&include_ext_has_nft_avatar=1"):
|
case ResourceType.Xhr when url.Contains("://api.twitter.com/") && url.Contains("include_entities=1") && !url.Contains("&include_ext_has_nft_avatar=1"):
|
||||||
return new RequestHandleResult.Redirect(url.Replace("include_entities=1", "include_entities=1&include_ext_has_nft_avatar=1"));
|
return new RequestHandleResult.Redirect(url.Replace("include_entities=1", "include_entities=1&include_ext_has_nft_avatar=1"));
|
||||||
|
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
using TweetLib.Browser.Request;
|
using TweetLib.Browser.Request;
|
||||||
using TweetLib.Core.Resources;
|
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.TweetDeck {
|
namespace TweetLib.Core.Features.TweetDeck {
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Twitter {
|
namespace TweetLib.Core.Features.Twitter {
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
public static class TwitterUrls {
|
public static class TwitterUrls {
|
||||||
public const string TweetDeck = "https://tweetdeck.twitter.com";
|
public const string TweetDeck = "https://tweetdeck.twitter.com";
|
||||||
private const string TwitterTrackingUrl = "t.co";
|
private const string TwitterTrackingUrl = "t.co";
|
||||||
@@ -22,6 +24,12 @@ namespace TweetLib.Core.Features.Twitter {
|
|||||||
return url.Contains("://twitter.com/account/login_verification") || url.Contains("://mobile.twitter.com/account/login_verification");
|
return url.Contains("://twitter.com/account/login_verification") || url.Contains("://mobile.twitter.com/account/login_verification");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsAllowedPopupUrl(string url) {
|
||||||
|
return url.StartsWithOrdinal("https://twitter.com/teams/authorize?") ||
|
||||||
|
url.StartsWithOrdinal("https://accounts.google.com/") ||
|
||||||
|
url.StartsWithOrdinal("https://appleid.apple.com/");
|
||||||
|
}
|
||||||
|
|
||||||
public static string? GetFileNameFromUrl(string url) {
|
public static string? GetFileNameFromUrl(string url) {
|
||||||
return StringUtils.NullIfEmpty(Path.GetFileName(new Uri(url).AbsolutePath));
|
return StringUtils.NullIfEmpty(Path.GetFileName(new Uri(url).AbsolutePath));
|
||||||
}
|
}
|
||||||
|
@@ -1,55 +1,49 @@
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using TweetLib.Core;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using TweetDuck;
|
||||||
|
|
||||||
namespace TweetDuck.Resources {
|
namespace TweetLib.Core.Resources {
|
||||||
static class ResourceHotSwap {
|
public static class ResourceHotSwap {
|
||||||
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(App.ProgramPath, "../../../")));
|
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(App.ProgramPath, "../../../../../")));
|
||||||
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
|
private static readonly string HotSwapTargetDir = FixPathSlash(Path.GetFullPath(Path.Combine(App.ProgramPath, "../../../bin/tmp")));
|
||||||
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "Resources", "PostBuild.ps1");
|
|
||||||
|
|
||||||
private static string FixPathSlash(string path) {
|
private static string FixPathSlash(string path) {
|
||||||
return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + '\\';
|
return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + '\\';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Run() {
|
public static void Run() {
|
||||||
if (!File.Exists(HotSwapRebuildScript)) {
|
Debug.WriteLine("Performing resource hot swap...");
|
||||||
Debug.WriteLine($"Failed resource hot swap, missing rebuild script: {HotSwapRebuildScript}");
|
|
||||||
|
string resourcesRoot = Path.Combine(HotSwapProjectRoot, "resources");
|
||||||
|
if (!Directory.Exists(resourcesRoot)) {
|
||||||
|
Debug.WriteLine("Failed resource hot swap, cannot find resources folder: " + resourcesRoot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.WriteLine("Performing resource hot swap...");
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
DeleteHotSwapFolder();
|
DeleteHotSwapFolder();
|
||||||
Directory.CreateDirectory(HotSwapTargetDir);
|
Directory.CreateDirectory(HotSwapTargetDir);
|
||||||
Directory.CreateDirectory(Path.Combine(HotSwapTargetDir, "plugins"));
|
Directory.CreateDirectory(Path.Combine(HotSwapTargetDir, "plugins"));
|
||||||
Directory.CreateDirectory(Path.Combine(HotSwapTargetDir, "plugins", "user"));
|
Directory.CreateDirectory(Path.Combine(HotSwapTargetDir, "plugins", "user"));
|
||||||
CopyDirectory(Path.Combine(HotSwapProjectRoot, "Resources", "Content"), Path.Combine(HotSwapTargetDir, "resources"));
|
CopyDirectory(Path.Combine(resourcesRoot, "Content"), Path.Combine(HotSwapTargetDir, "resources"));
|
||||||
CopyDirectory(Path.Combine(HotSwapProjectRoot, "Resources", "Guide"), Path.Combine(HotSwapTargetDir, "guide"));
|
CopyDirectory(Path.Combine(resourcesRoot, "Guide"), Path.Combine(HotSwapTargetDir, "guide"));
|
||||||
CopyDirectory(Path.Combine(HotSwapProjectRoot, "Resources", "Plugins"), Path.Combine(HotSwapTargetDir, "plugins", "official"));
|
CopyDirectory(Path.Combine(resourcesRoot, "Plugins"), Path.Combine(HotSwapTargetDir, "plugins", "official"));
|
||||||
Directory.Move(Path.Combine(HotSwapTargetDir, "plugins", "official", ".debug"), Path.Combine(HotSwapTargetDir, "plugins", "user", ".debug"));
|
Directory.Move(Path.Combine(HotSwapTargetDir, "plugins", "official", ".debug"), Path.Combine(HotSwapTargetDir, "plugins", "user", ".debug"));
|
||||||
|
|
||||||
Stopwatch sw = Stopwatch.StartNew();
|
foreach (var file in Directory.EnumerateFiles(Path.Combine(HotSwapTargetDir, "plugins"), "*.meta", SearchOption.AllDirectories)) {
|
||||||
|
var lines = File.ReadLines(file, Encoding.UTF8)
|
||||||
using (Process process = Process.Start(new ProcessStartInfo {
|
.Select(static line => line.Replace("{version}", Version.Tag))
|
||||||
FileName = "powershell",
|
.ToArray();
|
||||||
Arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{HotSwapRebuildScript}\" \"{HotSwapTargetDir}\\\" \"{Program.VersionTag}\"",
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden
|
File.WriteAllLines(file, lines, Encoding.UTF8);
|
||||||
})) {
|
|
||||||
// ReSharper disable once PossibleNullReferenceException
|
|
||||||
if (!process.WaitForExit(8000)) {
|
|
||||||
Debug.WriteLine("Failed resource hot swap, script did not finish in time");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (process.ExitCode != 0) {
|
|
||||||
Debug.WriteLine($"Failed resource hot swap, script exited with code {process.ExitCode}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
Debug.WriteLine($"Finished rebuild script in {sw.ElapsedMilliseconds} ms");
|
Debug.WriteLine($"Finished resource hot swap in {sw.ElapsedMilliseconds} ms");
|
||||||
|
|
||||||
Directory.Delete(App.ResourcesPath, true);
|
Directory.Delete(App.ResourcesPath, true);
|
||||||
Directory.Delete(App.GuidePath, true);
|
Directory.Delete(App.GuidePath, true);
|
@@ -4,8 +4,7 @@ using TweetLib.Utils.Serialization;
|
|||||||
|
|
||||||
namespace TweetLib.Core.Systems.Configuration {
|
namespace TweetLib.Core.Systems.Configuration {
|
||||||
sealed class FileConfigInstance<T> : IConfigInstance where T : IConfigObject<T> {
|
sealed class FileConfigInstance<T> : IConfigInstance where T : IConfigObject<T> {
|
||||||
public T Instance { get; }
|
private readonly T instance;
|
||||||
|
|
||||||
private readonly SimpleObjectSerializer<T> serializer;
|
private readonly SimpleObjectSerializer<T> serializer;
|
||||||
|
|
||||||
private readonly string filenameMain;
|
private readonly string filenameMain;
|
||||||
@@ -13,7 +12,7 @@ namespace TweetLib.Core.Systems.Configuration {
|
|||||||
private readonly string identifier;
|
private readonly string identifier;
|
||||||
|
|
||||||
public FileConfigInstance(string filename, T instance, string identifier, TypeConverterRegistry converterRegistry) {
|
public FileConfigInstance(string filename, T instance, string identifier, TypeConverterRegistry converterRegistry) {
|
||||||
this.Instance = instance;
|
this.instance = instance;
|
||||||
this.serializer = new SimpleObjectSerializer<T>(converterRegistry);
|
this.serializer = new SimpleObjectSerializer<T>(converterRegistry);
|
||||||
|
|
||||||
this.filenameMain = filename ?? throw new ArgumentNullException(nameof(filename), "Config file name must not be null!");
|
this.filenameMain = filename ?? throw new ArgumentNullException(nameof(filename), "Config file name must not be null!");
|
||||||
@@ -22,7 +21,7 @@ namespace TweetLib.Core.Systems.Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void LoadInternal(bool backup) {
|
private void LoadInternal(bool backup) {
|
||||||
serializer.Read(backup ? filenameBackup : filenameMain, Instance);
|
serializer.Read(backup ? filenameBackup : filenameMain, instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load() {
|
public void Load() {
|
||||||
@@ -64,7 +63,7 @@ namespace TweetLib.Core.Systems.Configuration {
|
|||||||
File.Move(filenameMain, filenameBackup);
|
File.Move(filenameMain, filenameBackup);
|
||||||
}
|
}
|
||||||
|
|
||||||
serializer.Write(filenameMain, Instance);
|
serializer.Write(filenameMain, instance);
|
||||||
} catch (SerializationSoftException e) {
|
} catch (SerializationSoftException e) {
|
||||||
OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e);
|
OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -77,7 +76,7 @@ namespace TweetLib.Core.Systems.Configuration {
|
|||||||
LoadInternal(false);
|
LoadInternal(false);
|
||||||
} catch (FileNotFoundException) {
|
} catch (FileNotFoundException) {
|
||||||
try {
|
try {
|
||||||
serializer.Write(filenameMain, Instance.ConstructWithDefaults());
|
serializer.Write(filenameMain, instance.ConstructWithDefaults());
|
||||||
LoadInternal(false);
|
LoadInternal(false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
OnException($"Could not regenerate the configuration file for {identifier}.", e);
|
OnException($"Could not regenerate the configuration file for {identifier}.", e);
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
namespace TweetLib.Core.Systems.Dialogs {
|
|
||||||
public static class Dialogs {
|
|
||||||
public const string OK = "OK";
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace TweetLib.Core.Systems.Dialogs {
|
|
||||||
public sealed class FileDialogFilter {
|
|
||||||
public string Name { get; }
|
|
||||||
public IReadOnlyList<string> Extensions { get; }
|
|
||||||
|
|
||||||
public FileDialogFilter(string name, params string[] extensions) {
|
|
||||||
Name = name;
|
|
||||||
Extensions = extensions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace TweetLib.Core.Systems.Dialogs {
|
|
||||||
public sealed class SaveFileDialogSettings {
|
|
||||||
public string DialogTitle { get; internal set; } = "Save File";
|
|
||||||
public bool OverwritePrompt { get; internal set; } = true;
|
|
||||||
public string? FileName { get; internal set; }
|
|
||||||
public IReadOnlyList<FileDialogFilter>? Filters { get; internal set; }
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user