1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-09-14 10:32:10 +02:00

Compare commits

..

30 Commits

Author SHA1 Message Date
b58c8f65fe Release 1.22.1 2022-06-26 11:56:13 +02:00
2c69289785 Disable TweetDeck preview for accounts that have it enabled
Closes #337
2022-06-26 11:56:12 +02:00
dc0fc06673 Add instructions for configuring DevEnvDir MSBuild property when developing with Rider 2022-05-16 19:35:08 +02:00
3114b489b6 Fix README headings 2022-03-31 15:39:43 +02:00
8e5934bd84 Fix possible error when focusing DM input field 2022-03-03 12:40:49 +01:00
a2129b957e Release 1.22.0.1 2022-02-28 07:13:50 +01:00
61cd632df6 Fix crash when the buffer for ResourceHandler is smaller than the resource 2022-02-28 07:10:37 +01:00
712bcd5a6f Release 1.22 2022-02-26 09:05:44 +01:00
dd47201d7b Update CefSharp to 98 2022-02-26 09:04:15 +01:00
2af864f337 Add Linux project (WIP) 2022-02-19 18:19:13 +01:00
acafbc3706 Minor refactoring of the core library 2022-02-15 22:28:52 +01:00
b815ae4b11 Move CEF dialog logic into library projects 2022-02-13 22:14:50 +01:00
45a3a7499f Minor refactoring and code fixes 2022-02-13 22:08:14 +01:00
09fac63ffc Move general CefSharp implementation to a separate library project 2022-02-12 20:12:12 +01:00
dd6776fef4 Pause notifications when Windows is on lock screen 2022-02-12 06:57:22 +01:00
cd02a03e8a Refactor notification pausing code 2022-02-12 06:35:53 +01:00
933e0e54df Address inspections 2022-02-05 23:34:00 +01:00
c4aa62fc3a Fix exception messages 2022-02-05 23:34:00 +01:00
ad30021d6d Fix missing platform declaration in F# project files 2022-02-05 23:34:00 +01:00
7c8b43adfe Move resource hot swap code to core library 2022-02-04 02:53:01 +01:00
3aace0b399 Work on abstracting CEF conventions and logic into a separate library 2022-02-03 04:41:44 +01:00
0a9c84feec Fix potentially not releasing a file lock if an exception is thrown while setting up a download 2022-02-02 19:31:31 +01:00
d5ae698855 Update README 2022-01-30 12:18:58 +01:00
26e6a09d5a Fix PostCefUpdate.ps1 script 2022-01-29 15:10:02 +01:00
57fcff3824 Move main app project into its own folder & assign resource files to core library project 2022-01-29 15:10:02 +01:00
2a7aec199f Fix nullable reference type checking in subprocess and video projects 2022-01-29 12:21:33 +01:00
1b01c38fda Move subprocess and video project into a subfolder 2022-01-29 12:13:12 +01:00
c9fd4634ab Work on abstracting CEF conventions and logic into a separate library 2022-01-27 20:58:32 +01:00
51d2ec92ca Work on abstracting app logic into libraries 2022-01-26 13:39:29 +01:00
12ec8baf5c Update README 2022-01-22 16:01:24 +01:00
482 changed files with 5928 additions and 1709 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@@ -1,125 +0,0 @@
using System;
using System.IO;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Browser.Handling;
using TweetDuck.Management;
using TweetDuck.Utils;
using TweetLib.Browser.Base;
using TweetLib.Browser.Events;
using TweetLib.Browser.Interfaces;
using TweetLib.Utils.Static;
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
namespace TweetDuck.Browser.Adapters {
abstract class CefBrowserComponent : IBrowserComponent {
public bool Ready { get; private set; }
public string Url => browser.Address;
public string CacheFolder => BrowserCache.CacheFolder;
public event EventHandler<BrowserLoadedEventArgs> BrowserLoaded;
public event EventHandler<PageLoadEventArgs> PageLoadStart;
public event EventHandler<PageLoadEventArgs> PageLoadEnd;
private readonly ChromiumWebBrowser browser;
protected CefBrowserComponent(ChromiumWebBrowser browser) {
this.browser = browser;
this.browser.JsDialogHandler = new JavaScriptDialogHandler();
this.browser.LifeSpanHandler = new CustomLifeSpanHandler();
this.browser.LoadingStateChanged += OnLoadingStateChanged;
this.browser.LoadError += OnLoadError;
this.browser.FrameLoadStart += OnFrameLoadStart;
this.browser.FrameLoadEnd += OnFrameLoadEnd;
this.browser.SetupZoomEvents();
}
void IBrowserComponent.Setup(BrowserSetup setup) {
browser.MenuHandler = SetupContextMenu(setup.ContextMenuHandler);
browser.ResourceRequestHandlerFactory = SetupResourceHandlerFactory(setup.ResourceRequestHandler);
}
protected abstract ContextMenuBase SetupContextMenu(IContextMenuHandler handler);
protected abstract CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler);
private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
if (!e.IsLoading) {
Ready = true;
browser.LoadingStateChanged -= OnLoadingStateChanged;
BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser));
BrowserLoaded = null;
}
}
private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs {
private readonly IWebBrowser browser;
public BrowserLoadedEventArgsImpl(IWebBrowser browser) {
this.browser = browser;
}
public override void AddDictionaryWords(params string[] words) {
foreach (string word in words) {
browser.AddWordToDictionary(word);
}
}
}
private void OnLoadError(object sender, LoadErrorEventArgs e) {
if (e.ErrorCode == CefErrorCode.Aborted) {
return;
}
if (!e.FailedUrl.StartsWithOrdinal("td://resources/error/")) {
string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
browser.Load("td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle));
}
}
private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) {
if (e.Frame.IsMain) {
PageLoadStart?.Invoke(this, new PageLoadEventArgs(e.Url));
}
}
private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
if (e.Frame.IsMain) {
PageLoadEnd?.Invoke(this, new PageLoadEventArgs(e.Url));
}
}
public void AttachBridgeObject(string name, object bridge) {
browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
browser.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder);
}
public void RunScript(string identifier, string script) {
using IFrame frame = browser.GetMainFrame();
frame.ExecuteJavaScriptAsync(script, identifier, 1);
}
public void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onError) {
Cef.UIThreadTaskFactory.StartNew(() => {
try {
using IFrame frame = browser.GetMainFrame();
var request = frame.CreateRequest(false);
request.Method = "GET";
request.Url = url;
request.Flags = UrlRequestFlags.AllowStoredCredentials;
request.SetReferrer(Url, ReferrerPolicy.Default);
var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
var client = new DownloadRequestClient(fileStream, onSuccess, onError);
frame.CreateUrlRequest(request, client);
} catch (Exception e) {
onError?.Invoke(e);
}
});
}
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using CefSharp;
namespace TweetDuck.Browser.Adapters {
sealed class CefContextMenuActionRegistry {
private readonly Dictionary<CefMenuCommand, Action> actions = new Dictionary<CefMenuCommand, Action>();
public CefMenuCommand AddAction(Action action) {
CefMenuCommand id = CefMenuCommand.UserFirst + 500 + actions.Count;
actions[id] = action;
return id;
}
public bool Execute(CefMenuCommand id) {
if (actions.TryGetValue(id, out var action)) {
action();
return true;
}
return false;
}
public void Clear() {
actions.Clear();
}
}
}

View File

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

View File

@@ -1,23 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using CefSharp;
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
namespace TweetDuck.Browser.Adapters {
sealed class CefResourceHandlerFactory : IResourceRequestHandlerFactory {
bool IResourceRequestHandlerFactory.HasHandlers => registry != null;
private readonly CefResourceRequestHandler resourceRequestHandler;
private readonly CefResourceHandlerRegistry registry;
public CefResourceHandlerFactory(IResourceRequestHandler resourceRequestHandler, CefResourceHandlerRegistry registry) {
this.resourceRequestHandler = new CefResourceRequestHandler(registry, resourceRequestHandler);
this.registry = registry;
}
[SuppressMessage("ReSharper", "RedundantAssignment")]
CefSharp.IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) {
disableDefaultHandling = registry != null && registry.HasHandler(request.Url);
return resourceRequestHandler;
}
}
}

View File

@@ -1,42 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Text;
using CefSharp;
namespace TweetDuck.Browser.Adapters {
sealed class CefResourceHandlerRegistry {
private readonly ConcurrentDictionary<string, Func<IResourceHandler>> resourceHandlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase);
public bool HasHandler(string url) {
return resourceHandlers.ContainsKey(url);
}
public IResourceHandler GetHandler(string url) {
return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null;
}
private void Register(string url, Func<IResourceHandler> factory) {
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
throw new ArgumentException("Resource handler URL must be absolute!");
}
resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory);
}
public void RegisterStatic(string url, byte[] staticData, string mimeType = ResourceHandler.DefaultMimeType) {
Register(url, () => ResourceHandler.FromByteArray(staticData, mimeType));
}
public void RegisterStatic(string url, string staticData, string mimeType = ResourceHandler.DefaultMimeType) {
Register(url, () => ResourceHandler.FromString(staticData, Encoding.UTF8, mimeType: mimeType));
}
public void RegisterDynamic(string url, IResourceHandler handler) {
Register(url, () => handler);
}
public void Unregister(string url) {
resourceHandlers.TryRemove(url, out _);
}
}
}

View File

@@ -1,77 +0,0 @@
using System.Collections.Generic;
using CefSharp;
using CefSharp.Handler;
using TweetDuck.Browser.Handling;
using TweetLib.Browser.Interfaces;
using TweetLib.Browser.Request;
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
using ResourceType = TweetLib.Browser.Request.ResourceType;
namespace TweetDuck.Browser.Adapters {
sealed class CefResourceRequestHandler : ResourceRequestHandler {
private readonly CefResourceHandlerRegistry resourceHandlerRegistry;
private readonly IResourceRequestHandler resourceRequestHandler;
private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new Dictionary<ulong, IResponseProcessor>();
public CefResourceRequestHandler(CefResourceHandlerRegistry resourceHandlerRegistry, IResourceRequestHandler resourceRequestHandler) {
this.resourceHandlerRegistry = resourceHandlerRegistry;
this.resourceRequestHandler = resourceRequestHandler;
}
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
if (request.ResourceType == CefSharp.ResourceType.CspReport) {
callback.Dispose();
return CefReturnValue.Cancel;
}
if (resourceRequestHandler != null) {
var result = resourceRequestHandler.Handle(request.Url, TranslateResourceType(request.ResourceType));
switch (result) {
case RequestHandleResult.Redirect redirect:
request.Url = redirect.Url;
break;
case RequestHandleResult.Process process:
request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true);
responseProcessors[request.Identifier] = process.Processor;
break;
case RequestHandleResult.Cancel _:
callback.Dispose();
return CefReturnValue.Cancel;
}
}
return base.OnBeforeResourceLoad(chromiumWebBrowser, browser, frame, request, callback);
}
protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) {
return resourceHandlerRegistry?.GetHandler(request.Url);
}
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
if (responseProcessors.TryGetValue(request.Identifier, out var processor) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)) {
return new ResponseFilter(processor, totalBytes);
}
return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
}
protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) {
responseProcessors.Remove(request.Identifier);
base.OnResourceLoadComplete(chromiumWebBrowser, browser, frame, request, response, status, receivedContentLength);
}
private static ResourceType TranslateResourceType(CefSharp.ResourceType resourceType) {
return resourceType switch {
CefSharp.ResourceType.MainFrame => ResourceType.MainFrame,
CefSharp.ResourceType.Script => ResourceType.Script,
CefSharp.ResourceType.Stylesheet => ResourceType.Stylesheet,
CefSharp.ResourceType.Xhr => ResourceType.Xhr,
CefSharp.ResourceType.Image => ResourceType.Image,
_ => ResourceType.Unknown
};
}
}
}

View File

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

View File

@@ -1,38 +0,0 @@
using System;
using System.IO;
using System.Net;
using CefSharp;
using TweetLib.Browser.Interfaces;
using TweetLib.Browser.Request;
namespace TweetDuck.Browser.Adapters {
sealed class CefSchemeResourceVisitor : ISchemeResourceVisitor<IResourceHandler> {
public static CefSchemeResourceVisitor Instance { get; } = new CefSchemeResourceVisitor();
private static readonly SchemeResource.Status FileIsEmpty = new SchemeResource.Status(HttpStatusCode.NoContent, "File is empty.");
private CefSchemeResourceVisitor() {}
public IResourceHandler Status(SchemeResource.Status status) {
var handler = CreateHandler(Array.Empty<byte>());
handler.StatusCode = (int) status.Code;
handler.StatusText = status.Message;
return handler;
}
public IResourceHandler File(SchemeResource.File file) {
byte[] contents = file.Contents;
if (contents.Length == 0) {
return Status(FileIsEmpty); // FromByteArray crashes CEF internals with no contents
}
var handler = CreateHandler(contents);
handler.MimeType = Cef.GetMimeType(file.Extension);
return handler;
}
private static ResourceHandler CreateHandler(byte[] bytes) {
return ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
}
}
}

View File

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

View File

@@ -1,13 +0,0 @@
using CefSharp;
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
namespace TweetDuck.Browser.Handling {
sealed class ContextMenuGuide : ContextMenuBase {
public ContextMenuGuide(IContextMenuHandler handler) : base(handler) {}
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
AddDebugMenuItems(model);
}
}
}

View File

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

View File

@@ -1,62 +0,0 @@
using System;
using System.IO;
using CefSharp;
namespace TweetDuck.Browser.Handling {
sealed class DownloadRequestClient : UrlRequestClient {
private readonly FileStream fileStream;
private readonly Action onSuccess;
private readonly Action<Exception> onError;
private bool hasFailed;
public DownloadRequestClient(FileStream fileStream, Action onSuccess, Action<Exception> onError) {
this.fileStream = fileStream;
this.onSuccess = onSuccess;
this.onError = onError;
}
protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) {
onError?.Invoke(new Exception("This URL requires authentication."));
fileStream.Dispose();
hasFailed = true;
return false;
}
protected override void OnDownloadData(IUrlRequest request, Stream data) {
if (hasFailed) {
return;
}
try {
data.CopyTo(fileStream);
} catch (Exception e) {
fileStream.Dispose();
onError?.Invoke(e);
hasFailed = true;
}
}
protected override void OnRequestComplete(IUrlRequest request) {
if (hasFailed) {
return;
}
bool isEmpty = fileStream.Position == 0;
fileStream.Dispose();
var status = request.RequestStatus;
if (status == UrlRequestStatus.Failed) {
onError?.Invoke(new Exception("Unknown error."));
}
else if (status == UrlRequestStatus.Success) {
if (isEmpty) {
onError?.Invoke(new Exception("File is empty."));
return;
}
onSuccess?.Invoke();
}
}
}
}

View File

@@ -1,35 +0,0 @@
using System.Collections.Generic;
using CefSharp;
using CefSharp.Enums;
namespace TweetDuck.Browser.Handling {
sealed class DragHandlerBrowser : IDragHandler {
private readonly RequestHandlerBrowser requestHandler;
public DragHandlerBrowser(RequestHandlerBrowser requestHandler) {
this.requestHandler = requestHandler;
}
public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask) {
void TriggerDragStart(string type, string data = null) {
browserControl.BrowserCore.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", type, data);
}
requestHandler.BlockNextUserNavUrl = dragData.LinkUrl; // empty if not a link
if (dragData.IsLink) {
TriggerDragStart("link", dragData.LinkUrl);
}
else if (dragData.IsFragment) {
TriggerDragStart("text", dragData.FragmentText.Trim());
}
else {
TriggerDragStart("unknown");
}
return false;
}
public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {}
}
}

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
using CefSharp;
using CefSharp.Handler;
namespace TweetDuck.Browser.Handling {
class RequestHandlerBase : RequestHandler {
private readonly bool autoReload;
public RequestHandlerBase(bool autoReload) {
this.autoReload = autoReload;
}
protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) {
return CustomLifeSpanHandler.HandleLinkClick(targetDisposition, targetUrl);
}
protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) {
if (autoReload) {
browser.Reload();
}
}
}
}

View File

@@ -1,23 +0,0 @@
using CefSharp;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Handling {
sealed class RequestHandlerBrowser : RequestHandlerBase {
public string BlockNextUserNavUrl { get; set; }
public RequestHandlerBrowser() : base(true) {}
protected override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) {
if (userGesture && request.TransitionType == TransitionType.LinkClicked) {
bool block = request.Url == BlockNextUserNavUrl;
BlockNextUserNavUrl = string.Empty;
return block;
}
else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)) {
return true;
}
return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
}
}
}

View File

@@ -1,79 +0,0 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Text;
using CefSharp;
using CefSharp.Callback;
namespace TweetDuck.Browser.Handling {
sealed class ResourceHandlerNotification : IResourceHandler {
private readonly NameValueCollection headers = new NameValueCollection(0);
private MemoryStream dataIn;
public void SetHTML(string html) {
dataIn?.Dispose();
dataIn = ResourceHandler.GetMemoryStream(html, Encoding.UTF8);
}
public void Dispose() {
if (dataIn != null) {
dataIn.Dispose();
dataIn = null;
}
}
bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) {
callback.Dispose();
handleRequest = true;
if (dataIn != null) {
dataIn.Position = 0;
}
return true;
}
void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) {
redirectUrl = null;
response.MimeType = "text/html";
response.StatusCode = 200;
response.StatusText = "OK";
response.Headers = headers;
responseLength = dataIn?.Length ?? 0;
}
bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) {
callback?.Dispose(); // TODO unnecessary null check once ReadResponse is removed
try {
byte[] buffer = new byte[Math.Min(dataIn.Length - dataIn.Position, dataOut.Length)];
int length = buffer.Length;
dataIn.Read(buffer, 0, length);
dataOut.Write(buffer, 0, length);
bytesRead = length;
} catch { // catch IOException, possibly NullReferenceException if dataIn is null
bytesRead = 0;
}
return bytesRead > 0;
}
bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) {
bytesSkipped = -2; // ERR_FAILED
callback.Dispose();
return false;
}
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) {
return ((IResourceHandler) this).Open(request, out bool _, callback);
}
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) {
return ((IResourceHandler) this).Read(dataOut, out bytesRead, null);
}
void IResourceHandler.Cancel() {}
}
}

249
README.md
View File

@@ -1,56 +1,211 @@
# Support
[Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) &nbsp;|&nbsp; [Support via Ko-fi](https://ko-fi.com/chylex) &nbsp;|&nbsp; [Support via Patreon](https://www.patreon.com/chylex) [Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) &nbsp;|&nbsp; [Support via Ko-fi](https://ko-fi.com/chylex) &nbsp;|&nbsp; [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.

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,26 @@
using System.Net;
using System.Text;
namespace TweetLib.Browser.CEF.Data {
public sealed class ByteArrayResource {
private const string DefaultMimeType = "text/html";
private const HttpStatusCode DefaultStatusCode = HttpStatusCode.OK;
private const string DefaultStatusText = "OK";
internal byte[] Contents { get; }
internal int Length { get; }
internal string MimeType { get; }
internal HttpStatusCode StatusCode { get; }
internal string StatusText { get; }
public ByteArrayResource(byte[] contents, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) {
this.Contents = contents;
this.Length = contents.Length;
this.MimeType = mimeType;
this.StatusCode = statusCode;
this.StatusText = statusText;
}
public ByteArrayResource(string contents, Encoding encoding, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) : this(encoding.GetBytes(contents), mimeType, statusCode, statusText) {}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace TweetLib.Browser.CEF.Data {
abstract class ContextMenuActionRegistry<T> {
private readonly Dictionary<T, Action> actions = new ();
protected abstract T NextId(int n);
public T AddAction(Action action) {
T id = NextId(actions.Count);
actions[id] = action;
return id;
}
public bool Execute(T id) {
if (actions.TryGetValue(id, out var action)) {
action();
return true;
}
return false;
}
public void Clear() {
actions.Clear();
}
}
}

View File

@@ -0,0 +1,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);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Concurrent;
using System.Text;
using TweetLib.Browser.CEF.Interfaces;
namespace TweetLib.Browser.CEF.Data {
public sealed class ResourceHandlerRegistry<TResourceHandler> where TResourceHandler : class {
private readonly IResourceHandlerFactory<TResourceHandler> factory;
private readonly ConcurrentDictionary<string, Func<TResourceHandler>> resourceHandlers = new (StringComparer.OrdinalIgnoreCase);
public ResourceHandlerRegistry(IResourceHandlerFactory<TResourceHandler> factory) {
this.factory = factory;
}
internal bool HasHandler(string url) {
return resourceHandlers.ContainsKey(url);
}
internal TResourceHandler? GetHandler(string url) {
return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null;
}
private void Register(string url, Func<TResourceHandler> factory) {
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
throw new ArgumentException("Resource handler URL must be absolute!");
}
resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (_, _) => factory);
}
public void RegisterStatic(string url, byte[] staticData, string mimeType = "text/html") {
Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, mimeType)));
}
public void RegisterStatic(string url, string staticData, string mimeType = "text/html") {
Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, Encoding.UTF8, mimeType)));
}
public void RegisterDynamic(string url, TResourceHandler handler) {
Register(url, () => handler);
}
public void Unregister(string url) {
resourceHandlers.TryRemove(url, out _);
}
}
}

View File

@@ -0,0 +1,7 @@
namespace TweetLib.Browser.CEF.Dialogs {
public enum FileDialogType {
Open,
OpenMultiple,
Other
}
}

View File

@@ -0,0 +1,8 @@
namespace TweetLib.Browser.CEF.Dialogs {
public enum JsDialogType {
Alert,
Confirm,
Prompt,
Unknown
}
}

View File

@@ -0,0 +1,9 @@
namespace TweetLib.Browser.CEF.Dialogs {
public enum MessageDialogType {
None,
Question,
Information,
Warning,
Error
}
}

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

View File

@@ -0,0 +1,7 @@
using System;
namespace TweetLib.Browser.CEF.Interfaces {
public interface ICefAdapter {
void RunOnUiThread(Action action);
}
}

View File

@@ -0,0 +1,9 @@
namespace TweetLib.Browser.CEF.Interfaces {
public interface IDragDataAdapter<T> {
bool IsLink(T data);
string GetLink(T data);
bool IsFragment(T data);
string GetFragmentAsText(T data);
}
}

View File

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

View File

@@ -0,0 +1,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);
}
}

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

View File

@@ -0,0 +1,8 @@
namespace TweetLib.Browser.CEF.Interfaces {
public interface IFrameAdapter<T> {
bool IsValid(T frame);
bool IsMain(T frame);
void LoadUrl(T frame, string url);
void ExecuteJavaScriptAsync(T frame, string script, string identifier, int startLine = 1);
}
}

View File

@@ -0,0 +1,6 @@
namespace TweetLib.Browser.CEF.Interfaces {
public interface IJsDialogCallbackAdapter<T> {
void Continue(T callback, bool success, string? userInput = null);
void Dispose(T callback);
}
}

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

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

View File

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

View File

@@ -0,0 +1,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);
}
}

View File

@@ -0,0 +1,8 @@
using TweetLib.Browser.CEF.Data;
namespace TweetLib.Browser.CEF.Interfaces {
public interface IResourceHandlerFactory<T> {
T CreateResourceHandler(ByteArrayResource resource);
string GetMimeTypeFromExtension(string extension);
}
}

View File

@@ -0,0 +1,9 @@
namespace TweetLib.Browser.CEF.Interfaces {
public interface IResponseAdapter<T> {
void SetCharset(T response, string charset);
void SetMimeType(T response, string mimeType);
void SetStatus(T response, int statusCode, string statusText);
void SetHeader(T response, string header, string value);
string? GetHeader(T response, string header);
}
}

View File

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

View File

@@ -0,0 +1,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;
}
}
}

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

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

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

View File

@@ -0,0 +1,37 @@
using TweetLib.Browser.CEF.Interfaces;
using TweetLib.Browser.Interfaces;
namespace TweetLib.Browser.CEF.Logic {
public sealed class DragHandlerLogic<TDragData, TRequest> {
private readonly IScriptExecutor executor;
private readonly RequestHandlerLogic<TRequest> requestHandlerLogic;
private readonly IDragDataAdapter<TDragData> dragDataAdapter;
public DragHandlerLogic(IScriptExecutor executor, RequestHandlerLogic<TRequest> requestHandlerLogic, IDragDataAdapter<TDragData> dragDataAdapter) {
this.executor = executor;
this.requestHandlerLogic = requestHandlerLogic;
this.dragDataAdapter = dragDataAdapter;
}
private void TriggerDragStart(string type, string? data = null) {
executor.RunFunction("window.TDGF_onGlobalDragStart && window.TDGF_onGlobalDragStart", type, data);
}
public bool OnDragEnter(TDragData dragData) {
var link = dragDataAdapter.GetLink(dragData);
requestHandlerLogic.BlockNextUserNavUrl = link;
if (dragDataAdapter.IsLink(dragData)) {
TriggerDragStart("link", link);
}
else if (dragDataAdapter.IsFragment(dragData)) {
TriggerDragStart("text", dragDataAdapter.GetFragmentAsText(dragData).Trim());
}
else {
TriggerDragStart("unknown");
}
return false;
}
}
}

View File

@@ -0,0 +1,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;
}
}
}

View File

@@ -0,0 +1,37 @@
using TweetLib.Browser.CEF.Interfaces;
namespace TweetLib.Browser.CEF.Logic {
public sealed class LifeSpanHandlerLogic {
public enum TargetDisposition {
NewBackgroundTab,
NewForegroundTab,
NewPopup,
NewWindow,
Other
}
private readonly IPopupHandler popupHandler;
public LifeSpanHandlerLogic(IPopupHandler popupHandler) {
this.popupHandler = popupHandler;
}
public bool OnBeforePopup(string targetUrl, TargetDisposition targetDisposition) {
switch (targetDisposition) {
case TargetDisposition.NewBackgroundTab:
case TargetDisposition.NewForegroundTab:
case TargetDisposition.NewPopup when !popupHandler.IsPopupAllowed(targetUrl):
case TargetDisposition.NewWindow:
popupHandler.OpenExternalBrowser(targetUrl);
return true;
default:
return false;
}
}
public bool DoClose() {
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
using TweetLib.Browser.CEF.Interfaces;
namespace TweetLib.Browser.CEF.Logic {
public sealed class RequestHandlerLogic<TRequest> {
internal string BlockNextUserNavUrl { get; set; } = string.Empty;
private readonly IRequestAdapter<TRequest> requestAdapter;
private readonly LifeSpanHandlerLogic lifeSpanHandlerLogic;
public RequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, LifeSpanHandlerLogic lifeSpanHandlerLogic) {
this.requestAdapter = requestAdapter;
this.lifeSpanHandlerLogic = lifeSpanHandlerLogic;
}
private bool ShouldBlockNav(string url) {
bool block = url == BlockNextUserNavUrl;
BlockNextUserNavUrl = string.Empty;
return block;
}
public bool OnBeforeBrowse(TRequest request, bool userGesture) {
return requestAdapter.IsTransitionForwardBack(request) || (userGesture && ShouldBlockNav(requestAdapter.GetUrl(request)));
}
public bool OnOpenUrlFromTab(string targetUrl, bool userGesture, LifeSpanHandlerLogic.TargetDisposition targetDisposition) {
return (userGesture && ShouldBlockNav(targetUrl)) || lifeSpanHandlerLogic.OnBeforePopup(targetUrl, targetDisposition);
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using TweetLib.Browser.CEF.Data;
using TweetLib.Browser.CEF.Interfaces;
namespace TweetLib.Browser.CEF.Logic {
public sealed class ResourceRequestHandlerFactoryLogic<TResourceRequestHandler, TResourceHandler, TRequest> where TResourceHandler : class {
private readonly IRequestAdapter<TRequest> requestAdapter;
private readonly TResourceRequestHandler handler;
private readonly ResourceHandlerRegistry<TResourceHandler> registry;
public ResourceRequestHandlerFactoryLogic(IRequestAdapter<TRequest> requestAdapter, TResourceRequestHandler handler, ResourceHandlerRegistry<TResourceHandler> registry) {
this.handler = handler;
this.registry = registry;
this.requestAdapter = requestAdapter;
}
[SuppressMessage("ReSharper", "RedundantAssignment")]
public TResourceRequestHandler GetResourceRequestHandler(TRequest request, ref bool disableDefaultHandling) {
disableDefaultHandling = registry.HasHandler(requestAdapter.GetUrl(request));
return handler;
}
}
}

View File

@@ -0,0 +1,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));
}
}
}

View File

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

View File

@@ -0,0 +1,21 @@
using System;
using TweetLib.Browser.CEF.Interfaces;
using TweetLib.Browser.Interfaces;
namespace TweetLib.Browser.CEF.Logic {
public sealed class SchemeHandlerFactoryLogic<TRequest, TResourceHandler> where TResourceHandler : class {
private readonly ICustomSchemeHandler handler;
private readonly IRequestAdapter<TRequest> requestAdapter;
private readonly ISchemeResourceVisitor<TResourceHandler> resourceVisitor;
public SchemeHandlerFactoryLogic(ICustomSchemeHandler handler, IRequestAdapter<TRequest> requestAdapter, IResourceHandlerFactory<TResourceHandler> resourceHandlerFactory) {
this.handler = handler;
this.requestAdapter = requestAdapter;
this.resourceVisitor = new SchemeResourceVisitor<TResourceHandler>(resourceHandlerFactory);
}
public TResourceHandler? Create(TRequest request) {
return Uri.TryCreate(requestAdapter.GetUrl(request), UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(resourceVisitor) : null;
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Net;
using TweetLib.Browser.CEF.Data;
using TweetLib.Browser.CEF.Interfaces;
using TweetLib.Browser.Interfaces;
using TweetLib.Browser.Request;
namespace TweetLib.Browser.CEF.Logic {
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)));
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 + '"')) + "]";
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
.Select(static line => line.Replace("{version}", Version.Tag))
.ToArray();
using (Process process = Process.Start(new ProcessStartInfo { File.WriteAllLines(file, lines, Encoding.UTF8);
FileName = "powershell",
Arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{HotSwapRebuildScript}\" \"{HotSwapTargetDir}\\\" \"{Program.VersionTag}\"",
WindowStyle = ProcessWindowStyle.Hidden
})) {
// 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);

View File

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

View File

@@ -1,5 +0,0 @@
namespace TweetLib.Core.Systems.Dialogs {
public static class Dialogs {
public const string OK = "OK";
}
}

View File

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

View File

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