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

Compare commits

...

65 Commits

Author SHA1 Message Date
366320d047 Create an API for picking which screen notifications are displayed on (WIP) 2022-11-21 08:10:04 +01:00
178dca1360 Add a DLL extension API 2022-11-21 08:10:04 +01:00
7a338076db Release 1.25.1 2022-11-21 08:09:55 +01:00
54bf1c2012 Make update installer delete unused files and folders 2022-11-21 08:09:55 +01:00
32681259f6 Re-add VC++ DLLs 2022-11-21 07:59:20 +01:00
1c1aa5ea44 Fix crash when right-clicking tray icon
Closes #350
2022-11-20 20:52:10 +01:00
da54af221c Release 1.25 2022-11-20 14:38:40 +01:00
6c8d518e0d Extract processor architecture to a variable in installer scripts 2022-11-20 14:38:40 +01:00
697f4f1569 Fix notification timer stopping permanently when the screen is locked while a notification is visible 2022-11-20 14:38:40 +01:00
15d4ec3228 Fix subprocess crashing because MSBuild is somehow copying wrong (not Large Address Aware) exe
Closes #342
2022-11-20 13:33:51 +01:00
c303346bc3 Halt reloading after subprocess crash if it happens too often 2022-11-20 08:04:53 +01:00
b9af966849 Update CefSharp to 107 2022-11-20 05:29:23 +01:00
0a7459b72e Update to .NET 7 2022-11-20 05:25:29 +01:00
9953f06ab1 Release 1.24 2022-09-14 02:19:02 +02:00
0c8159aa79 Update CefSharp to 105 2022-09-14 01:55:18 +02:00
c785a7ed8c Update test project dependencies 2022-09-01 01:32:15 +02:00
b1328e5b1f Release 1.23 2022-08-20 13:28:16 +02:00
cb94f0c81e Fix resource freezing not working for vendor.js 2022-08-20 13:26:01 +02:00
8de2989f12 Add icon to browser executable 2022-08-20 13:26:01 +02:00
1cf7d13873 Update installers to Inno Setup 6 & remove references to .NET Framework 2022-08-20 13:26:01 +02:00
35c2ee3673 Work around broken bottom anchor in PluginControl 2022-08-20 13:26:01 +02:00
a1b4c31450 Fix increased size of windows and dialogs from moving to .NET 6 2022-08-20 13:26:01 +02:00
ea95e5cbac Re-add ContextMenu that was removed in .NET Core 3.1 2022-08-20 13:26:00 +02:00
2927097e8e Add TLS 1.3 to default security protocols 2022-08-20 13:26:00 +02:00
b5bffdb95b Update all projects to C# 10 2022-08-20 13:26:00 +02:00
bee894bfbb Fix compiler and IDE warnings for .NET 6 2022-08-20 13:26:00 +02:00
96d2e7cc7c Migrate to .NET 6 & update CefSharp to 102 2022-08-20 13:26:00 +02:00
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
6040337bb4 Release 1.21.2 2022-01-22 14:38:27 +01:00
1ced72388b Add option to hide tweets by users with NFT avatars 2022-01-22 14:27:15 +01:00
4751a948e7 Fix JSDoc issues 2022-01-22 02:00:25 +01:00
3939c2263a Move some options from the General tab to the Advanced tab 2022-01-21 13:22:14 +01:00
b0ba4595ae Remove unnecessary 'internal' keyword on classes 2022-01-21 10:55:50 +01:00
38b1057a4c Fix downloading images from DMs 2022-01-21 10:55:50 +01:00
af5d785ff2 Move IScriptExecutor.RunFunction into an extension method 2022-01-18 15:07:55 +01:00
655d334714 Fix login session export not working across different computers after a Chromium update 2022-01-18 14:35:37 +01:00
583 changed files with 9036 additions and 3339 deletions

5
.gitignore vendored
View File

@@ -10,8 +10,9 @@ bld/*
!bld/Resources
# Rider
.idea/.idea.TweetDuck/.idea/dictionaries
.idea/.idea.TweetDuck/.idea/misc.xml
**/.idea/dictionaries
**/.idea/misc.xml
**/.idea/riderMarkupCache.xml
# User-specific files
*.suo

View File

@@ -1,18 +1,18 @@
<component name="ProjectRunConfigurationManager">
<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="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="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<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_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="Console" />
<option name="PROJECT_TFM" value=".NETFramework,Version=v4.7.2" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0-windows" />
<method v="2">
<option name="Build" />
</method>

View File

@@ -1,108 +0,0 @@
using System;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Browser.Handling;
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 {
internal abstract class CefBrowserComponent : IBrowserComponent {
public bool Ready { get; private set; }
public string Url => browser.Address;
public IFileDownloader FileDownloader => TwitterFileDownloader.Instance;
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 RunFunction(string name, params object[] args) {
browser.BrowserCore.ExecuteScriptAsync(name, args);
}
public void RunScript(string identifier, string script) {
using IFrame frame = browser.GetMainFrame();
frame.ExecuteJavaScriptAsync(script, identifier, 1);
}
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using CefSharp;
namespace TweetDuck.Browser.Adapters {
internal 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 {
internal 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 {
internal 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 {
internal 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 {
internal 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,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() {}
}
}

253
README.md
View File

@@ -1,56 +1,215 @@
# 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)
# 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 Framework 4.7.2 SDK
* .NET SDK
* F# desktop language support
* **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**:
```
PM> Install-Package CefSharp.WinForms -Version 67.0.0
```
In the **Installation details** panel, you can expand the workloads you selected, and uncheck any components that are not listed above to save space. You may uncheck the .NET SDK component if you installed the [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) directly.
### Debug
Building TweetDuck for Linux requires [.NET 6 SDK](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.
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
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).
### 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 6.2.1](https://files.jrsoftware.org/is/6/innosetup-6.2.1.exe)
* [Inno Download Plugin 1.5.1](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!
During installation, the download plugin will ask whether to add its include path to `ISPPBuiltins.iss`. Note that this option does not work with InnoSetup 6, so TweetDuck installers don't need it.
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 6`
You may need to restart Visual Studio or Rider 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. All projects target `.NET 6` and either `C# 10` or `F#`.
Projects are organized into folders:
* Windows projects are in the `windows/` folder
* Linux projects are in the `linux/` folder
* Libraries (`TweetLib.*`) are in the `lib/` folder
* Tests (`TweetTest.*`) are also in the `lib/` folder
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 has a dependency on [CefSharp](https://github.com/cefsharp/CefSharp/).
#### 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.
#### TweetLib.WinForms.Legacy
Windows library that re-adds some legacy Windows Forms components that were removed in .NET Core 3.1. The sources were taken from the [.NET Core 3.0 sources of Windows Forms](https://github.com/dotnet/winforms/tree/v3.0.2), and edited to remove unnecessary features.
### 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**
* This is the main installer that creates entries in the Start Menu & Programs and Features, and an optional desktop icon
* **TweetDuck.Update.exe**
@@ -60,12 +219,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
* 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.
#### 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.
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.
> 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`.
# 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

@@ -1,45 +0,0 @@
$ErrorActionPreference = "Stop"
try{
$mainProj = "..\TweetDuck.csproj"
$browserProj = "..\subprocess\TweetDuck.Browser.csproj"
$cefMatch = Select-String -Path $mainProj '<Import Project="packages\\cef\.redist\.x86\.(.*?)\\'
$cefVersion = $cefMatch.Matches[0].Groups[1].Value
$sharpMatch = Select-String -Path $mainProj '<Import Project="packages\\CefSharp\.Common\.(.*?)\\'
$sharpVersion = $sharpMatch.Matches[0].Groups[1].Value
# Greetings
$title = "CEF ${cefVersion}, CefSharp ${sharpVersion}"
Write-Host ("-" * $title.Length)
Write-Host $title
Write-Host ("-" * $title.Length)
# Perform update
Write-Host "Updating browser subprocess reference..."
$contents = [IO.File]::ReadAllText($browserProj)
$contents = $contents -Replace '(?<=<HintPath>\.\.\\packages\\CefSharp\.Common\.)(.*?)(?=\\)', $sharpVersion
$contents = $contents -Replace '(?<=<Reference Include="CefSharp, Version=)(\d+)', $sharpVersion.Split(".")[0]
$contents = $contents -Replace '(?<=<Reference Include="CefSharp\.BrowserSubprocess\.Core, Version=)(\d+)', $sharpVersion.Split(".")[0]
[IO.File]::WriteAllText($browserProj, $contents)
# Finished
Write-Host ""
Write-Host "Finished. Exiting in 6 seconds..."
Start-Sleep -Seconds 6
}catch{
Write-Host ""
Write-Host "Encountered an error while running PostBuild.ps1 on line" $_.InvocationInfo.ScriptLineNumber
Write-Host $_
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Exit 1
}

View File

@@ -1,543 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\CefSharp.Common.96.0.180\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.96.0.180\build\CefSharp.Common.props')" />
<Import Project="packages\cef.redist.x86.96.0.18\build\cef.redist.x86.props" Condition="Exists('packages\cef.redist.x86.96.0.18\build\cef.redist.x86.props')" />
<Import Project="packages\cef.redist.x64.96.0.18\build\cef.redist.x64.props" Condition="Exists('packages\cef.redist.x64.96.0.18\build\cef.redist.x64.props')" />
<Import Project="packages\CefSharp.WinForms.92.0.260\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.92.0.260\build\CefSharp.WinForms.props')" />
<Import Project="packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectGuid>{2389A7CD-E0D3-4706-8294-092929A33A2D}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TweetDuck</RootNamespace>
<AssemblyName>TweetDuck</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ApplicationIcon>Resources\Images\icon.ico</ApplicationIcon>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<StartArguments>-datafolder TweetDuckDebug</StartArguments>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<OutputPath>bin\x86\Release\</OutputPath>
<Optimize>true</Optimize>
<PlatformTarget>x86</PlatformTarget>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="CefSharp, Version=96.0.180.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=MSIL">
<HintPath>packages\CefSharp.Common.96.0.180\lib\net452\CefSharp.dll</HintPath>
</Reference>
<Reference Include="CefSharp.Core, Version=96.0.180.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=MSIL">
<HintPath>packages\CefSharp.Common.96.0.180\lib\net452\CefSharp.Core.dll</HintPath>
</Reference>
<Reference Include="CefSharp.WinForms, Version=96.0.180.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=MSIL">
<HintPath>packages\CefSharp.WinForms.96.0.180\lib\net462\CefSharp.WinForms.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="lib\TweetLib.Browser\TweetLib.Browser.csproj">
<Project>{eefb1f37-7cad-46bd-8042-66e7b502ab02}</Project>
<Name>TweetLib.Browser</Name>
</ProjectReference>
<ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj">
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
<Name>TweetLib.Core</Name>
</ProjectReference>
<ProjectReference Include="lib\TweetLib.Utils\TweetLib.Utils.csproj">
<Project>{476b1007-b12c-447f-b855-9886048201d6}</Project>
<Name>TweetLib.Utils</Name>
</ProjectReference>
<ProjectReference Include="subprocess\TweetDuck.Browser.csproj">
<Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
<Name>TweetDuck.Browser</Name>
</ProjectReference>
<ProjectReference Include="video\TweetDuck.Video.csproj">
<Project>{278b2d11-402d-44b6-b6a1-8fa67db65565}</Project>
<Name>TweetDuck.Video</Name>
</ProjectReference>
<ProjectReference Include="lib\TweetLib.Communication\TweetLib.Communication.csproj">
<Project>{72473763-4b9d-4fb6-a923-9364b2680f06}</Project>
<Name>TweetLib.Communication</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Application\FileDialogs.cs" />
<Compile Include="Application\MessageDialogs.cs" />
<Compile Include="Application\SystemHandler.cs" />
<Compile Include="Browser\Adapters\CefBrowserComponent.cs" />
<Compile Include="Browser\Adapters\CefContextMenuActionRegistry.cs" />
<Compile Include="Browser\Adapters\CefContextMenuModel.cs" />
<Compile Include="Browser\Adapters\CefResourceHandlerFactory.cs" />
<Compile Include="Browser\Adapters\CefResourceHandlerRegistry.cs" />
<Compile Include="Browser\Adapters\CefResourceRequestHandler.cs" />
<Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
<Compile Include="Browser\Adapters\CefSchemeResourceVisitor.cs" />
<Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
<Compile Include="Browser\Handling\ContextMenuBase.cs" />
<Compile Include="Browser\Handling\ContextMenuBrowser.cs" />
<Compile Include="Browser\Handling\ContextMenuGuide.cs" />
<Compile Include="Browser\Handling\ContextMenuNotification.cs" />
<Compile Include="Browser\Handling\CustomKeyboardHandler.cs" />
<Compile Include="Browser\Handling\CustomLifeSpanHandler.cs" />
<Compile Include="Browser\Handling\DragHandlerBrowser.cs" />
<Compile Include="Browser\Handling\FileDialogHandler.cs" />
<Compile Include="Browser\Handling\JavaScriptDialogHandler.cs" />
<Compile Include="Browser\Handling\RequestHandlerBase.cs" />
<Compile Include="Browser\Handling\RequestHandlerBrowser.cs" />
<Compile Include="Browser\Handling\ResourceHandlerNotification.cs" />
<Compile Include="Browser\Handling\ResponseFilter.cs" />
<Compile Include="Browser\Notification\FormNotificationExample.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Browser\Notification\Screenshot\ScreenshotBridge.cs" />
<Compile Include="Browser\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Browser\Notification\SoundNotification.cs" />
<Compile Include="Browser\TweetDeckBrowser.cs" />
<Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Controls\ControlExtensions.cs" />
<Compile Include="Management\BrowserCache.cs" />
<Compile Include="Management\ClipboardManager.cs" />
<Compile Include="Management\FormManager.cs" />
<Compile Include="Management\LockManager.cs" />
<Compile Include="Management\ProfileManager.cs" />
<Compile Include="Management\VideoPlayer.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reporter.cs" />
<Compile Include="Resources\ResourceHotSwap.cs" />
<Compile Include="Updates\UpdateCheckClient.cs" />
<Compile Include="Updates\UpdateInstaller.cs" />
<Compile Include="Utils\BrowserUtils.cs" />
<Compile Include="Utils\NativeMethods.cs" />
<Compile Include="Utils\TwitterFileDownloader.cs" />
<Compile Include="Utils\WindowsUtils.cs" />
<Compile Include="Version.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controls\FlatButton.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Controls\FlatProgressBar.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Controls\LabelVertical.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Controls\NumericUpDownEx.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Browser\FormBrowser.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Browser\FormBrowser.Designer.cs">
<DependentUpon>FormBrowser.cs</DependentUpon>
</Compile>
<Compile Include="Browser\Notification\FormNotificationMain.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Browser\Notification\FormNotificationMain.Designer.cs">
<DependentUpon>FormNotificationMain.cs</DependentUpon>
</Compile>
<Compile Include="Browser\Notification\FormNotificationBase.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Browser\Notification\FormNotificationBase.Designer.cs">
<DependentUpon>FormNotificationBase.cs</DependentUpon>
</Compile>
<Compile Include="Browser\Notification\FormNotificationTweet.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Browser\Notification\FormNotificationTweet.Designer.cs">
<DependentUpon>FormNotificationTweet.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\FormAbout.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\FormAbout.Designer.cs">
<DependentUpon>FormAbout.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\FormGuide.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\FormGuide.Designer.cs">
<DependentUpon>FormGuide.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\FormMessage.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\FormMessage.Designer.cs">
<DependentUpon>FormMessage.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\FormPlugins.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\FormPlugins.Designer.cs">
<DependentUpon>FormPlugins.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsExternalProgram.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsExternalProgram.Designer.cs">
<DependentUpon>DialogSettingsExternalProgram.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsSearchEngine.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsSearchEngine.Designer.cs">
<DependentUpon>DialogSettingsSearchEngine.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsCSS.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsCSS.Designer.cs">
<DependentUpon>DialogSettingsCSS.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsCefArgs.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsCefArgs.Designer.cs">
<DependentUpon>DialogSettingsCefArgs.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsManage.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsManage.Designer.cs">
<DependentUpon>DialogSettingsManage.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsRestart.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Settings\DialogSettingsRestart.Designer.cs">
<DependentUpon>DialogSettingsRestart.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsFeedback.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsFeedback.Designer.cs">
<DependentUpon>TabSettingsFeedback.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsTray.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsTray.Designer.cs">
<DependentUpon>TabSettingsTray.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsAdvanced.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsAdvanced.Designer.cs">
<DependentUpon>TabSettingsAdvanced.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsGeneral.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsGeneral.Designer.cs">
<DependentUpon>TabSettingsGeneral.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsSounds.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsSounds.Designer.cs">
<DependentUpon>TabSettingsSounds.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsNotifications.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Dialogs\Settings\TabSettingsNotifications.Designer.cs">
<DependentUpon>TabSettingsNotifications.cs</DependentUpon>
</Compile>
<Compile Include="Browser\Notification\Screenshot\FormNotificationScreenshotable.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\FormSettings.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\FormSettings.Designer.cs">
<DependentUpon>FormSettings.cs</DependentUpon>
</Compile>
<Compile Include="Plugins\PluginControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Plugins\PluginControl.Designer.cs">
<DependentUpon>PluginControl.cs</DependentUpon>
</Compile>
<Compile Include="Controls\FlowLayoutPanelNoHScroll.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Updates\FormUpdateDownload.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Updates\FormUpdateDownload.Designer.cs">
<DependentUpon>FormUpdateDownload.cs</DependentUpon>
</Compile>
<Compile Include="Browser\TrayIcon.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Browser\TrayIcon.Designer.cs">
<DependentUpon>TrayIcon.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Browser\FormBrowser.resx">
<DependentUpon>FormBrowser.cs</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<SubType>Designer</SubType>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
<None Include="Resources\Content\.all.js" />
<None Include="Resources\Content\api\bridge.js" />
<None Include="Resources\Content\api\jquery.js" />
<None Include="Resources\Content\api\patch.js" />
<None Include="Resources\Content\api\ready.js" />
<None Include="Resources\Content\api\td.js" />
<None Include="Resources\Content\api\utils.js" />
<None Include="Resources\Content\bootstrap.js" />
<None Include="Resources\Content\error\error.html" />
<None Include="Resources\Content\images\logo.png" />
<None Include="Resources\Content\images\spinner.apng" />
<None Include="Resources\Content\introduction\introduction.css" />
<None Include="Resources\Content\introduction\introduction.js" />
<None Include="Resources\Content\load.js" />
<None Include="Resources\Content\login\hide_cookie_bar.js" />
<None Include="Resources\Content\login\login.css" />
<None Include="Resources\Content\login\redirect_plain_twitter_com.js" />
<None Include="Resources\Content\login\setup_document_attributes.js" />
<None Include="Resources\Content\notification\add_skip_button.js" />
<None Include="Resources\Content\notification\disable_clipboard_formatting.js" />
<None Include="Resources\Content\notification\example\example.html" />
<None Include="Resources\Content\notification\expand_links_or_show_tooltip.js" />
<None Include="Resources\Content\notification\handle_links.js" />
<None Include="Resources\Content\notification\handle_show_this_thread_link.js" />
<None Include="Resources\Content\notification\notification.css" />
<None Include="Resources\Content\notification\recalculate_tweet_sent_time.js" />
<None Include="Resources\Content\notification\reset_scroll_position_on_load.js" />
<None Include="Resources\Content\notification\screenshot\screenshot.js" />
<None Include="Resources\Content\notification\scroll_smoothly.js" />
<None Include="Resources\Content\notification\setup_body_hover_class.js" />
<None Include="Resources\Content\plugins\base.js" />
<None Include="Resources\Content\plugins\notification\plugins.js" />
<None Include="Resources\Content\plugins\setup.js" />
<None Include="Resources\Content\plugins\tweetdeck\plugins.js" />
<None Include="Resources\Content\tweetdeck\add_tweetduck_to_settings_menu.js" />
<None Include="Resources\Content\tweetdeck\bypass_t.co_links.js" />
<None Include="Resources\Content\tweetdeck\clear_search_input.js" />
<None Include="Resources\Content\tweetdeck\configure_first_day_of_week.js" />
<None Include="Resources\Content\tweetdeck\configure_language_for_translations.js" />
<None Include="Resources\Content\tweetdeck\disable_clipboard_formatting.js" />
<None Include="Resources\Content\tweetdeck\disable_td_metrics.js" />
<None Include="Resources\Content\tweetdeck\drag_links_onto_columns.js" />
<None Include="Resources\Content\tweetdeck\expand_links_or_show_tooltip.js" />
<None Include="Resources\Content\tweetdeck\fix_dm_input_box_focus.js" />
<None Include="Resources\Content\tweetdeck\fix_horizontal_scrolling_of_column_container.js" />
<None Include="Resources\Content\tweetdeck\fix_marking_dm_as_read_when_replying.js" />
<None Include="Resources\Content\tweetdeck\fix_media_preview_urls.js" />
<None Include="Resources\Content\tweetdeck\fix_missing_bing_translator_languages.js" />
<None Include="Resources\Content\tweetdeck\fix_os_name.js" />
<None Include="Resources\Content\tweetdeck\fix_scheduled_tweets_not_appearing.js" />
<None Include="Resources\Content\tweetdeck\fix_youtube_previews.js" />
<None Include="Resources\Content\tweetdeck\focus_composer_after_alt_tab.js" />
<None Include="Resources\Content\tweetdeck\focus_composer_after_image_upload.js" />
<None Include="Resources\Content\tweetdeck\focus_composer_after_switching_account.js" />
<None Include="Resources\Content\tweetdeck\globals\apply_rot13.js" />
<None Include="Resources\Content\tweetdeck\globals\get_class_style_property.js" />
<None Include="Resources\Content\tweetdeck\globals\get_column_name.js" />
<None Include="Resources\Content\tweetdeck\globals\get_hovered_column.js" />
<None Include="Resources\Content\tweetdeck\globals\get_hovered_tweet.js" />
<None Include="Resources\Content\tweetdeck\globals\inject_mustache.js" />
<None Include="Resources\Content\tweetdeck\globals\prioritize_newest_event.js" />
<None Include="Resources\Content\tweetdeck\globals\reload_browser.js" />
<None Include="Resources\Content\tweetdeck\globals\reload_columns.js" />
<None Include="Resources\Content\tweetdeck\globals\retrieve_tweet.js" />
<None Include="Resources\Content\tweetdeck\globals\show_tweet_detail.js" />
<None Include="Resources\Content\tweetdeck\handle_extra_mouse_buttons.js" />
<None Include="Resources\Content\tweetdeck\hook_theme_settings.js" />
<None Include="Resources\Content\tweetdeck\inject_css.js" />
<None Include="Resources\Content\tweetdeck\keep_like_follow_dialogs_open.js" />
<None Include="Resources\Content\tweetdeck\limit_loaded_dm_count.js" />
<None Include="Resources\Content\tweetdeck\make_retweets_lowercase.js" />
<None Include="Resources\Content\tweetdeck\middle_click_tweet_icon_actions.js" />
<None Include="Resources\Content\tweetdeck\move_accounts_above_hashtags_in_search.js" />
<None Include="Resources\Content\tweetdeck\offline_notification.js" />
<None Include="Resources\Content\tweetdeck\open_search_externally.js" />
<None Include="Resources\Content\tweetdeck\open_search_in_first_column.js" />
<None Include="Resources\Content\tweetdeck\paste_images_from_clipboard.js" />
<None Include="Resources\Content\tweetdeck\perform_search.js" />
<None Include="Resources\Content\tweetdeck\pin_composer_icon.js" />
<None Include="Resources\Content\tweetdeck\ready_plugins.js" />
<None Include="Resources\Content\tweetdeck\register_composer_active_event.js" />
<None Include="Resources\Content\tweetdeck\register_global_functions.js" />
<None Include="Resources\Content\tweetdeck\register_global_functions_jquery.js" />
<None Include="Resources\Content\tweetdeck\restore_cleared_column.js" />
<None Include="Resources\Content\tweetdeck\screenshot_tweet.js" />
<None Include="Resources\Content\tweetdeck\setup_column_type_attributes.js" />
<None Include="Resources\Content\tweetdeck\setup_desktop_notifications.js" />
<None Include="Resources\Content\tweetdeck\setup_link_context_menu.js" />
<None Include="Resources\Content\tweetdeck\setup_sound_notifications.js" />
<None Include="Resources\Content\tweetdeck\setup_tweet_context_menu.js" />
<None Include="Resources\Content\tweetdeck\setup_tweetduck_account_bamboozle.js" />
<None Include="Resources\Content\tweetdeck\setup_video_player.js" />
<None Include="Resources\Content\tweetdeck\skip_pre_login_page.js" />
<None Include="Resources\Content\tweetdeck\tweetdeck.css" />
<None Include="Resources\Content\update\update.css" />
<None Include="Resources\Content\update\update.js" />
<None Include="Resources\Guide\img\app-menu.png" />
<None Include="Resources\Guide\img\column-clear-header.png" />
<None Include="Resources\Guide\img\column-clear-preferences.png" />
<None Include="Resources\Guide\img\column-preferences.png" />
<None Include="Resources\Guide\img\icon.ico" />
<None Include="Resources\Guide\img\new-tweet-emoji.png" />
<None Include="Resources\Guide\img\new-tweet-pin.png" />
<None Include="Resources\Guide\img\new-tweet-template-advanced.png" />
<None Include="Resources\Guide\img\new-tweet-template-basic.png" />
<None Include="Resources\Guide\img\options-manage-export.png" />
<None Include="Resources\Guide\img\options-manage-reset.png" />
<None Include="Resources\Guide\img\options-manage.png" />
<None Include="Resources\Guide\img\options-notifications-location.png" />
<None Include="Resources\Guide\img\options-notifications-size.png" />
<None Include="Resources\Guide\img\options-sounds.png" />
<None Include="Resources\Guide\img\settings-dropdown.png" />
<None Include="Resources\Guide\img\settings-editdesign.png" />
<None Include="Resources\Guide\index.html" />
<None Include="Resources\Guide\script.js" />
<None Include="Resources\Guide\style.css" />
<None Include="Resources\Images\icon-muted.ico" />
<None Include="Resources\Images\icon-small.ico" />
<None Include="Resources\Images\icon-tray-muted.ico" />
<None Include="Resources\Images\icon-tray-new.ico" />
<None Include="Resources\Images\icon-tray.ico" />
<None Include="Resources\Images\icon.ico" />
<None Include="Resources\Plugins\.debug\.meta" />
<None Include="Resources\Plugins\.debug\browser.js" />
<None Include="Resources\Plugins\.debug\notification.js" />
<None Include="Resources\Plugins\clear-columns\.meta" />
<None Include="Resources\Plugins\clear-columns\browser.js" />
<None Include="Resources\Plugins\edit-design\.meta" />
<None Include="Resources\Plugins\edit-design\browser.js" />
<None Include="Resources\Plugins\edit-design\modal.html" />
<None Include="Resources\Plugins\edit-design\theme.black.css" />
<None Include="Resources\Plugins\emoji-keyboard\.meta" />
<None Include="Resources\Plugins\emoji-keyboard\browser.js" />
<None Include="Resources\Plugins\emoji-keyboard\emoji-instructions.txt" />
<None Include="Resources\Plugins\emoji-keyboard\emoji-ordering.txt" />
<None Include="Resources\Plugins\reply-account\.meta" />
<None Include="Resources\Plugins\reply-account\browser.js" />
<None Include="Resources\Plugins\reply-account\configuration.default.js" />
<None Include="Resources\Plugins\templates\.meta" />
<None Include="Resources\Plugins\templates\browser.js" />
<None Include="Resources\Plugins\templates\modal.html" />
<None Include="Resources\Plugins\timeline-polls\.meta" />
<None Include="Resources\Plugins\timeline-polls\browser.js" />
<None Include="Resources\PostBuild.ps1" />
<None Include="Resources\PostCefUpdate.ps1" />
</ItemGroup>
<ItemGroup>
<ResourcesContent Include="$(ProjectDir)Resources\Content\**\*.*" Visible="false" />
<ResourcesGuide Include="$(ProjectDir)Resources\Guide\**\*.*" Visible="false" />
<ResourcesPlugins Include="$(ProjectDir)Resources\Plugins\**\*.*" Visible="false" />
<ResourcesPlugins Remove="$(ProjectDir)Resources\Plugins\.debug\**\*.*" />
<ResourcesPlugins Remove="$(ProjectDir)Resources\Plugins\emoji-keyboard\emoji-instructions.txt" />
<ResourcesPluginsDebug Include="$(ProjectDir)Resources\Plugins\.debug\**\*.*" Visible="false" />
<Redist Include="$(ProjectDir)bld\Redist\*.*" Visible="false" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>powershell -NoProfile -Command "$ErrorActionPreference = 'SilentlyContinue'; (Get-Process TweetDuck.Browser | Where-Object {$_.Path -eq '$(TargetDir)TweetDuck.Browser.exe'}).Kill(); Exit 0"</PreBuildEvent>
</PropertyGroup>
<Target Name="CopyResources" AfterTargets="Build">
<ItemGroup>
<LocalesToDelete Include="$(TargetDir)locales\*.pak" Exclude="$(TargetDir)locales\en-US.pak" Visible="false" />
</ItemGroup>
<Delete Files="@(LocalesToDelete)" />
<RemoveDir Directories="$(TargetDir)resources" />
<RemoveDir Directories="$(TargetDir)guide" />
<RemoveDir Directories="$(TargetDir)plugins" />
<MakeDir Directories="$(TargetDir)plugins\official" />
<MakeDir Directories="$(TargetDir)plugins\user" />
<Copy SourceFiles="@(ResourcesContent)" DestinationFiles="@(ResourcesContent->'$(TargetDir)\resources\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(ResourcesGuide)" DestinationFiles="@(ResourcesGuide->'$(TargetDir)\guide\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(ResourcesPlugins)" DestinationFiles="@(ResourcesPlugins->'$(TargetDir)\plugins\official\%(RecursiveDir)%(Filename)%(Extension)')" />
<Exec Command="powershell -NoProfile -ExecutionPolicy Unrestricted -File &quot;$(ProjectDir)Resources\PostBuild.ps1&quot; &quot;$(TargetDir)\&quot;" IgnoreExitCode="false" />
</Target>
<Target Name="FinalizeDebug" AfterTargets="CopyResources" Condition="$(ConfigurationName) == Debug">
<Copy SourceFiles="@(ResourcesPluginsDebug)" DestinationFiles="@(ResourcesPluginsDebug->'$(TargetDir)\plugins\user\.debug\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
<Target Name="FinalizeRelease" AfterTargets="CopyResources" Condition="$(ConfigurationName) == Release">
<ItemGroup>
<PdbFiles Include="$(TargetDir)*.pdb" Visible="false" />
<XmlFiles Include="$(TargetDir)*.xml" Visible="false" />
<TxtFiles Include="$(TargetDir)*.txt" Visible="false" />
</ItemGroup>
<Delete Files="$(TargetDir)CefSharp.BrowserSubprocess.exe" />
<Delete Files="$(TargetDir)widevinecdmadapter.dll" />
<Delete Files="@(PdbFiles)" />
<Delete Files="@(XmlFiles)" />
<Delete Files="@(TxtFiles)" />
<Copy SourceFiles="@(Redist)" DestinationFolder="$(TargetDir)" />
<Copy SourceFiles="$(ProjectDir)bld\Resources\LICENSES.txt" DestinationFolder="$(TargetDir)" />
<Exec Command="start &quot;&quot; /B &quot;ISCC.exe&quot; /Q &quot;$(ProjectDir)bld\gen_upd.iss&quot;" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
</Target>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
<Error Condition="!Exists('packages\cef.redist.x64.96.0.18\build\cef.redist.x64.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x64.96.0.18\build\cef.redist.x64.props'))" />
<Error Condition="!Exists('packages\cef.redist.x86.96.0.18\build\cef.redist.x86.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\cef.redist.x86.96.0.18\build\cef.redist.x86.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.96.0.180\build\CefSharp.Common.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.96.0.180\build\CefSharp.Common.props'))" />
<Error Condition="!Exists('packages\CefSharp.Common.96.0.180\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.96.0.180\build\CefSharp.Common.targets'))" />
</Target>
<Import Project="packages\CefSharp.Common.96.0.180\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.96.0.180\build\CefSharp.Common.targets')" />
</Project>

View File

@@ -2,11 +2,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28729.10
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
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
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.WinForms.Legacy", "windows\TweetLib.WinForms.Legacy\TweetLib.WinForms.Legacy.csproj", "{B54E732A-4090-4DAA-9ABD-311368C17B68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Api", "lib\TweetLib.Api\TweetLib.Api.csproj", "{85596C10-F76E-4619-9CC6-6C1593880F83}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
EndProject
@@ -14,10 +20,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetL
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser", "lib\TweetLib.Browser\TweetLib.Browser.csproj", "{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser.CEF", "lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj", "{1B7793C6-9002-483E-9BD7-897FE6CD18FB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Utils", "lib\TweetLib.Utils\TweetLib.Utils.csproj", "{476B1007-B12C-447F-B855-9886048201D6}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Core", "lib\TweetTest.Core\TweetTest.Core.fsproj", "{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Browser.CEF", "lib\TweetTest.Browser.CEF\TweetTest.Browser.CEF.fsproj", "{651B77C2-3745-4DAA-982C-398C2856E038}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Utils", "lib\TweetTest.Utils\TweetTest.Utils.fsproj", "{07F6D350-B16F-44E2-804D-C1142E1E345F}"
EndProject
Global
@@ -38,6 +48,18 @@ Global
{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.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
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.ActiveCfg = Debug|x86
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.Build.0 = Debug|x86
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.ActiveCfg = Release|x86
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.Build.0 = Release|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.ActiveCfg = Debug|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.Build.0 = Debug|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.ActiveCfg = Release|x86
{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.Build.0 = Release|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}.Release|x86.ActiveCfg = Release|x86
@@ -50,6 +72,10 @@ Global
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Debug|x86.Build.0 = Debug|x86
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.ActiveCfg = Release|x86
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.Build.0 = Release|x86
{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Debug|x86.ActiveCfg = Debug|x86
{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Debug|x86.Build.0 = Debug|x86
{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Release|x86.ActiveCfg = Release|x86
{1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Release|x86.Build.0 = Release|x86
{476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.ActiveCfg = Debug|x86
{476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.Build.0 = Debug|x86
{476B1007-B12C-447F-B855-9886048201D6}.Release|x86.ActiveCfg = Release|x86
@@ -58,6 +84,10 @@ Global
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Debug|x86.Build.0 = Debug|x86
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.ActiveCfg = Release|x86
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.Build.0 = Release|x86
{651B77C2-3745-4DAA-982C-398C2856E038}.Debug|x86.ActiveCfg = Debug|x86
{651B77C2-3745-4DAA-982C-398C2856E038}.Debug|x86.Build.0 = Debug|x86
{651B77C2-3745-4DAA-982C-398C2856E038}.Release|x86.ActiveCfg = Release|x86
{651B77C2-3745-4DAA-982C-398C2856E038}.Release|x86.Build.0 = Release|x86
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.ActiveCfg = Debug|x86
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.Build.0 = Debug|x86
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Release|x86.ActiveCfg = Release|x86

View File

@@ -1,41 +0,0 @@
using System;
using System.Net;
using System.Threading.Tasks;
using CefSharp;
using TweetDuck.Management;
using TweetLib.Browser.Interfaces;
using TweetLib.Utils.Static;
using Cookie = CefSharp.Cookie;
namespace TweetDuck.Utils {
sealed class TwitterFileDownloader : IFileDownloader {
public static TwitterFileDownloader Instance { get; } = new TwitterFileDownloader();
private TwitterFileDownloader() {}
public string CacheFolder => BrowserCache.CacheFolder;
public void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onFailure) {
const string authCookieName = "auth_token";
using ICookieManager cookies = Cef.GetGlobalCookieManager();
cookies.VisitUrlCookiesAsync(url, true).ContinueWith(task => {
string cookieStr = null;
if (task.Status == TaskStatus.RanToCompletion) {
Cookie found = task.Result?.Find(cookie => cookie.Name == authCookieName); // the list may be null
if (found != null) {
cookieStr = $"{found.Name}={found.Value}";
}
}
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentChrome);
client.Headers[HttpRequestHeader.Cookie] = cookieStr;
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(path, onSuccess, onFailure);
client.DownloadFileAsync(new Uri(url), path);
});
}
}
}

View File

@@ -6,6 +6,6 @@ using TweetDuck;
namespace TweetDuck {
internal static class Version {
public const string Tag = "1.21.1";
public const string Tag = "1.25.1";
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,16 +1,21 @@
; Script generated by the Inno Script Studio Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
#define MyAppName "TweetDuck"
#define MyAppPublisher "chylex"
#define MyAppURL "https://tweetduck.chylex.com"
#define MyAppShortURL "https://td.chylex.com"
#define MyAppExeName "TweetDuck.exe"
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
#define MyAppArchitecture "x86"
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\TweetDuck.exe")
#include ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") + "\idp.iss"
[Setup]
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
AppId={{{#MyAppID}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
@@ -29,13 +34,11 @@ Uninstallable=TDIsUninstallable
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma2/ultra
LZMADictionarySize=15360
LZMADictionarySize=32768
SolidCompression=yes
InternalCompressLevel=normal
MinVersion=0,6.1
#include <idp.iss>
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -43,8 +46,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
[Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
@@ -68,20 +71,11 @@ AdditionalTasks=Additional shortcuts and components:
var UpdatePath: String;
var VisitedTasksPage: Boolean;
function TDGetNetFrameworkVersion: Cardinal; forward;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
{ Prepare installation variables. }
function InitializeSetup: Boolean;
begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
VisitedTasksPage := False
if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
begin
Result := False
Exit
end;
Result := True
end;
@@ -140,17 +134,3 @@ function TDIsUninstallable: Boolean;
begin
Result := (UpdatePath = '')
end;
{ Return DWORD value containing the build version of .NET Framework. }
function TDGetNetFrameworkVersion: Cardinal;
var FrameworkVersion: Cardinal;
begin
if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', FrameworkVersion) then
begin
Result := FrameworkVersion
Exit
end;
Result := 0
end;

View File

@@ -1,16 +1,21 @@
; Script generated by the Inno Script Studio Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
#define MyAppName "TweetDuck"
#define MyAppPublisher "chylex"
#define MyAppURL "https://tweetduck.chylex.com"
#define MyAppShortURL "https://td.chylex.com"
#define MyAppExeName "TweetDuck.exe"
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
#define MyAppArchitecture "x86"
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\TweetDuck.exe")
#include ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") + "\idp.iss"
[Setup]
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
AppId={{{#MyAppID}}
AppName={#MyAppName} Portable
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
@@ -29,19 +34,17 @@ Uninstallable=no
UsePreviousAppDir=no
PrivilegesRequired=lowest
Compression=lzma2/ultra
LZMADictionarySize=15360
LZMADictionarySize=32768
SolidCompression=yes
InternalCompressLevel=normal
MinVersion=0,6.1
#include <idp.iss>
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
@@ -52,19 +55,10 @@ AdditionalTasks=Additional components:
[Code]
var UpdatePath: String;
function TDGetNetFrameworkVersion: Cardinal; forward;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
{ Prepare installation variables. }
function InitializeSetup: Boolean;
begin
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
begin
Result := False
Exit
end;
Result := True
end;
@@ -102,17 +96,3 @@ begin
end;
end;
end;
{ Return DWORD value containing the build version of .NET Framework. }
function TDGetNetFrameworkVersion: Cardinal;
var FrameworkVersion: Cardinal;
begin
if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', FrameworkVersion) then
begin
Result := FrameworkVersion
Exit
end;
Result := 0
end;

View File

@@ -1,15 +1,19 @@
; Script generated by the Inno Script Studio Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
#define MyAppName "TweetDuck"
#define MyAppPublisher "chylex"
#define MyAppURL "https://tweetduck.chylex.com"
#define MyAppShortURL "https://td.chylex.com"
#define MyAppExeName "TweetDuck.exe"
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
#define CefVersion GetFileVersion("..\bin\x86\Release\libcef.dll")
#define MyAppArchitecture "x86"
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\TweetDuck.exe")
#define CefVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\libcef.dll")
#include ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") + "\idp.iss"
[Setup]
AppId={{{#MyAppID}}
@@ -37,18 +41,17 @@ SolidCompression=True
InternalCompressLevel=normal
MinVersion=0,6.1
#include <idp.iss>
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\bin\x86\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\bin\x86\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\bin\x86\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\TweetImpl.*"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
@@ -68,22 +71,26 @@ Type: filesandordirs; Name: "{localappdata}\{#MyAppName}\GPUCache"
Type: files; Name: "{app}\CEFSHARP-LICENSE.txt"
Type: files; Name: "{app}\LICENSE.txt"
Type: files; Name: "{app}\README.txt"
Type: files; Name: "{app}\natives_blob.bin"
Type: files; Name: "{app}\cef.pak"
Type: files; Name: "{app}\cef_100_percent.pak"
Type: files; Name: "{app}\cef_200_percent.pak"
Type: files; Name: "{app}\cef_extensions.pak"
Type: files; Name: "{app}\devtools_resources.pak"
Type: files; Name: "{app}\natives_blob.bin"
Type: files; Name: "{app}\api-ms-win-*.dll"
Type: files; Name: "{app}\dbgshim.dll"
Type: files; Name: "{app}\mscordaccore_x86_x86_6.*.dll"
Type: files; Name: "{app}\ucrtbase.dll"
Type: filesandordirs; Name: "{app}\guide"
Type: filesandordirs; Name: "{app}\plugins\official"
Type: filesandordirs; Name: "{app}\resources"
Type: filesandordirs; Name: "{app}\scripts"
Type: filesandordirs; Name: "{app}\swiftshader"
[Code]
function TDIsUninstallable: Boolean; forward;
function TDGetRunArgs(Param: String): String; forward;
function TDFindUpdatePath: String; forward;
function TDGetNetFrameworkVersion: Cardinal; forward;
function TDGetAppVersionClean: String; forward;
function TDGetFullDownloadFileName: String; forward;
function TDIsMatchingCEFVersion: Boolean; forward;
@@ -92,7 +99,7 @@ procedure TDExecuteFullDownload; forward;
var IsPortable: Boolean;
var UpdatePath: String;
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. Prepare full download package if required. }
{ Prepare update installation, and the full download package if required. }
function InitializeSetup: Boolean;
begin
IsPortable := ExpandConstant('{param:PORTABLE}') = '1'
@@ -110,12 +117,6 @@ begin
idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe'))
end;
if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
begin
Result := False
Exit
end;
Result := True
end;
@@ -210,20 +211,6 @@ begin
Result := Path
end;
{ Return DWORD value containing the build version of .NET Framework. }
function TDGetNetFrameworkVersion: Cardinal;
var FrameworkVersion: Cardinal;
begin
if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', FrameworkVersion) then
begin
Result := FrameworkVersion
Exit
end;
Result := 0
end;
{ Return the name of the full installer file to download from GitHub. }
function TDGetFullDownloadFileName: String;
begin

7
global.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "7.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}

View File

@@ -0,0 +1,37 @@
namespace TweetLib.Api.Data {
public readonly struct NamespacedResource {
public Resource Namespace { get; }
public Resource Path { get; }
public NamespacedResource(Resource ns, Resource path) {
Namespace = ns;
Path = path;
}
private bool Equals(NamespacedResource other) {
return Namespace.Equals(other.Namespace) && Path.Equals(other.Path);
}
public override bool Equals(object? obj) {
return obj is NamespacedResource other && Equals(other);
}
public static bool operator ==(NamespacedResource left, NamespacedResource right) {
return left.Equals(right);
}
public static bool operator !=(NamespacedResource left, NamespacedResource right) {
return !left.Equals(right);
}
public override int GetHashCode() {
unchecked {
return (Namespace.GetHashCode() * 397) ^ Path.GetHashCode();
}
}
public override string ToString() {
return $"{Namespace}:{Path}";
}
}
}

View File

@@ -0,0 +1,28 @@
namespace TweetLib.Api.Data.Notification {
/// <summary>
/// Allows extensions to decide which screen to use for notifications.
///
/// Every registered provider becomes available in the Options dialog and has to be explicitly selected by the user. Only one provider
/// can be active at any given time.
/// </summary>
public interface IDesktopNotificationScreenProvider {
/// <summary>
/// A unique identifier of this provider. Only needs to be unique within the scope of this plugin.
/// </summary>
Resource Id { get; }
/// <summary>
/// Text displayed in the user interface.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Returns a screen that will be used to display the next desktop notification.
///
/// If the return value is <c>null</c> or a screen that is not present in <see cref="IScreenLayout.AllScreens" />, desktop
/// notifications will be temporarily paused and this method will be called again after an unspecified amount of time (but
/// not sooner than 1 second since the last call).
/// </summary>
IScreen? PickScreen(IScreenLayout layout);
}
}

View File

@@ -0,0 +1,7 @@
namespace TweetLib.Api.Data.Notification {
public interface IScreen {
ScreenBounds Bounds { get; }
string Name { get; }
bool IsPrimary { get; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace TweetLib.Api.Data.Notification {
public interface IScreenLayout {
IScreen PrimaryScreen { get; }
IScreen TweetDuckScreen { get; }
List<IScreen> AllScreens { get; }
}
}

View File

@@ -0,0 +1,18 @@
namespace TweetLib.Api.Data.Notification {
public readonly struct ScreenBounds {
public int X1 { get; }
public int Y1 { get; }
public int Width { get; }
public int Height { get; }
public int X2 => X1 + Width;
public int Y2 => Y1 + Height;
public ScreenBounds(int x1, int y1, int width, int height) {
X1 = x1;
Y1 = y1;
Width = width;
Height = height;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Text.RegularExpressions;
namespace TweetLib.Api.Data {
public readonly struct Resource {
private const string ValidCharacterPattern = "^[a-z0-9_]+$";
private static readonly Regex ValidCharacterRegex = new Regex(ValidCharacterPattern, RegexOptions.Compiled);
public string Name { get; }
public Resource(string name) {
if (!ValidCharacterRegex.IsMatch(name)) {
throw new ArgumentException("Resource name must match the regex: " + ValidCharacterPattern);
}
Name = name;
}
private bool Equals(Resource other) {
return Name == other.Name;
}
public override bool Equals(object? obj) {
return obj is Resource other && Equals(other);
}
public static bool operator ==(Resource left, Resource right) {
return left.Equals(right);
}
public static bool operator !=(Resource left, Resource right) {
return !left.Equals(right);
}
public override int GetHashCode() {
return Name.GetHashCode();
}
public override string ToString() {
return Name;
}
}
}

View File

@@ -0,0 +1,5 @@
namespace TweetLib.Api {
public interface ITweetDuckApi {
T? FindService<T>() where T : class, ITweetDuckService;
}
}

View File

@@ -0,0 +1,3 @@
namespace TweetLib.Api {
public interface ITweetDuckService {}
}

View File

@@ -0,0 +1,7 @@
using TweetLib.Api.Data.Notification;
namespace TweetLib.Api.Service {
public interface INotificationService : ITweetDuckService {
void RegisterDesktopNotificationScreenProvider(IDesktopNotificationScreenProvider provider);
}
}

View File

@@ -0,0 +1,15 @@
using TweetLib.Api.Data;
namespace TweetLib.Api {
public abstract class TweetDuckExtension {
/// <summary>
/// Unique identifier of the extension.
/// </summary>
public abstract Resource Id { get; }
/// <summary>
/// Called when the extension is loaded on startup, or enabled at runtime.
/// </summary>
public abstract void Enable(ITweetDuckApi api);
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Platforms>x86;x64</Platforms>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Version.cs" Link="Version.cs" />
</ItemGroup>
</Project>

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> where T : notnull {
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 var 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);
}
}

Some files were not shown because too many files have changed in this diff Show More