mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 19:32:10 +02:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
b58c8f65fe
|
|||
2c69289785
|
|||
dc0fc06673
|
|||
3114b489b6
|
|||
8e5934bd84
|
|||
a2129b957e
|
|||
61cd632df6
|
|||
712bcd5a6f
|
|||
dd47201d7b
|
|||
2af864f337
|
|||
acafbc3706
|
|||
b815ae4b11
|
|||
45a3a7499f
|
|||
09fac63ffc
|
|||
dd6776fef4
|
|||
cd02a03e8a
|
|||
933e0e54df
|
|||
c4aa62fc3a
|
|||
ad30021d6d
|
|||
7c8b43adfe
|
|||
3aace0b399
|
|||
0a9c84feec
|
|||
d5ae698855
|
|||
26e6a09d5a
|
|||
57fcff3824
|
|||
2a7aec199f
|
|||
1b01c38fda
|
|||
c9fd4634ab
|
|||
51d2ec92ca
|
|||
12ec8baf5c
|
|||
6040337bb4
|
|||
1ced72388b
|
|||
4751a948e7
|
|||
3939c2263a
|
|||
b0ba4595ae
|
|||
38b1057a4c
|
|||
af5d785ff2
|
|||
655d334714
|
|||
eee72959e6
|
|||
89b8977f7d
|
|||
9ede2e1ccc
|
|||
03d1bc0f4c
|
|||
cde9f66111
|
|||
8149ed50e1
|
|||
24f5075116
|
|||
2a636245b4
|
|||
3f4844f6f6
|
|||
29308de3ee
|
|||
0d3d744d94
|
|||
d38e525fed
|
|||
e2ac38ed0b
|
|||
fa534f9eb3
|
|||
ec7827df24
|
|||
b915488651
|
|||
bf9a0226be
|
|||
68582f6973
|
|||
03f3d4d450
|
|||
115428ec50
|
|||
5f60852fbb
|
|||
a7a5723c4b
|
|||
17e42df42d
|
|||
7e692460d8
|
|||
f41a5946e4
|
|||
29fee155d7
|
|||
32728fc20a
|
|||
394cb80022
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,8 +10,8 @@ bld/*
|
||||
!bld/Resources
|
||||
|
||||
# Rider
|
||||
.idea/.idea.TweetDuck/.idea/dictionaries
|
||||
.idea/.idea.TweetDuck/.idea/misc.xml
|
||||
**/.idea/dictionaries
|
||||
**/.idea/misc.xml
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<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" />
|
||||
|
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using TweetLib.Core.Application;
|
||||
|
||||
namespace TweetDuck.Application {
|
||||
class SystemHandler : IAppSystemHandler {
|
||||
void IAppSystemHandler.OpenAssociatedProgram(string path) {
|
||||
try {
|
||||
using (Process.Start(new ProcessStartInfo {
|
||||
FileName = path,
|
||||
ErrorDialog = true
|
||||
})) {}
|
||||
} catch (Exception e) {
|
||||
Program.Reporter.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
|
||||
}
|
||||
}
|
||||
|
||||
void IAppSystemHandler.OpenFileExplorer(string path) {
|
||||
if (File.Exists(path)) {
|
||||
using (Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")) {}
|
||||
}
|
||||
else if (Directory.Exists(path)) {
|
||||
using (Process.Start("explorer.exe", '"' + path.Replace('/', '\\') + '"')) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CefSharp;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Browser;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
sealed class CefScriptExecutor : IScriptExecutor {
|
||||
private readonly IWebBrowser browser;
|
||||
|
||||
public CefScriptExecutor(IWebBrowser browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
public void RunFunction(string name, params object[] args) {
|
||||
browser.ExecuteJsAsync(name, args);
|
||||
}
|
||||
|
||||
public void RunScript(string identifier, string script) {
|
||||
using IFrame frame = browser.GetMainFrame();
|
||||
RunScript(frame, script, identifier);
|
||||
}
|
||||
|
||||
public void RunBootstrap(string moduleNamespace) {
|
||||
using IFrame frame = browser.GetMainFrame();
|
||||
RunBootstrap(frame, moduleNamespace);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
public static void RunScript(IFrame frame, string script, string identifier) {
|
||||
if (script != null) {
|
||||
frame.ExecuteJavaScriptAsync(script, identifier, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RunBootstrap(IFrame frame, string moduleNamespace) {
|
||||
string script = GetBootstrapScript(moduleNamespace, includeStylesheets: true);
|
||||
|
||||
if (script != null) {
|
||||
RunScript(frame, script, "bootstrap");
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetBootstrapScript(string moduleNamespace, bool includeStylesheets) {
|
||||
string script = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "bootstrap.js"));
|
||||
|
||||
if (script == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
string path = Path.Combine(Program.ResourcesPath, moduleNamespace);
|
||||
var files = new DirectoryInfo(path).GetFiles();
|
||||
|
||||
var moduleNames = new List<string>();
|
||||
var stylesheetNames = new List<string>();
|
||||
|
||||
foreach (var file in files) {
|
||||
var ext = Path.GetExtension(file.Name);
|
||||
|
||||
var targetList = ext switch {
|
||||
".js" => moduleNames,
|
||||
".css" => includeStylesheets ? stylesheetNames : null,
|
||||
_ => null
|
||||
};
|
||||
|
||||
targetList?.Add(Path.GetFileNameWithoutExtension(file.Name));
|
||||
}
|
||||
|
||||
script = script.Replace("{{namespace}}", moduleNamespace);
|
||||
script = script.Replace("{{modules}}", string.Join("|", moduleNames));
|
||||
script = script.Replace("{{stylesheets}}", string.Join("|", stylesheetNames));
|
||||
|
||||
return script;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Bridge {
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
class TweetDeckBridge {
|
||||
public static void ResetStaticProperties() {
|
||||
FormNotificationBase.FontSize = null;
|
||||
FormNotificationBase.HeadLayout = null;
|
||||
}
|
||||
|
||||
private readonly FormBrowser form;
|
||||
private readonly FormNotificationMain notification;
|
||||
|
||||
private TweetDeckBridge(FormBrowser form, FormNotificationMain notification) {
|
||||
this.form = form;
|
||||
this.notification = notification;
|
||||
}
|
||||
|
||||
// Browser only
|
||||
|
||||
public sealed class Browser : TweetDeckBridge {
|
||||
public Browser(FormBrowser form, FormNotificationMain notification) : base(form, notification) {}
|
||||
|
||||
public void OnModulesLoaded(string moduleNamespace) {
|
||||
form.InvokeAsyncSafe(() => form.OnModulesLoaded(moduleNamespace));
|
||||
}
|
||||
|
||||
public void OpenContextMenu() {
|
||||
form.InvokeAsyncSafe(form.OpenContextMenu);
|
||||
}
|
||||
|
||||
public void OpenProfileImport() {
|
||||
form.InvokeAsyncSafe(form.OpenProfileImport);
|
||||
}
|
||||
|
||||
public void OnIntroductionClosed(bool showGuide) {
|
||||
form.InvokeAsyncSafe(() => form.OnIntroductionClosed(showGuide));
|
||||
}
|
||||
|
||||
public void LoadNotificationLayout(string fontSize, string headLayout) {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
FormNotificationBase.FontSize = fontSize;
|
||||
FormNotificationBase.HeadLayout = headLayout;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetRightClickedLink(string type, string url) {
|
||||
ContextMenuBase.CurrentInfo.SetLink(type, url);
|
||||
}
|
||||
|
||||
public void SetRightClickedChirp(string columnId, string chirpId, string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages) {
|
||||
ContextMenuBase.CurrentInfo.SetChirp(columnId, chirpId, tweetUrl, quoteUrl, chirpAuthors, chirpImages);
|
||||
}
|
||||
|
||||
public void DisplayTooltip(string text) {
|
||||
form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
|
||||
}
|
||||
}
|
||||
|
||||
// Notification only
|
||||
|
||||
public sealed class Notification : TweetDeckBridge {
|
||||
public Notification(FormBrowser form, FormNotificationMain notification) : base(form, notification) {}
|
||||
|
||||
public void DisplayTooltip(string text) {
|
||||
notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
|
||||
}
|
||||
|
||||
public void LoadNextNotification() {
|
||||
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
|
||||
}
|
||||
|
||||
public void ShowTweetDetail() {
|
||||
notification.InvokeAsyncSafe(notification.ShowTweetDetail);
|
||||
}
|
||||
}
|
||||
|
||||
// Global
|
||||
|
||||
public void OnTweetPopup(string columnId, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl) {
|
||||
notification.InvokeAsyncSafe(() => {
|
||||
form.OnTweetNotification();
|
||||
notification.ShowNotification(new DesktopNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
|
||||
});
|
||||
}
|
||||
|
||||
public void OnTweetSound() {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
form.OnTweetNotification();
|
||||
form.OnTweetSound();
|
||||
});
|
||||
}
|
||||
|
||||
public void ScreenshotTweet(string html, int width) {
|
||||
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width));
|
||||
}
|
||||
|
||||
public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
|
||||
form.InvokeAsyncSafe(() => form.PlayVideo(videoUrl, tweetUrl, username, callShowOverlay));
|
||||
}
|
||||
|
||||
public void StopVideo() {
|
||||
form.InvokeAsyncSafe(form.StopVideo);
|
||||
}
|
||||
|
||||
public void FixClipboard() {
|
||||
form.InvokeAsyncSafe(ClipboardManager.StripHtmlStyles);
|
||||
}
|
||||
|
||||
public void OpenBrowser(string url) {
|
||||
form.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
|
||||
}
|
||||
|
||||
public void MakeGetRequest(string url, IJavascriptCallback onSuccess, IJavascriptCallback onError) {
|
||||
Task.Run(async () => {
|
||||
var client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
|
||||
|
||||
try {
|
||||
var result = await client.DownloadStringTaskAsync(url);
|
||||
await onSuccess.ExecuteAsync(result);
|
||||
} catch (Exception e) {
|
||||
await onError.ExecuteAsync(e.Message);
|
||||
} finally {
|
||||
onSuccess.Dispose();
|
||||
onError.Dispose();
|
||||
client.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int GetIdleSeconds() {
|
||||
return NativeMethods.GetIdleSeconds();
|
||||
}
|
||||
|
||||
public void Alert(string type, string contents) {
|
||||
MessageBoxIcon icon = type switch {
|
||||
"error" => MessageBoxIcon.Error,
|
||||
"warning" => MessageBoxIcon.Warning,
|
||||
"info" => MessageBoxIcon.Information,
|
||||
_ => MessageBoxIcon.None
|
||||
};
|
||||
|
||||
FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
|
||||
}
|
||||
|
||||
public void CrashDebug(string message) {
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine(message);
|
||||
System.Diagnostics.Debugger.Break();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Core.Systems.Updates;
|
||||
|
||||
namespace TweetDuck.Browser.Bridge {
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
class UpdateBridge {
|
||||
private readonly UpdateHandler updates;
|
||||
private readonly Control sync;
|
||||
|
||||
private UpdateInfo nextUpdate = null;
|
||||
|
||||
public event EventHandler<UpdateInfo> UpdateAccepted;
|
||||
public event EventHandler<UpdateInfo> UpdateDismissed;
|
||||
|
||||
public UpdateBridge(UpdateHandler updates, Control sync) {
|
||||
this.sync = sync;
|
||||
|
||||
this.updates = updates;
|
||||
this.updates.CheckFinished += updates_CheckFinished;
|
||||
}
|
||||
|
||||
internal void Cleanup() {
|
||||
updates.CheckFinished -= updates_CheckFinished;
|
||||
nextUpdate?.DeleteInstaller();
|
||||
}
|
||||
|
||||
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e) {
|
||||
UpdateInfo foundUpdate = e.Result.HasValue ? e.Result.Value : null;
|
||||
|
||||
if (nextUpdate != null && !nextUpdate.Equals(foundUpdate)) {
|
||||
nextUpdate.DeleteInstaller();
|
||||
}
|
||||
|
||||
nextUpdate = foundUpdate;
|
||||
}
|
||||
|
||||
private void HandleInteractionEvent(EventHandler<UpdateInfo> eventHandler) {
|
||||
UpdateInfo tmpInfo = nextUpdate;
|
||||
|
||||
if (tmpInfo != null) {
|
||||
sync.InvokeAsyncSafe(() => eventHandler?.Invoke(this, tmpInfo));
|
||||
}
|
||||
}
|
||||
|
||||
// Bridge methods
|
||||
|
||||
public void TriggerUpdateCheck() {
|
||||
updates.Check(false);
|
||||
}
|
||||
|
||||
public void OnUpdateAccepted() {
|
||||
HandleInteractionEvent(UpdateAccepted);
|
||||
}
|
||||
|
||||
public void OnUpdateDismissed() {
|
||||
HandleInteractionEvent(UpdateDismissed);
|
||||
|
||||
nextUpdate?.DeleteInstaller();
|
||||
nextUpdate = null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,166 +0,0 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Data {
|
||||
sealed class ContextInfo {
|
||||
private LinkInfo link;
|
||||
private ChirpInfo? chirp;
|
||||
|
||||
public ContextInfo() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void SetLink(string type, string url) {
|
||||
link = string.IsNullOrEmpty(url) ? null : new LinkInfo(type, url);
|
||||
}
|
||||
|
||||
public void SetChirp(string columnId, string chirpId, string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages) {
|
||||
chirp = string.IsNullOrEmpty(tweetUrl) ? (ChirpInfo?) null : new ChirpInfo(columnId, chirpId, tweetUrl, quoteUrl, chirpAuthors, chirpImages);
|
||||
}
|
||||
|
||||
public ContextData Reset() {
|
||||
link = null;
|
||||
chirp = null;
|
||||
return ContextData.Empty;
|
||||
}
|
||||
|
||||
public ContextData Create(IContextMenuParams parameters) {
|
||||
ContextData.Builder builder = new ContextData.Builder();
|
||||
builder.AddContext(parameters);
|
||||
|
||||
if (link != null) {
|
||||
builder.AddOverride(link.Type, link.Url);
|
||||
}
|
||||
|
||||
if (chirp.HasValue) {
|
||||
builder.AddChirp(chirp.Value);
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
// Data structures
|
||||
|
||||
private sealed class LinkInfo {
|
||||
public string Type { get; }
|
||||
public string Url { get; }
|
||||
|
||||
public LinkInfo(string type, string url) {
|
||||
this.Type = type;
|
||||
this.Url = url;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ChirpInfo {
|
||||
public string ColumnId { get; }
|
||||
public string ChirpId { get; }
|
||||
|
||||
public string TweetUrl { get; }
|
||||
public string QuoteUrl { get; }
|
||||
|
||||
public string[] Authors => chirpAuthors?.Split(';') ?? StringUtils.EmptyArray;
|
||||
public string[] Images => chirpImages?.Split(';') ?? StringUtils.EmptyArray;
|
||||
|
||||
private readonly string chirpAuthors;
|
||||
private readonly string chirpImages;
|
||||
|
||||
public ChirpInfo(string columnId, string chirpId, string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages) {
|
||||
this.ColumnId = columnId;
|
||||
this.ChirpId = chirpId;
|
||||
this.TweetUrl = tweetUrl;
|
||||
this.QuoteUrl = quoteUrl;
|
||||
this.chirpAuthors = chirpAuthors;
|
||||
this.chirpImages = chirpImages;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructed context
|
||||
|
||||
[Flags]
|
||||
public enum ContextType {
|
||||
Unknown = 0,
|
||||
Link = 0b0001,
|
||||
Image = 0b0010,
|
||||
Video = 0b0100,
|
||||
Chirp = 0b1000
|
||||
}
|
||||
|
||||
public sealed class ContextData {
|
||||
public static readonly ContextData Empty = new Builder().Build();
|
||||
|
||||
public ContextType Types { get; }
|
||||
|
||||
public string LinkUrl { get; }
|
||||
public string UnsafeLinkUrl { get; }
|
||||
public string MediaUrl { get; }
|
||||
|
||||
public ChirpInfo Chirp { get; }
|
||||
|
||||
private ContextData(ContextType types, string linkUrl, string unsafeLinkUrl, string mediaUrl, ChirpInfo chirp) {
|
||||
Types = types;
|
||||
LinkUrl = linkUrl;
|
||||
UnsafeLinkUrl = unsafeLinkUrl;
|
||||
MediaUrl = mediaUrl;
|
||||
Chirp = chirp;
|
||||
}
|
||||
|
||||
public sealed class Builder {
|
||||
private ContextType types = ContextType.Unknown;
|
||||
|
||||
private string linkUrl = string.Empty;
|
||||
private string unsafeLinkUrl = string.Empty;
|
||||
private string mediaUrl = string.Empty;
|
||||
|
||||
private ChirpInfo chirp = default;
|
||||
|
||||
public void AddContext(IContextMenuParams parameters) {
|
||||
ContextMenuType flags = parameters.TypeFlags;
|
||||
|
||||
if (flags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) {
|
||||
types |= ContextType.Image;
|
||||
types &= ~ContextType.Video;
|
||||
mediaUrl = parameters.SourceUrl;
|
||||
}
|
||||
|
||||
if (flags.HasFlag(ContextMenuType.Link)) {
|
||||
types |= ContextType.Link;
|
||||
linkUrl = parameters.LinkUrl;
|
||||
unsafeLinkUrl = parameters.UnfilteredLinkUrl;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOverride(string type, string url) {
|
||||
switch (type) {
|
||||
case "link":
|
||||
types |= ContextType.Link;
|
||||
linkUrl = url;
|
||||
unsafeLinkUrl = url;
|
||||
break;
|
||||
|
||||
case "image":
|
||||
types |= ContextType.Image;
|
||||
types &= ~(ContextType.Video | ContextType.Link);
|
||||
mediaUrl = url;
|
||||
break;
|
||||
|
||||
case "video":
|
||||
types |= ContextType.Video;
|
||||
types &= ~(ContextType.Image | ContextType.Link);
|
||||
mediaUrl = url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddChirp(ChirpInfo chirp) {
|
||||
this.types |= ContextType.Chirp;
|
||||
this.chirp = chirp;
|
||||
}
|
||||
|
||||
public ContextData Build() {
|
||||
return new ContextData(types, linkUrl, unsafeLinkUrl, mediaUrl, chirp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using CefSharp;
|
||||
|
||||
namespace TweetDuck.Browser.Data {
|
||||
sealed class ResourceHandlers {
|
||||
private readonly ConcurrentDictionary<string, Func<IResourceHandler>> handlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public bool HasHandler(IRequest request) {
|
||||
return handlers.ContainsKey(request.Url);
|
||||
}
|
||||
|
||||
public IResourceHandler GetHandler(IRequest request) {
|
||||
return handlers.TryGetValue(request.Url, out var factory) ? factory() : null;
|
||||
}
|
||||
|
||||
public bool Register(string url, Func<IResourceHandler> factory) {
|
||||
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
|
||||
handlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Register(ResourceLink link) {
|
||||
return Register(link.Url, link.Factory);
|
||||
}
|
||||
|
||||
public bool Unregister(string url) {
|
||||
return handlers.TryRemove(url, out _);
|
||||
}
|
||||
|
||||
public bool Unregister(ResourceLink link) {
|
||||
return Unregister(link.Url);
|
||||
}
|
||||
|
||||
public static Func<IResourceHandler> ForString(string str) {
|
||||
return () => ResourceHandler.FromString(str);
|
||||
}
|
||||
|
||||
public static Func<IResourceHandler> ForString(string str, string mimeType) {
|
||||
return () => ResourceHandler.FromString(str, mimeType: mimeType);
|
||||
}
|
||||
|
||||
public static Func<IResourceHandler> ForBytes(byte[] bytes, string mimeType) {
|
||||
return () => ResourceHandler.FromByteArray(bytes, mimeType);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
|
||||
namespace TweetDuck.Browser.Data {
|
||||
sealed class ResourceLink {
|
||||
public string Url { get; }
|
||||
public Func<IResourceHandler> Factory { get; }
|
||||
|
||||
public ResourceLink(string url, Func<IResourceHandler> factory) {
|
||||
this.Url = url;
|
||||
this.Factory = factory;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Core.Serialization.Converters;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Data {
|
||||
sealed class WindowState {
|
||||
private Rectangle rect;
|
||||
private bool isMaximized;
|
||||
|
||||
public void Save(Form form) {
|
||||
rect = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds;
|
||||
isMaximized = form.WindowState == FormWindowState.Maximized;
|
||||
}
|
||||
|
||||
public void Restore(Form form, bool firstTimeFullscreen) {
|
||||
if (rect != Rectangle.Empty) {
|
||||
form.DesktopBounds = rect;
|
||||
form.WindowState = isMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
||||
}
|
||||
|
||||
if ((rect == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) {
|
||||
form.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
|
||||
form.WindowState = FormWindowState.Maximized;
|
||||
Save(form);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly SingleTypeConverter<WindowState> Converter = new SingleTypeConverter<WindowState> {
|
||||
ConvertToString = value => $"{(value.isMaximized ? 'M' : '_')}{value.rect.X} {value.rect.Y} {value.rect.Width} {value.rect.Height}",
|
||||
ConvertToObject = value => {
|
||||
int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
|
||||
|
||||
return new WindowState {
|
||||
rect = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
|
||||
isMaximized = value[0] == 'M'
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,230 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
abstract class ContextMenuBase : IContextMenuHandler {
|
||||
public static ContextInfo CurrentInfo { get; } = new ContextInfo();
|
||||
|
||||
protected static UserConfig Config => Program.Config.User;
|
||||
private static ImageQuality ImageQuality => Config.TwitterImageQuality;
|
||||
|
||||
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand) 26500;
|
||||
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand) 26501;
|
||||
private const CefMenuCommand MenuCopyUsername = (CefMenuCommand) 26502;
|
||||
private const CefMenuCommand MenuViewImage = (CefMenuCommand) 26503;
|
||||
private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand) 26504;
|
||||
private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand) 26505;
|
||||
private const CefMenuCommand MenuCopyImage = (CefMenuCommand) 26506;
|
||||
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand) 26507;
|
||||
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand) 26508;
|
||||
private const CefMenuCommand MenuSearchInBrowser = (CefMenuCommand) 26509;
|
||||
private const CefMenuCommand MenuReadApplyROT13 = (CefMenuCommand) 26510;
|
||||
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26599;
|
||||
|
||||
protected ContextInfo.ContextData Context { get; private set; }
|
||||
|
||||
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
|
||||
if (!TwitterUrls.IsTweetDeck(frame.Url) || browser.IsLoading) {
|
||||
Context = CurrentInfo.Reset();
|
||||
}
|
||||
else {
|
||||
Context = CurrentInfo.Create(parameters);
|
||||
}
|
||||
|
||||
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection) && !parameters.TypeFlags.HasFlag(ContextMenuType.Editable)) {
|
||||
model.AddItem(MenuSearchInBrowser, "Search in browser");
|
||||
model.AddSeparator();
|
||||
model.AddItem(MenuReadApplyROT13, "Apply ROT13");
|
||||
model.AddSeparator();
|
||||
}
|
||||
|
||||
static string TextOpen(string name) => "Open " + name + " in browser";
|
||||
static string TextCopy(string name) => "Copy " + name + " address";
|
||||
static string TextSave(string name) => "Save " + name + " as...";
|
||||
|
||||
if (Context.Types.HasFlag(ContextInfo.ContextType.Link) && !Context.UnsafeLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal)) {
|
||||
if (TwitterUrls.RegexAccount.IsMatch(Context.UnsafeLinkUrl)) {
|
||||
model.AddItem(MenuOpenLinkUrl, TextOpen("account"));
|
||||
model.AddItem(MenuCopyLinkUrl, TextCopy("account"));
|
||||
model.AddItem(MenuCopyUsername, "Copy account username");
|
||||
}
|
||||
else {
|
||||
model.AddItem(MenuOpenLinkUrl, TextOpen("link"));
|
||||
model.AddItem(MenuCopyLinkUrl, TextCopy("link"));
|
||||
}
|
||||
|
||||
model.AddSeparator();
|
||||
}
|
||||
|
||||
if (Context.Types.HasFlag(ContextInfo.ContextType.Video)) {
|
||||
model.AddItem(MenuOpenMediaUrl, TextOpen("video"));
|
||||
model.AddItem(MenuCopyMediaUrl, TextCopy("video"));
|
||||
model.AddItem(MenuSaveMedia, TextSave("video"));
|
||||
model.AddSeparator();
|
||||
}
|
||||
else if (Context.Types.HasFlag(ContextInfo.ContextType.Image) && Context.MediaUrl != FormNotificationBase.AppLogo.Url) {
|
||||
model.AddItem(MenuViewImage, "View image in photo viewer");
|
||||
model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
|
||||
model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
|
||||
model.AddItem(MenuCopyImage, "Copy image");
|
||||
model.AddItem(MenuSaveMedia, TextSave("image"));
|
||||
|
||||
if (Context.Chirp.Images.Length > 1) {
|
||||
model.AddItem(MenuSaveTweetImages, TextSave("all images"));
|
||||
}
|
||||
|
||||
model.AddSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
|
||||
Control control = browserControl.AsControl();
|
||||
|
||||
switch (commandId) {
|
||||
case MenuOpenLinkUrl:
|
||||
OpenBrowser(control, Context.LinkUrl);
|
||||
break;
|
||||
|
||||
case MenuCopyLinkUrl:
|
||||
SetClipboardText(control, Context.UnsafeLinkUrl);
|
||||
break;
|
||||
|
||||
case MenuCopyUsername: {
|
||||
string url = Context.UnsafeLinkUrl;
|
||||
Match match = TwitterUrls.RegexAccount.Match(url);
|
||||
|
||||
SetClipboardText(control, match.Success ? match.Groups[1].Value : url);
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuOpenMediaUrl:
|
||||
OpenBrowser(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
|
||||
break;
|
||||
|
||||
case MenuCopyMediaUrl:
|
||||
SetClipboardText(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
|
||||
break;
|
||||
|
||||
case MenuCopyImage: {
|
||||
string url = Context.MediaUrl;
|
||||
|
||||
control.InvokeAsyncSafe(() => { TwitterUtils.CopyImage(url, ImageQuality); });
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuViewImage: {
|
||||
string url = Context.MediaUrl;
|
||||
|
||||
control.InvokeAsyncSafe(() => {
|
||||
TwitterUtils.ViewImage(url, ImageQuality);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuSaveMedia: {
|
||||
bool isVideo = Context.Types.HasFlag(ContextInfo.ContextType.Video);
|
||||
string url = Context.MediaUrl;
|
||||
string username = Context.Chirp.Authors.LastOrDefault();
|
||||
|
||||
control.InvokeAsyncSafe(() => {
|
||||
if (isVideo) {
|
||||
TwitterUtils.DownloadVideo(url, username);
|
||||
}
|
||||
else {
|
||||
TwitterUtils.DownloadImage(url, username, ImageQuality);
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuSaveTweetImages: {
|
||||
string[] urls = Context.Chirp.Images;
|
||||
string username = Context.Chirp.Authors.LastOrDefault();
|
||||
|
||||
control.InvokeAsyncSafe(() => {
|
||||
TwitterUtils.DownloadImages(urls, username, ImageQuality);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuReadApplyROT13:
|
||||
string selection = parameters.SelectionText;
|
||||
control.InvokeAsyncSafe(() => FormMessage.Information("ROT13", StringUtils.ConvertRot13(selection), FormMessage.OK));
|
||||
return true;
|
||||
|
||||
case MenuSearchInBrowser:
|
||||
string query = parameters.SelectionText;
|
||||
control.InvokeAsyncSafe(() => BrowserUtils.OpenExternalSearch(query));
|
||||
DeselectAll(frame);
|
||||
break;
|
||||
|
||||
case MenuOpenDevTools:
|
||||
browserControl.OpenDevToolsCustom(new Point(parameters.XCoord, parameters.YCoord));
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
|
||||
Context = CurrentInfo.Reset();
|
||||
}
|
||||
|
||||
public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static void DeselectAll(IFrame frame) {
|
||||
CefScriptExecutor.RunScript(frame, "window.getSelection().removeAllRanges()", "gen:deselect");
|
||||
}
|
||||
|
||||
protected static void OpenBrowser(Control control, string url) {
|
||||
control.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
|
||||
}
|
||||
|
||||
protected static void SetClipboardText(Control control, string text) {
|
||||
control.InvokeAsyncSafe(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
|
||||
}
|
||||
|
||||
protected static void InsertSelectionSearchItem(IMenuModel model, CefMenuCommand insertCommand, string insertLabel) {
|
||||
model.InsertItemAt(model.GetIndexOf(MenuSearchInBrowser) + 1, insertCommand, insertLabel);
|
||||
}
|
||||
|
||||
protected static void AddDebugMenuItems(IMenuModel model) {
|
||||
if (Config.DevToolsInContextMenu) {
|
||||
AddSeparator(model);
|
||||
model.AddItem(MenuOpenDevTools, "Open dev tools");
|
||||
}
|
||||
}
|
||||
|
||||
protected static void RemoveSeparatorIfLast(IMenuModel model) {
|
||||
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) {
|
||||
model.RemoveAt(model.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void AddSeparator(IMenuModel model) {
|
||||
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate
|
||||
model.AddSeparator();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
using CefSharp;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class ContextMenuGuide : ContextMenuBase {
|
||||
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
|
||||
model.Clear();
|
||||
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
|
||||
AddDebugMenuItems(model);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Controls;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class ContextMenuNotification : ContextMenuBase {
|
||||
private const CefMenuCommand MenuViewDetail = (CefMenuCommand) 26600;
|
||||
private const CefMenuCommand MenuSkipTweet = (CefMenuCommand) 26601;
|
||||
private const CefMenuCommand MenuFreeze = (CefMenuCommand) 26602;
|
||||
private const CefMenuCommand MenuCopyTweetUrl = (CefMenuCommand) 26603;
|
||||
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand) 26604;
|
||||
|
||||
private readonly FormNotificationBase form;
|
||||
private readonly bool enableCustomMenu;
|
||||
|
||||
public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu) {
|
||||
this.form = form;
|
||||
this.enableCustomMenu = enableCustomMenu;
|
||||
}
|
||||
|
||||
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
|
||||
model.Clear();
|
||||
|
||||
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)) {
|
||||
model.AddItem(CefMenuCommand.Copy, "Copy");
|
||||
model.AddSeparator();
|
||||
}
|
||||
|
||||
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
|
||||
|
||||
if (enableCustomMenu) {
|
||||
if (form.CanViewDetail) {
|
||||
model.AddItem(MenuViewDetail, "View detail");
|
||||
}
|
||||
|
||||
model.AddItem(MenuSkipTweet, "Skip tweet");
|
||||
model.AddCheckItem(MenuFreeze, "Freeze");
|
||||
model.SetChecked(MenuFreeze, form.FreezeTimer);
|
||||
|
||||
if (!string.IsNullOrEmpty(form.CurrentTweetUrl)) {
|
||||
model.AddSeparator();
|
||||
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
|
||||
|
||||
if (!string.IsNullOrEmpty(form.CurrentQuoteUrl)) {
|
||||
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddDebugMenuItems(model);
|
||||
RemoveSeparatorIfLast(model);
|
||||
|
||||
form.InvokeAsyncSafe(() => {
|
||||
form.ContextMenuOpen = true;
|
||||
});
|
||||
}
|
||||
|
||||
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
|
||||
if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (commandId) {
|
||||
case MenuSkipTweet:
|
||||
form.InvokeAsyncSafe(form.FinishCurrentNotification);
|
||||
return true;
|
||||
|
||||
case MenuFreeze:
|
||||
form.InvokeAsyncSafe(() => form.FreezeTimer = !form.FreezeTimer);
|
||||
return true;
|
||||
|
||||
case MenuViewDetail:
|
||||
form.InvokeSafe(form.ShowTweetDetail);
|
||||
return true;
|
||||
|
||||
case MenuCopyTweetUrl:
|
||||
SetClipboardText(form, form.CurrentTweetUrl);
|
||||
return true;
|
||||
|
||||
case MenuCopyQuotedTweetUrl:
|
||||
SetClipboardText(form, form.CurrentQuoteUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
|
||||
base.OnContextMenuDismissed(browserControl, browser, frame);
|
||||
form.InvokeAsyncSafe(() => form.ContextMenuOpen = false);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CefSharp;
|
||||
using CefSharp.Enums;
|
||||
using TweetDuck.Utils;
|
||||
|
||||
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.ExecuteJsAsync("window.TDGF_onGlobalDragStart", type, data);
|
||||
}
|
||||
|
||||
requestHandler.BlockNextUserNavUrl = dragData.LinkUrl; // empty if not a link
|
||||
|
||||
if (dragData.IsLink) {
|
||||
TriggerDragStart("link", dragData.LinkUrl);
|
||||
}
|
||||
else if (dragData.IsFragment) {
|
||||
TriggerDragStart("text", dragData.FragmentText.Trim());
|
||||
}
|
||||
else {
|
||||
TriggerDragStart("unknown");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {}
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.Filters {
|
||||
sealed class ResponseFilterVendor : ResponseFilterBase {
|
||||
private static readonly Regex RegexRestoreJQuery = new Regex(@"(\w+)\.fn=\1\.prototype", RegexOptions.Compiled);
|
||||
|
||||
public ResponseFilterVendor(int totalBytes) : base(totalBytes, Encoding.UTF8) {}
|
||||
|
||||
public override bool InitFilter() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override string ProcessResponse(string text) {
|
||||
return RegexRestoreJQuery.Replace(text, "window.$$=$1;$&", 1);
|
||||
}
|
||||
|
||||
public override void Dispose() {}
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
using CefSharp;
|
||||
using CefSharp.Handler;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
sealed class CustomLifeSpanHandler : LifeSpanHandler {
|
||||
private static bool IsPopupAllowed(string url) {
|
||||
return url.StartsWith("https://twitter.com/teams/authorize?");
|
||||
}
|
||||
|
||||
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl) {
|
||||
switch (targetDisposition) {
|
||||
case WindowOpenDisposition.NewBackgroundTab:
|
||||
case WindowOpenDisposition.NewForegroundTab:
|
||||
case WindowOpenDisposition.NewPopup when !IsPopupAllowed(targetUrl):
|
||||
case WindowOpenDisposition.NewWindow:
|
||||
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(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(browserControl, targetDisposition, targetUrl);
|
||||
}
|
||||
|
||||
protected override bool DoClose(IWebBrowser browserControl, IBrowser browser) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
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 };
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "image/jpeg": return new string[] { ".jpg", ".jpeg" };
|
||||
case "image/png": return new string[] { ".png" };
|
||||
case "image/gif": return new string[] { ".gif" };
|
||||
case "image/webp": return new string[] { ".webp" };
|
||||
case "video/mp4": return new string[] { ".mp4" };
|
||||
case "video/quicktime": return new string[] { ".mov", ".qt" };
|
||||
}
|
||||
|
||||
System.Diagnostics.Debugger.Break();
|
||||
return StringUtils.EmptyArray;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
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) {
|
||||
browserControl.AsControl().InvokeSafe(() => {
|
||||
FormMessage form;
|
||||
TextBox input = null;
|
||||
|
||||
if (dialogType == CefJsDialogType.Alert) {
|
||||
form = CreateMessageForm("Browser Message", messageText);
|
||||
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
|
||||
}
|
||||
else if (dialogType == CefJsDialogType.Confirm) {
|
||||
form = CreateMessageForm("Browser Confirmation", messageText);
|
||||
form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel);
|
||||
form.AddButton(FormMessage.Yes, ControlType.Focused);
|
||||
}
|
||||
else if (dialogType == CefJsDialogType.Prompt) {
|
||||
form = CreateMessageForm("Browser Prompt", messageText);
|
||||
form.AddButton(FormMessage.Cancel, DialogResult.Cancel, ControlType.Cancel);
|
||||
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
|
||||
|
||||
float dpiScale = form.GetDPIScale();
|
||||
int inputPad = form.HasIcon ? 43 : 0;
|
||||
|
||||
input = new TextBox {
|
||||
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
|
||||
Font = SystemFonts.MessageBoxFont,
|
||||
Location = new Point(BrowserUtils.Scale(22 + inputPad, dpiScale), form.ActionPanelY - BrowserUtils.Scale(46, dpiScale)),
|
||||
Size = new Size(form.ClientSize.Width - BrowserUtils.Scale(44 + inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale))
|
||||
};
|
||||
|
||||
form.Controls.Add(input);
|
||||
form.ActiveControl = input;
|
||||
form.Height += input.Size.Height + input.Margin.Vertical;
|
||||
}
|
||||
else {
|
||||
callback.Continue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = form.ShowDialog() == DialogResult.OK;
|
||||
|
||||
if (input == null) {
|
||||
callback.Continue(success);
|
||||
}
|
||||
else {
|
||||
callback.Continue(success, input.Text);
|
||||
input.Dispose();
|
||||
}
|
||||
|
||||
form.Dispose();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IJsDialogHandler.OnBeforeUnloadDialog(IWebBrowser browserControl, IBrowser browser, string messageText, bool isReload, IJsDialogCallback callback) {
|
||||
callback.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
void IJsDialogHandler.OnResetDialogState(IWebBrowser browserControl, IBrowser browser) {}
|
||||
void IJsDialogHandler.OnDialogClosed(IWebBrowser browserControl, IBrowser browser) {}
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class KeyboardHandlerBrowser : KeyboardHandlerBase {
|
||||
private readonly FormBrowser form;
|
||||
|
||||
public KeyboardHandlerBrowser(FormBrowser form) {
|
||||
this.form = form;
|
||||
}
|
||||
|
||||
protected override bool HandleRawKey(IWebBrowser browserControl, Keys key, CefEventFlags modifiers) {
|
||||
return base.HandleRawKey(browserControl, key, modifiers) || form.ProcessBrowserKey(key);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Controls;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class KeyboardHandlerNotification : KeyboardHandlerBase {
|
||||
private readonly FormNotificationBase notification;
|
||||
|
||||
public KeyboardHandlerNotification(FormNotificationBase notification) {
|
||||
this.notification = notification;
|
||||
}
|
||||
|
||||
protected override bool HandleRawKey(IWebBrowser browserControl, Keys key, CefEventFlags modifiers) {
|
||||
if (base.HandleRawKey(browserControl, key, modifiers)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case Keys.Enter:
|
||||
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
|
||||
return true;
|
||||
|
||||
case Keys.Escape:
|
||||
notification.InvokeAsyncSafe(notification.HideNotification);
|
||||
return true;
|
||||
|
||||
case Keys.Space:
|
||||
notification.InvokeAsyncSafe(() => notification.FreezeTimer = !notification.FreezeTimer);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
using CefSharp;
|
||||
using CefSharp.Handler;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
|
||||
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(browserControl, targetDisposition, targetUrl);
|
||||
}
|
||||
|
||||
protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) {
|
||||
if (autoReload) {
|
||||
browser.Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class RequestHandlerBrowser : RequestHandlerBase {
|
||||
public string BlockNextUserNavUrl { get; set; }
|
||||
|
||||
public RequestHandlerBrowser() : base(true) {}
|
||||
|
||||
protected override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) {
|
||||
if (userGesture && request.TransitionType == TransitionType.LinkClicked) {
|
||||
bool block = request.Url == BlockNextUserNavUrl;
|
||||
BlockNextUserNavUrl = string.Empty;
|
||||
return block;
|
||||
}
|
||||
else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using CefSharp.Callback;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class ResourceHandlerNotification : IResourceHandler {
|
||||
private readonly NameValueCollection headers = new NameValueCollection(0);
|
||||
private MemoryStream dataIn;
|
||||
|
||||
public void SetHTML(string html) {
|
||||
dataIn?.Dispose();
|
||||
dataIn = ResourceHandler.GetMemoryStream(html, Encoding.UTF8);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (dataIn != null) {
|
||||
dataIn.Dispose();
|
||||
dataIn = null;
|
||||
}
|
||||
}
|
||||
|
||||
bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) {
|
||||
callback.Dispose();
|
||||
handleRequest = true;
|
||||
|
||||
if (dataIn != null) {
|
||||
dataIn.Position = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) {
|
||||
redirectUrl = null;
|
||||
|
||||
response.MimeType = "text/html";
|
||||
response.StatusCode = 200;
|
||||
response.StatusText = "OK";
|
||||
response.Headers = headers;
|
||||
responseLength = dataIn?.Length ?? 0;
|
||||
}
|
||||
|
||||
bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) {
|
||||
callback?.Dispose(); // TODO unnecessary null check once ReadResponse is removed
|
||||
|
||||
try {
|
||||
byte[] buffer = new byte[Math.Min(dataIn.Length - dataIn.Position, dataOut.Length)];
|
||||
int length = buffer.Length;
|
||||
|
||||
dataIn.Read(buffer, 0, length);
|
||||
dataOut.Write(buffer, 0, length);
|
||||
bytesRead = length;
|
||||
} catch { // catch IOException, possibly NullReferenceException if dataIn is null
|
||||
bytesRead = 0;
|
||||
}
|
||||
|
||||
return bytesRead > 0;
|
||||
}
|
||||
|
||||
bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) {
|
||||
bytesSkipped = -2; // ERR_FAILED
|
||||
callback.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) {
|
||||
return ((IResourceHandler) this).Open(request, out bool _, callback);
|
||||
}
|
||||
|
||||
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) {
|
||||
return ((IResourceHandler) this).Read(dataOut, out bytesRead, null);
|
||||
}
|
||||
|
||||
void IResourceHandler.Cancel() {}
|
||||
}
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Browser;
|
||||
using IOFile = System.IO.File;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
internal sealed class ResourceProvider : IResourceProvider<IResourceHandler> {
|
||||
private readonly Dictionary<string, ICachedResource> cache = new Dictionary<string, ICachedResource>();
|
||||
|
||||
public IResourceHandler Status(HttpStatusCode code, string message) {
|
||||
return CreateStatusHandler(code, message);
|
||||
}
|
||||
|
||||
public IResourceHandler File(string path) {
|
||||
string key = new Uri(path).LocalPath;
|
||||
|
||||
if (cache.TryGetValue(key, out var cachedResource)) {
|
||||
return cachedResource.GetResource();
|
||||
}
|
||||
|
||||
cachedResource = FileWithCaching(path);
|
||||
cache[key] = cachedResource;
|
||||
return cachedResource.GetResource();
|
||||
}
|
||||
|
||||
private ICachedResource FileWithCaching(string path) {
|
||||
try {
|
||||
return new CachedFile(System.IO.File.ReadAllBytes(path), Path.GetExtension(path));
|
||||
} catch (FileNotFoundException) {
|
||||
return new CachedStatus(HttpStatusCode.NotFound, "File not found.");
|
||||
} catch (DirectoryNotFoundException) {
|
||||
return new CachedStatus(HttpStatusCode.NotFound, "Directory not found.");
|
||||
} catch (Exception e) {
|
||||
return new CachedStatus(HttpStatusCode.InternalServerError, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCache() {
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
private static ResourceHandler CreateHandler(byte[] bytes) {
|
||||
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
|
||||
handler.Headers.Set("Access-Control-Allow-Origin", "*");
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static IResourceHandler CreateFileContentsHandler(byte[] bytes, string extension) {
|
||||
if (bytes.Length == 0) {
|
||||
return CreateStatusHandler(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
|
||||
}
|
||||
else {
|
||||
var handler = CreateHandler(bytes);
|
||||
handler.MimeType = Cef.GetMimeType(extension);
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
|
||||
private static IResourceHandler CreateStatusHandler(HttpStatusCode code, string message) {
|
||||
var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
|
||||
handler.StatusCode = (int) code;
|
||||
return handler;
|
||||
}
|
||||
|
||||
private interface ICachedResource {
|
||||
IResourceHandler GetResource();
|
||||
}
|
||||
|
||||
private sealed class CachedFile : ICachedResource {
|
||||
private readonly byte[] bytes;
|
||||
private readonly string extension;
|
||||
|
||||
public CachedFile(byte[] bytes, string extension) {
|
||||
this.bytes = bytes;
|
||||
this.extension = extension;
|
||||
}
|
||||
|
||||
public IResourceHandler GetResource() {
|
||||
return CreateFileContentsHandler(bytes, extension);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CachedStatus : ICachedResource {
|
||||
private readonly HttpStatusCode code;
|
||||
private readonly string message;
|
||||
|
||||
public CachedStatus(HttpStatusCode code, string message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public IResourceHandler GetResource() {
|
||||
return CreateStatusHandler(code, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Data;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
abstract class ResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler {
|
||||
private class SelfFactoryImpl : IResourceRequestHandlerFactory {
|
||||
private readonly ResourceRequestHandler me;
|
||||
|
||||
public SelfFactoryImpl(ResourceRequestHandler me) {
|
||||
this.me = me;
|
||||
}
|
||||
|
||||
bool IResourceRequestHandlerFactory.HasHandlers => true;
|
||||
|
||||
[SuppressMessage("ReSharper", "RedundantAssignment")]
|
||||
IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) {
|
||||
disableDefaultHandling = me.ResourceHandlers.HasHandler(request);
|
||||
return me;
|
||||
}
|
||||
}
|
||||
|
||||
public IResourceRequestHandlerFactory SelfFactory { get; }
|
||||
public ResourceHandlers ResourceHandlers { get; }
|
||||
|
||||
protected ResourceRequestHandler() {
|
||||
this.SelfFactory = new SelfFactoryImpl(this);
|
||||
this.ResourceHandlers = new ResourceHandlers();
|
||||
}
|
||||
|
||||
protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) {
|
||||
return ResourceHandlers.GetHandler(request);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
class ResourceRequestHandlerBase : ResourceRequestHandler {
|
||||
private static readonly Regex TweetDeckResourceUrl = new Regex(@"/dist/(.*?)\.(.*?)\.(css|js)$");
|
||||
private static readonly SortedList<string, string> TweetDeckHashes = new SortedList<string, string>(4);
|
||||
|
||||
public static void LoadResourceRewriteRules(string rules) {
|
||||
if (string.IsNullOrEmpty(rules)) {
|
||||
return;
|
||||
}
|
||||
|
||||
TweetDeckHashes.Clear();
|
||||
|
||||
foreach (string rule in rules.Replace(" ", "").ToLower().Split(',')) {
|
||||
var (key, hash) = StringUtils.SplitInTwo(rule, '=') ?? throw new ArgumentException("A rule must have one '=' character: " + rule);
|
||||
|
||||
if (hash.All(chr => char.IsDigit(chr) || (chr >= 'a' && chr <= 'f'))) {
|
||||
TweetDeckHashes.Add(key, hash);
|
||||
}
|
||||
else {
|
||||
throw new ArgumentException("Invalid hash characters: " + rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
|
||||
if (request.ResourceType == ResourceType.CspReport) {
|
||||
callback.Dispose();
|
||||
return CefReturnValue.Cancel;
|
||||
}
|
||||
|
||||
NameValueCollection headers = request.Headers;
|
||||
headers.Remove("x-devtools-emulate-network-conditions-client-id");
|
||||
request.Headers = headers;
|
||||
|
||||
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
|
||||
}
|
||||
|
||||
protected override bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
|
||||
if ((request.ResourceType == ResourceType.Script || request.ResourceType == ResourceType.Stylesheet) && TweetDeckHashes.Count > 0) {
|
||||
string url = request.Url;
|
||||
Match match = TweetDeckResourceUrl.Match(url);
|
||||
|
||||
if (match.Success && TweetDeckHashes.TryGetValue($"{match.Groups[1]}.{match.Groups[3]}", out string hash)) {
|
||||
if (match.Groups[2].Value == hash) {
|
||||
Program.Reporter.LogVerbose("[RequestHandlerBase] Accepting " + url);
|
||||
}
|
||||
else {
|
||||
Program.Reporter.LogVerbose("[RequestHandlerBase] Replacing " + url + " hash with " + hash);
|
||||
request.Url = TweetDeckResourceUrl.Replace(url, $"/dist/$1.{hash}.$3");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnResourceResponse(browserControl, browser, frame, request, response);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Handling.Filters;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
class ResourceRequestHandlerBrowser : ResourceRequestHandlerBase {
|
||||
private const string UrlVendorResource = "/dist/vendor";
|
||||
private const string UrlLoadingSpinner = "/backgrounds/spinner_blue";
|
||||
private const string UrlVersionCheck = "/web/dist/version.json";
|
||||
|
||||
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
|
||||
if (request.ResourceType == ResourceType.MainFrame) {
|
||||
if (request.Url.EndsWith("//twitter.com/")) {
|
||||
request.Url = TwitterUrls.TweetDeck; // redirect plain twitter.com requests, fixes bugs with login 2FA
|
||||
}
|
||||
}
|
||||
else if (request.ResourceType == ResourceType.Image) {
|
||||
if (request.Url.Contains(UrlLoadingSpinner)) {
|
||||
request.Url = TwitterUtils.LoadingSpinner.Url;
|
||||
}
|
||||
}
|
||||
else if (request.ResourceType == ResourceType.Script) {
|
||||
string url = request.Url;
|
||||
|
||||
if (url.Contains("analytics.")) {
|
||||
callback.Dispose();
|
||||
return CefReturnValue.Cancel;
|
||||
}
|
||||
else if (url.Contains(UrlVendorResource)) {
|
||||
request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true);
|
||||
}
|
||||
}
|
||||
else if (request.ResourceType == ResourceType.Xhr) {
|
||||
if (request.Url.Contains(UrlVersionCheck)) {
|
||||
callback.Dispose();
|
||||
return CefReturnValue.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
|
||||
}
|
||||
|
||||
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
|
||||
if (request.ResourceType == ResourceType.Script && request.Url.Contains(UrlVendorResource) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)) {
|
||||
return new ResponseFilterVendor(totalBytes);
|
||||
}
|
||||
|
||||
return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Notification.Example {
|
||||
sealed class FormNotificationExample : FormNotificationMain {
|
||||
public override bool RequiresResize => true;
|
||||
protected override bool CanDragWindow => Config.NotificationPosition == DesktopNotification.Position.Custom;
|
||||
|
||||
protected override FormBorderStyle NotificationBorderStyle {
|
||||
get {
|
||||
if (Config.NotificationSize == DesktopNotification.Size.Custom) {
|
||||
switch (base.NotificationBorderStyle) {
|
||||
case FormBorderStyle.FixedSingle: return FormBorderStyle.Sizable;
|
||||
case FormBorderStyle.FixedToolWindow: return FormBorderStyle.SizableToolWindow;
|
||||
}
|
||||
}
|
||||
|
||||
return base.NotificationBorderStyle;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string BodyClasses => base.BodyClasses + " td-example";
|
||||
|
||||
public event EventHandler Ready;
|
||||
|
||||
private readonly DesktopNotification exampleNotification;
|
||||
|
||||
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false) {
|
||||
browser.LoadingStateChanged += browser_LoadingStateChanged;
|
||||
|
||||
string exampleTweetHTML = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "notification/example/example.html"))?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
|
||||
|
||||
#if DEBUG
|
||||
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
|
||||
#endif
|
||||
|
||||
exampleNotification = new DesktopNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
|
||||
if (!e.IsLoading) {
|
||||
Ready?.Invoke(this, EventArgs.Empty);
|
||||
browser.LoadingStateChanged -= browser_LoadingStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public override void HideNotification() {
|
||||
Location = ControlExtensions.InvisibleLocation;
|
||||
}
|
||||
|
||||
public override void FinishCurrentNotification() {}
|
||||
|
||||
public void ShowExampleNotification(bool reset) {
|
||||
if (reset) {
|
||||
LoadTweet(exampleNotification);
|
||||
}
|
||||
else {
|
||||
PrepareAndDisplayWindow();
|
||||
}
|
||||
|
||||
UpdateTitle();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,272 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Browser.Bridge;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Browser {
|
||||
sealed class TweetDeckBrowser : IDisposable {
|
||||
private static UserConfig Config => Program.Config.User;
|
||||
|
||||
private const string NamespaceTweetDeck = "tweetdeck";
|
||||
|
||||
public bool Ready { get; private set; }
|
||||
|
||||
public bool Enabled {
|
||||
get => browser.Enabled;
|
||||
set => browser.Enabled = value;
|
||||
}
|
||||
|
||||
public bool IsTweetDeckWebsite {
|
||||
get {
|
||||
if (!Ready) {
|
||||
return false;
|
||||
}
|
||||
|
||||
using IFrame frame = browser.GetBrowser().MainFrame;
|
||||
return TwitterUrls.IsTweetDeck(frame.Url);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ChromiumWebBrowser browser;
|
||||
private readonly ResourceHandlers resourceHandlers;
|
||||
|
||||
private string prevSoundNotificationPath = null;
|
||||
|
||||
public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge tdBridge, UpdateBridge updateBridge) {
|
||||
var resourceRequestHandler = new ResourceRequestHandlerBrowser();
|
||||
resourceHandlers = resourceRequestHandler.ResourceHandlers;
|
||||
|
||||
resourceHandlers.Register(FormNotificationBase.AppLogo);
|
||||
resourceHandlers.Register(TwitterUtils.LoadingSpinner);
|
||||
|
||||
RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
|
||||
|
||||
this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck) {
|
||||
DialogHandler = new FileDialogHandler(),
|
||||
DragHandler = new DragHandlerBrowser(requestHandler),
|
||||
MenuHandler = new ContextMenuBrowser(owner),
|
||||
JsDialogHandler = new JavaScriptDialogHandler(),
|
||||
KeyboardHandler = new KeyboardHandlerBrowser(owner),
|
||||
LifeSpanHandler = new CustomLifeSpanHandler(),
|
||||
RequestHandler = requestHandler,
|
||||
ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
|
||||
};
|
||||
|
||||
this.browser.LoadingStateChanged += browser_LoadingStateChanged;
|
||||
this.browser.FrameLoadStart += browser_FrameLoadStart;
|
||||
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
|
||||
this.browser.LoadError += browser_LoadError;
|
||||
|
||||
this.browser.RegisterJsBridge("$TD", tdBridge);
|
||||
this.browser.RegisterJsBridge("$TDU", updateBridge);
|
||||
|
||||
this.browser.Dock = DockStyle.None;
|
||||
this.browser.Location = ControlExtensions.InvisibleLocation;
|
||||
this.browser.SetupZoomEvents();
|
||||
|
||||
owner.Controls.Add(browser);
|
||||
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser, TwitterUrls.IsTweetDeck));
|
||||
|
||||
Config.MuteToggled += Config_MuteToggled;
|
||||
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
|
||||
}
|
||||
|
||||
// setup and management
|
||||
|
||||
public void PrepareSize(Size size) {
|
||||
if (!Ready) {
|
||||
browser.Size = size;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowserReady() {
|
||||
if (!Ready) {
|
||||
browser.Location = Point.Empty;
|
||||
browser.Dock = DockStyle.Fill;
|
||||
Ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Focus() {
|
||||
browser.Focus();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Config.MuteToggled -= Config_MuteToggled;
|
||||
Config.SoundNotificationChanged -= Config_SoundNotificationInfoChanged;
|
||||
|
||||
browser.Dispose();
|
||||
}
|
||||
|
||||
// event handlers
|
||||
|
||||
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
|
||||
if (!e.IsLoading) {
|
||||
foreach (string word in TwitterUtils.DictionaryWords) {
|
||||
browser.AddWordToDictionary(word);
|
||||
}
|
||||
|
||||
browser.BeginInvoke(new Action(OnBrowserReady));
|
||||
browser.LoadingStateChanged -= browser_LoadingStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void browser_FrameLoadStart(object sender, FrameLoadStartEventArgs e) {
|
||||
IFrame frame = e.Frame;
|
||||
|
||||
if (frame.IsMain) {
|
||||
string url = frame.Url;
|
||||
|
||||
if (TwitterUrls.IsTweetDeck(url) || (TwitterUrls.IsTwitter(url) && !TwitterUrls.IsTwitterLogin2Factor(url))) {
|
||||
frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorOverride);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
|
||||
IFrame frame = e.Frame;
|
||||
string url = frame.Url;
|
||||
|
||||
if (frame.IsMain) {
|
||||
if (TwitterUrls.IsTweetDeck(url)) {
|
||||
UpdateProperties();
|
||||
CefScriptExecutor.RunBootstrap(frame, NamespaceTweetDeck);
|
||||
|
||||
TweetDeckBridge.ResetStaticProperties();
|
||||
|
||||
if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) {
|
||||
CefScriptExecutor.RunScript(frame, "TD.storage.Account.prototype.requiresConsent = function(){ return false; }", "gen:gdpr");
|
||||
}
|
||||
|
||||
if (Config.FirstRun) {
|
||||
CefScriptExecutor.RunBootstrap(frame, "introduction");
|
||||
}
|
||||
}
|
||||
else if (TwitterUrls.IsTwitter(url)) {
|
||||
CefScriptExecutor.RunBootstrap(frame, "login");
|
||||
}
|
||||
|
||||
CefScriptExecutor.RunBootstrap(frame, "update");
|
||||
}
|
||||
}
|
||||
|
||||
private void browser_LoadError(object sender, LoadErrorEventArgs e) {
|
||||
if (e.ErrorCode == CefErrorCode.Aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.FailedUrl.StartsWith("td://", StringComparison.Ordinal)) {
|
||||
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 Config_MuteToggled(object sender, EventArgs e) {
|
||||
UpdateProperties();
|
||||
}
|
||||
|
||||
private void Config_SoundNotificationInfoChanged(object sender, EventArgs e) {
|
||||
const string soundUrl = "https://ton.twimg.com/tduck/updatesnd";
|
||||
|
||||
bool hasCustomSound = Config.IsCustomSoundNotificationSet;
|
||||
string newNotificationPath = Config.NotificationSoundPath;
|
||||
|
||||
if (prevSoundNotificationPath != newNotificationPath) {
|
||||
prevSoundNotificationPath = newNotificationPath;
|
||||
|
||||
if (hasCustomSound) {
|
||||
resourceHandlers.Register(soundUrl, SoundNotification.CreateFileHandler(newNotificationPath));
|
||||
}
|
||||
else {
|
||||
resourceHandlers.Unregister(soundUrl);
|
||||
}
|
||||
}
|
||||
|
||||
browser.ExecuteJsAsync("TDGF_setSoundNotificationData", hasCustomSound, Config.NotificationSoundVolume);
|
||||
}
|
||||
|
||||
// external handling
|
||||
|
||||
public void HideVideoOverlay(bool focus) {
|
||||
if (focus) {
|
||||
browser.GetBrowser().GetHost().SendFocusEvent(true);
|
||||
}
|
||||
|
||||
browser.ExecuteJsAsync("$('#td-video-player-overlay').remove()");
|
||||
}
|
||||
|
||||
// javascript calls
|
||||
|
||||
public void ReloadToTweetDeck() {
|
||||
browser.ExecuteJsAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUrls.TweetDeck}'");
|
||||
}
|
||||
|
||||
public void OnModulesLoaded(string moduleNamespace) {
|
||||
if (moduleNamespace == NamespaceTweetDeck) {
|
||||
ReinjectCustomCSS(Config.CustomBrowserCSS);
|
||||
Config_SoundNotificationInfoChanged(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateProperties() {
|
||||
browser.ExecuteJsAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Browser));
|
||||
}
|
||||
|
||||
public void ReinjectCustomCSS(string css) {
|
||||
browser.ExecuteJsAsync("TDGF_reinjectCustomCSS", css?.Replace(Environment.NewLine, " ") ?? string.Empty);
|
||||
}
|
||||
|
||||
public void OnMouseClickExtra(IntPtr param) {
|
||||
browser.ExecuteJsAsync("TDGF_onMouseClickExtra", (param.ToInt32() >> 16) & 0xFFFF);
|
||||
}
|
||||
|
||||
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl) {
|
||||
browser.ExecuteJsAsync("TDGF_showTweetDetail", columnId, chirpId, fallbackUrl);
|
||||
}
|
||||
|
||||
public void AddSearchColumn(string query) {
|
||||
browser.ExecuteJsAsync("TDGF_performSearch", query);
|
||||
}
|
||||
|
||||
public void TriggerTweetScreenshot(string columnId, string chirpId) {
|
||||
browser.ExecuteJsAsync("TDGF_triggerScreenshot", columnId, chirpId);
|
||||
}
|
||||
|
||||
public void ReloadColumns() {
|
||||
browser.ExecuteJsAsync("TDGF_reloadColumns()");
|
||||
}
|
||||
|
||||
public void PlaySoundNotification() {
|
||||
browser.ExecuteJsAsync("TDGF_playSoundNotification()");
|
||||
}
|
||||
|
||||
public void ApplyROT13() {
|
||||
browser.ExecuteJsAsync("TDGF_applyROT13()");
|
||||
}
|
||||
|
||||
public void ShowUpdateNotification(string versionTag, string releaseNotes) {
|
||||
browser.ExecuteJsAsync("TDUF_displayNotification", versionTag, Convert.ToBase64String(Encoding.GetEncoding("iso-8859-1").GetBytes(releaseNotes)));
|
||||
}
|
||||
|
||||
public void OpenDevTools() {
|
||||
browser.OpenDevToolsCustom();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,82 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Core.Serialization.Converters;
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Configuration {
|
||||
sealed class ConfigManager : IConfigManager {
|
||||
public UserConfig User { get; }
|
||||
public SystemConfig System { get; }
|
||||
public PluginConfig Plugins { get; }
|
||||
|
||||
public event EventHandler ProgramRestartRequested;
|
||||
|
||||
private readonly FileConfigInstance<UserConfig> infoUser;
|
||||
private readonly FileConfigInstance<SystemConfig> infoSystem;
|
||||
private readonly PluginConfigInstance<PluginConfig> infoPlugins;
|
||||
|
||||
private readonly IConfigInstance<BaseConfig>[] infoList;
|
||||
|
||||
public ConfigManager() {
|
||||
User = new UserConfig(this);
|
||||
System = new SystemConfig(this);
|
||||
Plugins = new PluginConfig(this);
|
||||
|
||||
infoList = new IConfigInstance<BaseConfig>[] {
|
||||
infoUser = new FileConfigInstance<UserConfig>(Program.UserConfigFilePath, User, "program options"),
|
||||
infoSystem = new FileConfigInstance<SystemConfig>(Program.SystemConfigFilePath, System, "system options"),
|
||||
infoPlugins = new PluginConfigInstance<PluginConfig>(Program.PluginConfigFilePath, Plugins)
|
||||
};
|
||||
|
||||
// TODO refactor further
|
||||
|
||||
infoUser.Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
|
||||
|
||||
infoUser.Serializer.RegisterTypeConverter(typeof(Point), new SingleTypeConverter<Point> {
|
||||
ConvertToString = value => $"{value.X} {value.Y}",
|
||||
ConvertToObject = value => {
|
||||
int[] elements = StringUtils.ParseInts(value, ' ');
|
||||
return new Point(elements[0], elements[1]);
|
||||
}
|
||||
});
|
||||
|
||||
infoUser.Serializer.RegisterTypeConverter(typeof(Size), new SingleTypeConverter<Size> {
|
||||
ConvertToString = value => $"{value.Width} {value.Height}",
|
||||
ConvertToObject = value => {
|
||||
int[] elements = StringUtils.ParseInts(value, ' ');
|
||||
return new Size(elements[0], elements[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void LoadAll() {
|
||||
infoUser.Load();
|
||||
infoSystem.Load();
|
||||
infoPlugins.Load();
|
||||
}
|
||||
|
||||
public void SaveAll() {
|
||||
infoUser.Save();
|
||||
infoSystem.Save();
|
||||
infoPlugins.Save();
|
||||
}
|
||||
|
||||
public void ReloadAll() {
|
||||
infoUser.Reload();
|
||||
infoSystem.Reload();
|
||||
infoPlugins.Reload();
|
||||
}
|
||||
|
||||
void IConfigManager.TriggerProgramRestartRequested() {
|
||||
ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance) {
|
||||
Type instanceType = instance.GetType();
|
||||
return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
|
||||
namespace TweetDuck.Configuration {
|
||||
sealed class PluginConfig : BaseConfig, IPluginConfig {
|
||||
private static readonly string[] DefaultDisabled = {
|
||||
"official/clear-columns",
|
||||
"official/reply-account"
|
||||
};
|
||||
|
||||
// CONFIGURATION DATA
|
||||
|
||||
private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
|
||||
|
||||
// EVENTS
|
||||
|
||||
public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
|
||||
|
||||
// END OF CONFIG
|
||||
|
||||
public PluginConfig(IConfigManager configManager) : base(configManager) {}
|
||||
|
||||
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
|
||||
return new PluginConfig(configManager);
|
||||
}
|
||||
|
||||
// INTERFACE IMPLEMENTATION
|
||||
|
||||
IEnumerable<string> IPluginConfig.DisabledPlugins => disabled;
|
||||
|
||||
void IPluginConfig.Reset(IEnumerable<string> newDisabledPlugins) {
|
||||
disabled.Clear();
|
||||
disabled.UnionWith(newDisabledPlugins);
|
||||
}
|
||||
|
||||
public void SetEnabled(Plugin plugin, bool enabled) {
|
||||
if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))) {
|
||||
PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(Plugin plugin) {
|
||||
return !disabled.Contains(plugin.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
|
||||
namespace TweetDuck.Configuration {
|
||||
sealed class SystemConfig : BaseConfig {
|
||||
// CONFIGURATION DATA
|
||||
|
||||
private bool _hardwareAcceleration = true;
|
||||
|
||||
public bool ClearCacheAutomatically { get; set; } = true;
|
||||
public int ClearCacheThreshold { get; set; } = 250;
|
||||
|
||||
// SPECIAL PROPERTIES
|
||||
|
||||
public bool HardwareAcceleration {
|
||||
get => _hardwareAcceleration;
|
||||
set => UpdatePropertyWithRestartRequest(ref _hardwareAcceleration, value);
|
||||
}
|
||||
|
||||
// END OF CONFIG
|
||||
|
||||
public SystemConfig(IConfigManager configManager) : base(configManager) {}
|
||||
|
||||
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
|
||||
return new SystemConfig(configManager);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
|
||||
namespace TweetDuck.Dialogs {
|
||||
sealed partial class FormGuide : Form, FormManager.IAppDialog {
|
||||
private const string GuideUrl = @"td://guide/index.html";
|
||||
|
||||
private static readonly ResourceLink DummyPage = new ResourceLink("http://td/dummy", ResourceHandlers.ForString(string.Empty));
|
||||
|
||||
public static void Show(string hash = null) {
|
||||
string url = GuideUrl + (string.IsNullOrEmpty(hash) ? string.Empty : "#" + hash);
|
||||
FormGuide guide = FormManager.TryFind<FormGuide>();
|
||||
|
||||
if (guide == null) {
|
||||
FormBrowser owner = FormManager.TryFind<FormBrowser>();
|
||||
|
||||
if (owner != null) {
|
||||
new FormGuide(url, owner).Show(owner);
|
||||
}
|
||||
}
|
||||
else {
|
||||
guide.Reload(url);
|
||||
guide.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0069 // Disposable fields should be disposed
|
||||
private readonly ChromiumWebBrowser browser;
|
||||
#pragma warning restore IDE0069 // Disposable fields should be disposed
|
||||
|
||||
private string nextUrl;
|
||||
|
||||
private FormGuide(string url, FormBrowser owner) {
|
||||
InitializeComponent();
|
||||
|
||||
Text = Program.BrandName + " Guide";
|
||||
Size = new Size(owner.Size.Width * 3 / 4, owner.Size.Height * 3 / 4);
|
||||
VisibleChanged += (sender, args) => this.MoveToCenter(owner);
|
||||
|
||||
var resourceRequestHandler = new ResourceRequestHandlerBase();
|
||||
resourceRequestHandler.ResourceHandlers.Register(DummyPage);
|
||||
|
||||
this.browser = new ChromiumWebBrowser(url) {
|
||||
MenuHandler = new ContextMenuGuide(),
|
||||
JsDialogHandler = new JavaScriptDialogHandler(),
|
||||
KeyboardHandler = new KeyboardHandlerBase(),
|
||||
LifeSpanHandler = new CustomLifeSpanHandler(),
|
||||
RequestHandler = new RequestHandlerBase(true),
|
||||
ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
|
||||
};
|
||||
|
||||
browser.LoadingStateChanged += browser_LoadingStateChanged;
|
||||
|
||||
browser.BrowserSettings.BackgroundColor = (uint) BackColor.ToArgb();
|
||||
browser.Dock = DockStyle.None;
|
||||
browser.Location = ControlExtensions.InvisibleLocation;
|
||||
browser.SetupZoomEvents();
|
||||
|
||||
Controls.Add(browser);
|
||||
Disposed += (sender, args) => browser.Dispose();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
if (disposing) {
|
||||
components?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void Reload(string url) {
|
||||
nextUrl = url;
|
||||
browser.LoadingStateChanged += browser_LoadingStateChanged;
|
||||
browser.Dock = DockStyle.None;
|
||||
browser.Location = ControlExtensions.InvisibleLocation;
|
||||
browser.Load(DummyPage.Url);
|
||||
}
|
||||
|
||||
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
|
||||
if (!e.IsLoading) {
|
||||
if (browser.Address == DummyPage.Url) {
|
||||
browser.Load(nextUrl);
|
||||
}
|
||||
else {
|
||||
this.InvokeAsyncSafe(() => {
|
||||
browser.Location = Point.Empty;
|
||||
browser.Dock = DockStyle.Fill;
|
||||
});
|
||||
|
||||
browser.LoadingStateChanged -= browser_LoadingStateChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Browser;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
|
||||
namespace TweetDuck.Plugins {
|
||||
sealed class PluginDispatcher : IPluginDispatcher {
|
||||
public event EventHandler<PluginDispatchEventArgs> Ready;
|
||||
|
||||
private readonly IWebBrowser browser;
|
||||
private readonly IScriptExecutor executor;
|
||||
private readonly Func<string, bool> executeOnUrl;
|
||||
|
||||
public PluginDispatcher(IWebBrowser browser, Func<string, bool> executeOnUrl) {
|
||||
this.executeOnUrl = executeOnUrl;
|
||||
this.browser = browser;
|
||||
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
|
||||
this.executor = new CefScriptExecutor(browser);
|
||||
}
|
||||
|
||||
void IPluginDispatcher.AttachBridge(string name, object bridge) {
|
||||
browser.RegisterJsBridge(name, bridge);
|
||||
}
|
||||
|
||||
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
|
||||
IFrame frame = e.Frame;
|
||||
|
||||
if (frame.IsMain && executeOnUrl(frame.Url)) {
|
||||
Ready?.Invoke(this, new PluginDispatchEventArgs(executor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
|
||||
namespace TweetDuck.Plugins {
|
||||
sealed class PluginSchemeFactory : ISchemeHandlerFactory {
|
||||
public const string Name = PluginSchemeHandler<IResourceHandler>.Name;
|
||||
|
||||
private readonly PluginSchemeHandler<IResourceHandler> handler;
|
||||
|
||||
public PluginSchemeFactory(ResourceProvider resourceProvider) {
|
||||
handler = new PluginSchemeHandler<IResourceHandler>(resourceProvider);
|
||||
}
|
||||
|
||||
internal void Setup(PluginManager plugins) {
|
||||
handler.Setup(plugins);
|
||||
}
|
||||
|
||||
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
|
||||
return handler.Process(request.Url);
|
||||
}
|
||||
}
|
||||
}
|
217
Program.cs
217
Program.cs
@@ -1,217 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Application;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Collections;
|
||||
using TweetLib.Core.Utils;
|
||||
using Win = System.Windows.Forms;
|
||||
|
||||
namespace TweetDuck {
|
||||
static class Program {
|
||||
public const string BrandName = Lib.BrandName;
|
||||
public const string VersionTag = Version.Tag;
|
||||
|
||||
public const string Website = "https://tweetduck.chylex.com";
|
||||
|
||||
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
public static readonly string ExecutablePath = Win.Application.ExecutablePath;
|
||||
|
||||
public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
|
||||
|
||||
public static readonly string ResourcesPath = Path.Combine(ProgramPath, "resources");
|
||||
public static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
||||
public static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
|
||||
|
||||
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataStoragePath();
|
||||
|
||||
public static readonly string PluginDataPath = Path.Combine(StoragePath, "TD_Plugins");
|
||||
public static readonly string InstallerPath = Path.Combine(StoragePath, "TD_Updates");
|
||||
private static readonly string CefDataPath = Path.Combine(StoragePath, "TD_Chromium");
|
||||
|
||||
public static string UserConfigFilePath => Path.Combine(StoragePath, "TD_UserConfig.cfg");
|
||||
public static string SystemConfigFilePath => Path.Combine(StoragePath, "TD_SystemConfig.cfg");
|
||||
public static string PluginConfigFilePath => Path.Combine(StoragePath, "TD_PluginConfig.cfg");
|
||||
|
||||
private static string ErrorLogFilePath => Path.Combine(StoragePath, "TD_Log.txt");
|
||||
private static string ConsoleLogFilePath => Path.Combine(StoragePath, "TD_Console.txt");
|
||||
|
||||
public static uint WindowRestoreMessage;
|
||||
|
||||
private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
|
||||
private static bool hasCleanedUp;
|
||||
|
||||
public static Reporter Reporter { get; }
|
||||
public static ConfigManager Config { get; }
|
||||
|
||||
static Program() {
|
||||
Reporter = new Reporter(ErrorLogFilePath);
|
||||
Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :(");
|
||||
|
||||
Config = new ConfigManager();
|
||||
|
||||
Lib.Initialize(new App.Builder {
|
||||
ErrorHandler = Reporter,
|
||||
SystemHandler = new SystemHandler(),
|
||||
});
|
||||
}
|
||||
|
||||
internal static void SetupWinForms() {
|
||||
Win.Application.EnableVisualStyles();
|
||||
Win.Application.SetCompatibleTextRenderingDefault(false);
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
private static void Main() {
|
||||
SetupWinForms();
|
||||
Cef.EnableHighDPISupport();
|
||||
|
||||
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
|
||||
|
||||
if (!FileUtils.CheckFolderWritePermission(StoragePath)) {
|
||||
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: " + StoragePath, FormMessage.OK);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))) {
|
||||
return;
|
||||
}
|
||||
|
||||
Config.LoadAll();
|
||||
|
||||
if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
|
||||
ProfileManager.ImportCookies();
|
||||
}
|
||||
else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
|
||||
ProfileManager.DeleteCookies();
|
||||
}
|
||||
|
||||
if (Arguments.HasFlag(Arguments.ArgUpdated)) {
|
||||
WindowsUtils.TryDeleteFolderWhenAble(InstallerPath, 8000);
|
||||
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(StoragePath, "Service Worker"), 4000);
|
||||
BrowserCache.TryClearNow();
|
||||
}
|
||||
|
||||
try {
|
||||
ResourceRequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
|
||||
} catch (Exception e) {
|
||||
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Config.User.UseSystemProxyForAllConnections) {
|
||||
WebUtils.EnableSystemProxy();
|
||||
}
|
||||
|
||||
BrowserCache.RefreshTimer();
|
||||
|
||||
CefSharpSettings.WcfEnabled = false;
|
||||
|
||||
CefSettings settings = new CefSettings {
|
||||
UserAgent = BrowserUtils.UserAgentChrome,
|
||||
BrowserSubprocessPath = Path.Combine(ProgramPath, BrandName + ".Browser.exe"),
|
||||
CachePath = StoragePath,
|
||||
UserDataPath = CefDataPath,
|
||||
LogFile = ConsoleLogFilePath,
|
||||
#if !DEBUG
|
||||
LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable
|
||||
#endif
|
||||
};
|
||||
|
||||
var resourceProvider = new ResourceProvider();
|
||||
var resourceScheme = new ResourceSchemeFactory(resourceProvider);
|
||||
var pluginScheme = new PluginSchemeFactory(resourceProvider);
|
||||
|
||||
settings.SetupCustomScheme(ResourceSchemeFactory.Name, resourceScheme);
|
||||
settings.SetupCustomScheme(PluginSchemeFactory.Name, pluginScheme);
|
||||
|
||||
CommandLineArgs.ReadCefArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
|
||||
BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
|
||||
|
||||
Cef.Initialize(settings, false, new BrowserProcessHandler());
|
||||
|
||||
Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
|
||||
|
||||
FormBrowser mainForm = new FormBrowser(resourceProvider, pluginScheme);
|
||||
Win.Application.Run(mainForm);
|
||||
|
||||
if (mainForm.UpdateInstaller != null) {
|
||||
ExitCleanup();
|
||||
|
||||
if (mainForm.UpdateInstaller.Launch()) {
|
||||
Win.Application.Exit();
|
||||
}
|
||||
else {
|
||||
RestartWithArgsInternal(Arguments.GetCurrentClean());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDataStoragePath() {
|
||||
string custom = Arguments.GetValue(Arguments.ArgDataFolder);
|
||||
|
||||
if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))) {
|
||||
if (Path.GetInvalidPathChars().Any(custom.Contains)) {
|
||||
Reporter.HandleEarlyFailure("Data Folder Invalid", "The data folder contains invalid characters:\n" + custom);
|
||||
}
|
||||
else if (!Path.IsPathRooted(custom)) {
|
||||
Reporter.HandleEarlyFailure("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n" + custom);
|
||||
}
|
||||
|
||||
return Environment.ExpandEnvironmentVariables(custom);
|
||||
}
|
||||
else {
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), custom ?? BrandName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Restart(params string[] extraArgs) {
|
||||
CommandLineArgs args = Arguments.GetCurrentClean();
|
||||
CommandLineArgs.ReadStringArray('-', extraArgs, args);
|
||||
RestartWithArgs(args);
|
||||
}
|
||||
|
||||
public static void RestartWithArgs(CommandLineArgs args) {
|
||||
FormBrowser browserForm = FormManager.TryFind<FormBrowser>();
|
||||
|
||||
if (browserForm != null) {
|
||||
browserForm.ForceClose();
|
||||
|
||||
ExitCleanup();
|
||||
RestartWithArgsInternal(args);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestartWithArgsInternal(CommandLineArgs args) {
|
||||
args.AddFlag(Arguments.ArgRestart);
|
||||
Process.Start(ExecutablePath, args.ToString());
|
||||
Win.Application.Exit();
|
||||
}
|
||||
|
||||
private static void ExitCleanup() {
|
||||
if (hasCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
Config.SaveAll();
|
||||
|
||||
Cef.Shutdown();
|
||||
BrowserCache.Exit();
|
||||
|
||||
LockManager.Unlock();
|
||||
hasCleanedUp = true;
|
||||
}
|
||||
}
|
||||
}
|
249
README.md
249
README.md
@@ -1,56 +1,211 @@
|
||||
# Support
|
||||
|
||||
[Follow TweetDuck on Twitter](https://twitter.com/TryMyAwesomeApp) | [Support via Ko-fi](https://ko-fi.com/chylex) | [Support via Patreon](https://www.patreon.com/chylex)
|
||||
|
||||
# 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 Framework 4.7.2 targeting pack
|
||||
* 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.
|
||||
|
||||
### Debug
|
||||
Building TweetDuck for Linux requires [.NET 5](https://docs.microsoft.com/en-us/dotnet/core/install/linux). The Linux project has its own solution file in the `linux/` folder.
|
||||
|
||||
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
|
||||
### Editors
|
||||
|
||||
While debugging, opening the main menu and clicking **Reload browser** automatically rebuilds all resources in `Resources/Scripts` and `Resources/Plugins`. This allows editing HTML/CSS/JS files without restarting the program, but it will cause a short delay between browser reloads.
|
||||
For editing code, I recommend either:
|
||||
|
||||
### Release
|
||||
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) for C# / F# + [VS Code](https://code.visualstudio.com/) for the rest (free when using the Community edition of Visual Studio)
|
||||
* [Rider](https://www.jetbrains.com/rider/) for all languages (paid)
|
||||
|
||||
Open **Batch Build**, tick all `Release` configurations with `x86` platform, and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the [Troubleshooting](#troubleshooting) section.
|
||||
|
||||
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 5.6.1](https://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) with Preprocessor support
|
||||
* [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list)
|
||||
|
||||
Now you can generate installers by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio first!
|
||||
When installing InnoSetup, you can choose to include Inno Script Studio which I recommend for editing and testing installer configuration files in the `bld` folder (`.iss` extension).
|
||||
|
||||
After the window closes, three installers will be generated inside the `bld/Output` folder:
|
||||
Scripts for building installers require the `PATH` environment variable to include the InnoSetup installation folder. You can either edit `PATH` manually, or use a program like [Rapid Environment Editor](https://www.rapidee.com/en/about) to simplify the process. For example, this is the installation folder I added to `PATH` under **User variables**:
|
||||
* `C:\Program Files (x86)\Inno Setup 5`
|
||||
|
||||
You may need to restart Visual Studio after changing `PATH` for the change to take place.
|
||||
|
||||
## Solution Overview
|
||||
|
||||
Open the solution file `TweetDuck.sln` (or `linux/TweetDuck.Linux.sln`) in an IDE, and use the **Restore NuGet Packages** option in your IDE to install dependencies.
|
||||
|
||||
On Windows, TweetDuck uses the [CefSharp](https://github.com/cefsharp/CefSharp/) library for the browser component, and Windows Forms for the GUI.
|
||||
|
||||
On Linux, TweetDuck uses the [ChromiumGtk](https://github.com/lunixo/ChromiumGtk) library, which combines [CefGlue](https://gitlab.com/xiliumhq/chromiumembedded/cefglue) for the browser component and [GtkSharp](https://github.com/GtkSharp/GtkSharp) for the GUI.
|
||||
|
||||
The solution contains several C# projects for executables and libraries, and F# projects for automated tests.
|
||||
|
||||
Projects are organized into folders:
|
||||
* Windows projects are in the `windows/` folder, and target `.NET Framework 4.7.2` + `C# 8.0`
|
||||
* Linux projects are in the `linux/` folder, and target `.NET 5` + `C#`
|
||||
* Libraries (`TweetLib.*`) are in the `lib/` folder, and target `.NET Standard 2.0` + `C# 9.0`
|
||||
* Tests (`TweetTest.*`) are also in the `lib/` folder, and target `.NET Framework 4.7.2` + `F#`
|
||||
|
||||
Here are a few things to keep in mind:
|
||||
* Executable projects have their entry points in `Program.cs`
|
||||
* Library projects targeting `.NET Standard` have their assembly information in `Lib.cs`
|
||||
* All non-test projects include a link to the `Version.cs` file in the root of the repository, which allows changing the version of all executables and library files in one place
|
||||
|
||||
Web resource files (HTML, CSS, JS) are in the `Resources/` folder:
|
||||
* `Resources/Content/` contains all the core features of TweetDuck injected into the browser components
|
||||
* `Resources/Guide/` contains the official TweetDuck guide that opens as a popup
|
||||
* `Resources/Plugins/` contains all official plugins, and a `.debug` plugin for testing
|
||||
|
||||
These resource folders are linked as part of the `TweetLib.Core` project so they can be edited directly within an IDE. Alternatively, you can edit them using [VS Code](https://code.visualstudio.com/) by opening the workspace file `Resources/..code-workspace`.
|
||||
|
||||
### Core Libraries
|
||||
|
||||
#### TweetLib.Core
|
||||
|
||||
This library contains the core TweetDuck application and browser logic. It is built around simple dependency injection that makes it independent of any concrete OS, GUI framework, or browser implementation.
|
||||
|
||||
To simplify porting to other systems, it is not necessary to implement all interfaces, but some functionality will be missing (for ex. if clipboard-related interfaces are not implemented, then context menus will not contain options to copy text or images to clipboard).
|
||||
|
||||
#### TweetLib.Browser
|
||||
|
||||
This library provides a zero-dependency abstraction of browser components and logic. It defines interfaces, events, and container objects that are used by the `TweetLib.Core` library to describe how a browser should behave, while making as few assumptions about the actual browser implementation as possible.
|
||||
|
||||
#### TweetLib.Browser.CEF
|
||||
|
||||
This library is a partial implementation of `TweetLib.Browser` based on [CEF](https://bitbucket.org/chromiumembedded/cef/) interfaces and conventions.
|
||||
|
||||
While `TweetLib.Browser` is highly generic, most browser libraries are likely to be using some form of [CEF](https://bitbucket.org/chromiumembedded/cef/), so this library significantly reduces the amount of work required to swap between browser libraries that are based on [CEF](https://bitbucket.org/chromiumembedded/cef/).
|
||||
|
||||
### Windows Projects
|
||||
|
||||
#### TweetDuck
|
||||
|
||||
Main Windows executable. It has a dependency on [CefSharp](https://github.com/cefsharp/CefSharp/) and Windows Forms. Here you will find the entry point that bootstraps the main application, as well as code for GUIs and Windows-specific functionality.
|
||||
|
||||
#### TweetDuck.Browser
|
||||
|
||||
Windows executable that hosts various Chromium processes. It depends on two specific DLLs from the [CefSharp](https://github.com/cefsharp/CefSharp/) package. After updating [CefSharp](https://github.com/cefsharp/CefSharp/), run the `windows/TweetDuck/Resources/PostCefUpdate.ps1` PowerShell script to update these dependencies to the new version.
|
||||
|
||||
#### TweetDuck.Video
|
||||
|
||||
Windows executable that hosts a video player, which is based on the WMPLib ActiveX component responsible for integrating Windows Media Player into .NET Framework.
|
||||
|
||||
By default, [CefSharp](https://github.com/cefsharp/CefSharp/) is not built with support for H.264 video playback due to software patent nonsense, and even though TweetDuck could be moved entirely to Europe where MPEG LA's patent means nothing, it would require building a custom version of Chromium which requires too many resources. Instead, when a Twitter video played, TweetDuck launches this video player process, which uses Windows Media Player to play H.264 videos.
|
||||
|
||||
#### TweetImpl.CefSharp
|
||||
|
||||
Windows library that implements `TweetLib.Browser.CEF` using the [CefSharp](https://github.com/cefsharp/CefSharp/) library and Windows Forms.
|
||||
|
||||
### Linux Projects
|
||||
|
||||
#### TweetDuck
|
||||
|
||||
Main Linux executable. It has a transitive dependency on [ChromiumGtk](https://github.com/lunixo/ChromiumGtk). Here you will find the entry point that bootstraps the main application, as well as code for GUIs and Linux-specific functionality.
|
||||
|
||||
#### TweetImpl.CefGlue
|
||||
|
||||
Linux library that implements `TweetLib.Browser.CEF` using [ChromiumGtk](https://github.com/lunixo/ChromiumGtk), which is based on [CefGlue](https://gitlab.com/xiliumhq/chromiumembedded/cefglue) and [GtkSharp](https://github.com/GtkSharp/GtkSharp).
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
#### TweetLib.Communication
|
||||
|
||||
This library provides a `DuplexPipe` class for two-way communication between processes.
|
||||
|
||||
#### TweetLib.Utils
|
||||
|
||||
This library contains various utilities that fill some very specific holes in the .NET standard library.
|
||||
|
||||
#### TweetTest.*
|
||||
|
||||
These are F# projects with automated tests.
|
||||
|
||||
# Development (Windows)
|
||||
|
||||
When developing with [Rider](https://www.jetbrains.com/rider/), it must be configured to use MSBuild from Visual Studio, and the `DevEnvDir` property must be set to the full path to the `Common7\IDE` folder which is inside Visual Studio's installation folder. You can set both in **File | Settings | Build, Execution, Deployment | Toolset and Build**:
|
||||
|
||||
1. Click the `MSBuild version` drop-down, and select the path that includes the Visual Studio installation folder.
|
||||
2. Click the Edit button next to `MSBuild global properties`.
|
||||
3. Add a new property named `DevEnvDir`, and set its value to the full path to `Common7\IDE`. For example:
|
||||
- `VS 2019 Community` - `C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE`
|
||||
- `VS 2022 Community` - `C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE`
|
||||
|
||||
## Building
|
||||
|
||||
The `windows/TweetDuck/TweetDuck.csproj` project file has several tasks (targets) that run before and after a build:
|
||||
* `PreBuildEvent` runs a PowerShell script that kills `TweetDuck.Browser` processes, in case they got stuck
|
||||
* `CopyResources` copies resource files into the build folder, and patches and validates them using the `PostBuild.ps1` PowerShell script
|
||||
* `FinalizeDebug` copies a debug plugin (`Resources/Plugins/.debug`) into the build folder (Debug only)
|
||||
* `FinalizeRelease` prepares the build folder for publishing, and if InnoSetup is installed, regenerates the [update installer](#installers-1) (Release only)
|
||||
|
||||
If the build fails, usually with an error like `The command (...) exited with code 1`, open the **Output** tab for detailed logs. A possible cause is the `PostBuild.ps1` script's file validation:
|
||||
* `Resources/Plugins/emoji-keyboard/emoji-ordering.txt` line endings must be LF (line feed); if the file contains any CR (carriage return) characters, the build will fail
|
||||
|
||||
## Debugging
|
||||
|
||||
The `Debug` configuration uses a separate data folder by default (`%LOCALAPPDATA%\TweetDuckDebug`) to avoid affecting an existing installation of TweetDuck. You can modify this by opening **TweetDuck Properties** in Visual Studio, clicking the **Debug** tab, and changing the **Command line arguments** field.
|
||||
|
||||
While debugging, opening the main menu and clicking **Reload browser** automatically applies all changes to HTML/CSS/JS files in the `Resources/` folder. This allows editing and testing resource files without restarting the program, but it will cause a short delay between browser reloads.
|
||||
|
||||
## Release
|
||||
|
||||
Open **Batch Build**, tick all `Release` configurations with `x86` platform, and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the end of the [Building](#building) section.
|
||||
|
||||
If the build succeeds, the `windows/TweetDuck/bin/x86/Release` folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#installers-1) section for automated installer generation.
|
||||
|
||||
If you decide to publicly release a custom version, please change all references to the TweetDuck name, website, and other links such as the issue tracker. The source files contain several constants and references to the official website and this repository, so don't forget to search all files for `chylex.com` and `github.com` in all files and replace them appropriately.
|
||||
|
||||
### Installers
|
||||
|
||||
If you have all the requirements for building [installers](#installers), you can generate them by running `bld/GEN INSTALLERS.bat`. Note that this will only package the files, you still need to create a [release build](#release) in Visual Studio first!
|
||||
|
||||
After the window closes, three installers will be generated inside the `bld/Output/` folder:
|
||||
* **TweetDuck.exe**
|
||||
* 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 +215,28 @@ After the window closes, three installers will be generated inside the `bld/Outp
|
||||
* This is a portable installer that does not need administrator privileges
|
||||
* 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.
|
||||
|
119
Reporter.cs
119
Reporter.cs
@@ -1,119 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Application;
|
||||
|
||||
namespace TweetDuck {
|
||||
sealed class Reporter : IAppErrorHandler {
|
||||
private readonly string logFile;
|
||||
|
||||
public Reporter(string logFile) {
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
public void SetupUnhandledExceptionHandler(string caption) {
|
||||
AppDomain.CurrentDomain.UnhandledException += (sender, args) => {
|
||||
if (args.ExceptionObject is Exception ex) {
|
||||
HandleException(caption, "An unhandled exception has occurred.", false, ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public bool LogVerbose(string data) {
|
||||
return Arguments.HasFlag(Arguments.ArgLogging) && LogImportant(data);
|
||||
}
|
||||
|
||||
public bool LogImportant(string data) {
|
||||
return ((IAppErrorHandler) this).Log(data);
|
||||
}
|
||||
|
||||
bool IAppErrorHandler.Log(string text) {
|
||||
#if DEBUG
|
||||
Debug.WriteLine(text);
|
||||
#endif
|
||||
|
||||
StringBuilder build = new StringBuilder();
|
||||
|
||||
if (!File.Exists(logFile)) {
|
||||
build.Append("Please, report all issues to: https://github.com/chylex/TweetDuck/issues\r\n\r\n");
|
||||
}
|
||||
|
||||
build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("]\r\n");
|
||||
build.Append(text).Append("\r\n\r\n");
|
||||
|
||||
try {
|
||||
File.AppendAllText(logFile, build.ToString(), Encoding.UTF8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleException(string caption, string message, bool canIgnore, Exception e) {
|
||||
bool loggedSuccessfully = LogImportant(e.ToString());
|
||||
|
||||
string exceptionText = e is ExpandedLogException ? e.Message + "\n\nDetails with potentially sensitive information are in the Error Log." : e.Message;
|
||||
FormMessage form = new FormMessage(caption, message + "\nError: " + exceptionText, canIgnore ? MessageBoxIcon.Warning : MessageBoxIcon.Error);
|
||||
|
||||
Button btnExit = form.AddButton(FormMessage.Exit);
|
||||
Button btnIgnore = form.AddButton(FormMessage.Ignore, DialogResult.Ignore, ControlType.Cancel);
|
||||
|
||||
btnIgnore.Enabled = canIgnore;
|
||||
form.ActiveControl = canIgnore ? btnIgnore : btnExit;
|
||||
|
||||
Button btnOpenLog = new Button {
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Left,
|
||||
Enabled = loggedSuccessfully,
|
||||
Font = SystemFonts.MessageBoxFont,
|
||||
Location = new Point(9, 12),
|
||||
Margin = new Padding(0, 0, 48, 0),
|
||||
Size = new Size(106, 26),
|
||||
Text = "Show Error Log",
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
|
||||
btnOpenLog.Click += (sender, args) => {
|
||||
using (Process.Start(logFile)) {}
|
||||
};
|
||||
|
||||
form.AddActionControl(btnOpenLog);
|
||||
|
||||
if (form.ShowDialog() == DialogResult.Ignore) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Process.GetCurrentProcess().Kill();
|
||||
} catch {
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleEarlyFailure(string caption, string message) {
|
||||
Program.SetupWinForms();
|
||||
FormMessage.Error(caption, message, "Exit");
|
||||
|
||||
try {
|
||||
Process.GetCurrentProcess().Kill();
|
||||
} catch {
|
||||
Environment.FailFast(message, new Exception(message));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExpandedLogException : Exception {
|
||||
private readonly string details;
|
||||
|
||||
public ExpandedLogException(Exception source, string details) : base(source.Message, source) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public override string ToString() => base.ToString() + "\r\n" + details;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
enabled(){
|
||||
this.isDebugging = false;
|
||||
|
||||
this.onKeyDown = (e) => {
|
||||
|
||||
// ==========================
|
||||
// F4 key - toggle debug mode
|
||||
// ==========================
|
||||
|
||||
if (e.keyCode === 115){
|
||||
this.isDebugging = !this.isDebugging;
|
||||
$(".nav-user-info").first().css("background-color", this.isDebugging ? "#5A6B75" : "#292F33");
|
||||
}
|
||||
|
||||
else if (this.isDebugging){
|
||||
e.preventDefault();
|
||||
|
||||
// ===================================
|
||||
// N key - simulate popup notification
|
||||
// S key - simulate sound notification
|
||||
// ===================================
|
||||
|
||||
if (e.keyCode === 78 || e.keyCode === 83){
|
||||
let col = TD.controller.columnManager.getAllOrdered()[0];
|
||||
let model = col.model;
|
||||
|
||||
let prevPopup = model.getHasNotification();
|
||||
let prevSound = model.getHasSound();
|
||||
|
||||
model.setHasNotification(e.keyCode === 78);
|
||||
model.setHasSound(e.keyCode === 83);
|
||||
|
||||
$.publish("/notifications/new", [{
|
||||
column: col,
|
||||
items: [
|
||||
col.updateArray[Math.floor(Math.random()*col.updateArray.length)]
|
||||
]
|
||||
}]);
|
||||
|
||||
setTimeout(function(){
|
||||
model.setHasNotification(prevPopup);
|
||||
model.setHasSound(prevSound);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// ========================
|
||||
// D key - trigger debugger
|
||||
// ========================
|
||||
|
||||
else if (e.keyCode === 68){
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ready(){
|
||||
$(document).on("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
disabled(){
|
||||
$(document).off("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
configure(){
|
||||
alert("Configure triggered");
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
run(){
|
||||
console.info("executed debug plugin in notification");
|
||||
}
|
@@ -1,173 +0,0 @@
|
||||
enabled(){
|
||||
const clearColumn = (columnName) => {
|
||||
TD.controller.columnManager.get(columnName).clear();
|
||||
TD.controller.stats.columnActionClick("clear");
|
||||
};
|
||||
|
||||
const resetColumn = (columnName) => {
|
||||
let col = TD.controller.columnManager.get(columnName);
|
||||
col.model.setClearedTimestamp(0);
|
||||
col.reloadTweets();
|
||||
};
|
||||
|
||||
const forEachColumn = (func) => {
|
||||
Object.keys(TD.controller.columnManager.getAll()).forEach(func);
|
||||
};
|
||||
|
||||
let wasShiftPressed = false;
|
||||
|
||||
const updateShiftState = (pressed) => {
|
||||
if (pressed != wasShiftPressed){
|
||||
wasShiftPressed = pressed;
|
||||
|
||||
if (pressed){
|
||||
$(document).on("mousemove", this.eventKeyUp);
|
||||
}
|
||||
else{
|
||||
$(document).off("mousemove", this.eventKeyUp);
|
||||
}
|
||||
|
||||
$(".clear-columns-btn-all").text(pressed ? "Restore columns" : "Clear columns");
|
||||
}
|
||||
};
|
||||
|
||||
// event handlers
|
||||
|
||||
this.eventClickOneCapture = function(e){
|
||||
if (e.target.getAttribute("data-action") === "td-clearcolumns-dosingle"){
|
||||
let name = $(e.target).closest(".js-column").attr("data-column");
|
||||
e.shiftKey ? resetColumn(name) : clearColumn(name);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
this.eventClickAll = function(e){
|
||||
forEachColumn(e.shiftKey ? resetColumn : clearColumn);
|
||||
};
|
||||
|
||||
this.eventKeyDown = function(e){
|
||||
if (!(document.activeElement === null || document.activeElement === document.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateShiftState(e.shiftKey);
|
||||
|
||||
if (e.keyCode === 46){ // 46 = delete
|
||||
if (e.altKey){
|
||||
forEachColumn(e.shiftKey ? resetColumn : clearColumn);
|
||||
}
|
||||
else{
|
||||
let focusedColumn = $(".js-column.is-focused");
|
||||
|
||||
if (focusedColumn.length){
|
||||
let name = focusedColumn.attr("data-column");
|
||||
e.shiftKey ? resetColumn(name) : clearColumn(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.eventKeyUp = function(e){
|
||||
if (!e.shiftKey){
|
||||
updateShiftState(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.eventKeyboardShortcuts = function(e){
|
||||
$(".keyboard-shortcut-list").first().append(`
|
||||
<dd class="keyboard-shortcut-definition" style="white-space:nowrap">
|
||||
<span class="text-like-keyboard-key">1</span> … <span class="text-like-keyboard-key">9</span> + <span class="text-like-keyboard-key">Del</span> Clear column 1-9
|
||||
</dd>
|
||||
<dd class="keyboard-shortcut-definition">
|
||||
<span class="text-like-keyboard-key">Alt</span> + <span class="text-like-keyboard-key">Del</span> Clear all columns
|
||||
</dd>`);
|
||||
};
|
||||
|
||||
// update UI
|
||||
|
||||
this.btnClearAllHTML = `
|
||||
<a class="clear-columns-btn-all-parent js-header-action link-clean cf app-nav-link padding-h--16 padding-v--2" data-title="Clear columns (hold Shift to restore)" data-action="td-clearcolumns-doall">
|
||||
<div class="obj-left margin-l--2"><i class="icon icon-medium icon-clear-timeline"></i></div>
|
||||
<div class="clear-columns-btn-all nbfc padding-ts hide-condensed txt-size--14 app-nav-link-text">Clear columns</div>
|
||||
</a>`;
|
||||
|
||||
this.btnClearOneHTML = `
|
||||
<a class="js-action-header-button column-header-link" href="#" data-action="td-clearcolumns-dosingle">
|
||||
<i class="icon icon-clear-timeline js-show-tip" data-placement="bottom" data-original-title="Clear column (hold Shift to restore)" data-action="td-clearcolumns-dosingle"></i>
|
||||
</a>`;
|
||||
|
||||
this.prevNavMenuMustache = TD.mustaches["menus/column_nav_menu.mustache"];
|
||||
window.TDPF_injectMustache("menus/column_nav_menu.mustache", "replace", "{{_i}}Add column{{/i}}</div> </a> </div>", `{{_i}}Add column{{/i}}</div></a>${this.btnClearAllHTML}</div>`);
|
||||
|
||||
this.prevColumnHeaderMustache = TD.mustaches["column/column_header.mustache"];
|
||||
window.TDPF_injectMustache("column/column_header.mustache", "prepend", "<a data-testid=\"optionsToggle\"", this.btnClearOneHTML);
|
||||
|
||||
if (TD.ready){
|
||||
$(".js-header-add-column").after(this.btnClearAllHTML);
|
||||
$("a[data-testid='optionsToggle']", ".js-column-header").before(this.btnClearOneHTML);
|
||||
}
|
||||
|
||||
// styles
|
||||
|
||||
if (!document.getElementById("td-clearcolumns-workaround")){
|
||||
// TD started caching mustaches so disabling the plugin doesn't update the column headers properly...
|
||||
let workaround = document.createElement("style");
|
||||
workaround.id = "td-clearcolumns-workaround";
|
||||
workaround.innerText = "#tduck a[data-action='td-clearcolumns-dosingle'] { display: none }";
|
||||
document.head.appendChild(workaround);
|
||||
}
|
||||
|
||||
this.css = window.TDPF_createCustomStyle(this);
|
||||
|
||||
this.css.insert(".js-app-add-column.is-hidden + .clear-columns-btn-all-parent { display: none; }");
|
||||
this.css.insert(".column-navigator-overflow .clear-columns-btn-all-parent { display: none !important; }");
|
||||
this.css.insert(".column-navigator-overflow { bottom: 224px !important; }");
|
||||
this.css.insert(".app-navigator .clear-columns-btn-all-parent { font-weight: 700; }");
|
||||
|
||||
this.css.insert(".column-header-links { min-width: 51px !important; }");
|
||||
this.css.insert(".column[data-td-icon='icon-message'] .column-header-links { min-width: 110px !important; }");
|
||||
this.css.insert(".btn-options-tray[data-action='clear'] { display: none !important; }");
|
||||
|
||||
this.css.insert("#tduck a[data-action='td-clearcolumns-dosingle'] { display: inline-block; }");
|
||||
this.css.insert("#tduck .column[data-td-icon='icon-schedule'] a[data-action='td-clearcolumns-dosingle'] { display: none; }");
|
||||
this.css.insert("#tduck .column[data-td-icon='icon-custom-timeline'] a[data-action='td-clearcolumns-dosingle'] { display: none; }");
|
||||
}
|
||||
|
||||
ready(){
|
||||
document.addEventListener("click", this.eventClickOneCapture, true);
|
||||
$(document).on("click", "[data-action='td-clearcolumns-doall']", this.eventClickAll);
|
||||
$(document).on("keydown", this.eventKeyDown);
|
||||
$(document).on("keyup", this.eventKeyUp);
|
||||
$(document).on("uiShowKeyboardShortcutList", this.eventKeyboardShortcuts);
|
||||
|
||||
$(".js-app-add-column").first().after(this.btnClearAllHTML);
|
||||
|
||||
// fix tooltip
|
||||
|
||||
let tooltipEvents = $._data($(".js-header-action")[0], "events");
|
||||
|
||||
if (tooltipEvents.mouseover && tooltipEvents.mouseover.length && tooltipEvents.mouseout && tooltipEvents.mouseout.length){
|
||||
$(".clear-columns-btn-all-parent").on({
|
||||
mouseover: tooltipEvents.mouseover[0].handler,
|
||||
mouseout: tooltipEvents.mouseout[0].handler
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disabled(){
|
||||
this.css.remove();
|
||||
|
||||
document.removeEventListener("click", this.eventClickOneCapture);
|
||||
$(document).off("click", "[data-action='td-clearcolumns-doall']", this.eventClickAll);
|
||||
$(document).off("keydown", this.eventKeyDown);
|
||||
$(document).off("keyup", this.eventKeyUp);
|
||||
$(document).off("uiShowKeyboardShortcutList", this.eventKeyboardShortcuts);
|
||||
|
||||
TD.mustaches["menus/column_nav_menu.mustache"] = this.prevNavMenuMustache;
|
||||
TD.mustaches["column/column_header.mustache"] = this.prevColumnHeaderMustache;
|
||||
|
||||
$("[data-action^='td-clearcolumns-']").remove();
|
||||
}
|
@@ -1,747 +0,0 @@
|
||||
enabled(){
|
||||
// elements & data
|
||||
this.css = null;
|
||||
this.icons = null;
|
||||
this.config = null;
|
||||
|
||||
this.defaultConfig = {
|
||||
_theme: "light",
|
||||
themeOverride: false,
|
||||
columnWidth: "310px",
|
||||
composerWidth: "default",
|
||||
fontSize: "12px",
|
||||
hideTweetActions: true,
|
||||
moveTweetActionsToRight: true,
|
||||
themeColorTweaks: true,
|
||||
revertIcons: true,
|
||||
showCharacterCount: true,
|
||||
forceArialFont: true,
|
||||
increaseQuoteTextSize: false,
|
||||
smallComposeTextSize: false,
|
||||
optimizeAnimations: true,
|
||||
avatarRadius: 2
|
||||
};
|
||||
|
||||
const prepareDefaultConfig = () => {
|
||||
this.defaultConfig._theme = TD.settings.getTheme();
|
||||
|
||||
switch(TD.settings.getColumnWidth()){
|
||||
case "wide": this.defaultConfig.columnWidth = "350px"; break;
|
||||
case "narrow": this.defaultConfig.columnWidth = "270px"; break;
|
||||
}
|
||||
|
||||
switch(TD.settings.getFontSize()){
|
||||
case "small": this.defaultConfig.fontSize = "13px"; break;
|
||||
case "medium": this.defaultConfig.fontSize = "14px"; break;
|
||||
case "large": this.defaultConfig.fontSize = "15px"; break;
|
||||
case "largest": this.defaultConfig.fontSize = "16px"; break;
|
||||
}
|
||||
};
|
||||
|
||||
this.firstTimeLoad = null;
|
||||
|
||||
const me = this;
|
||||
|
||||
// configuration
|
||||
const configFile = "config.json";
|
||||
|
||||
this.tmpConfig = null;
|
||||
this.currentStage = 0;
|
||||
|
||||
this.onStageReady = () => {
|
||||
if (this.currentStage === 0){
|
||||
this.currentStage = 1;
|
||||
}
|
||||
else if (this.tmpConfig !== null){
|
||||
let needsResave = !("_theme" in this.tmpConfig);
|
||||
|
||||
prepareDefaultConfig();
|
||||
this.config = $.extend(this.defaultConfig, this.tmpConfig);
|
||||
this.tmpConfig = null;
|
||||
this.reinjectAll();
|
||||
|
||||
if (this.firstTimeLoad || needsResave){
|
||||
$TDP.writeFile(this.$token, configFile, JSON.stringify(this.config));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfigObject = obj => {
|
||||
this.tmpConfig = obj || {};
|
||||
this.firstTimeLoad = obj === null;
|
||||
|
||||
this.onStageReady();
|
||||
};
|
||||
|
||||
if (TD.ready){
|
||||
this.onStageReady();
|
||||
}
|
||||
else{
|
||||
$(document).one("dataSettingsValues", () => this.onStageReady());
|
||||
}
|
||||
|
||||
$TDP.checkFileExists(this.$token, configFile).then(exists => {
|
||||
if (!exists){
|
||||
loadConfigObject(null);
|
||||
}
|
||||
else{
|
||||
$TDP.readFile(this.$token, configFile, true).then(contents => {
|
||||
try{
|
||||
loadConfigObject(JSON.parse(contents));
|
||||
}catch(err){
|
||||
loadConfigObject(null);
|
||||
}
|
||||
}).catch(err => {
|
||||
loadConfigObject(null);
|
||||
$TD.alert("error", "Problem loading configuration for the design edit plugin: "+err.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.saveConfig = () => {
|
||||
$TDP.writeFile(this.$token, configFile, JSON.stringify(this.config)).catch(err => {
|
||||
$TD.alert("error", "Problem saving configuration for the design edit plugin: "+err.message);
|
||||
});
|
||||
};
|
||||
|
||||
// settings click event
|
||||
this.onSettingsMenuClickedEvent = () => {
|
||||
if (this.config === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
let menu = $(".js-dropdown-content").children("ul").first();
|
||||
if (menu.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let itemTD = menu.children("[data-tweetduck]").first();
|
||||
if (itemTD.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!itemTD.prev().hasClass("drp-h-divider")){
|
||||
itemTD.before('<li class="drp-h-divider"></li>');
|
||||
}
|
||||
|
||||
let itemEditDesign = $('<li class="is-selectable"><a href="#" data-action>Edit layout & design</a></li>');
|
||||
itemEditDesign.insertAfter(itemTD);
|
||||
|
||||
itemEditDesign.on("click", "a", this.configure.bind(this));
|
||||
|
||||
itemEditDesign.hover(function(){
|
||||
$(this).addClass("is-selected");
|
||||
}, function(){
|
||||
$(this).removeClass("is-selected");
|
||||
});
|
||||
}, 2);
|
||||
};
|
||||
|
||||
// modal dialog setup
|
||||
const updateKey = function(key, value){
|
||||
me.config[key] = value;
|
||||
|
||||
setTimeout(function(){
|
||||
me.saveConfig();
|
||||
me.reinjectAll();
|
||||
}, 1); // delays the slight lag caused by saving and reinjection
|
||||
};
|
||||
|
||||
this.customDesignModal = TD.components.BaseModal.extend(function(){
|
||||
let modal = $("#td-design-plugin-modal");
|
||||
this.setAndShowContainer(modal, false);
|
||||
|
||||
// RELOAD
|
||||
this.reloadPage = false;
|
||||
modal.find("[data-td-reload]").click(() => this.reloadPage = true);
|
||||
|
||||
// UI EVENTS
|
||||
let getTextForCustom = function(key){
|
||||
return "Custom ("+me.config[key]+")";
|
||||
};
|
||||
|
||||
modal.find("[data-td-key]").each(function(){
|
||||
let item = $(this);
|
||||
let tag = item.prop("tagName");
|
||||
let key = item.attr("data-td-key");
|
||||
|
||||
// INPUTS
|
||||
if (tag === "INPUT"){
|
||||
let type = item.attr("type");
|
||||
|
||||
if (type === "checkbox"){
|
||||
item.prop("checked", me.config[key]);
|
||||
|
||||
item.change(function(){
|
||||
updateKey(key, item.prop("checked"));
|
||||
});
|
||||
}
|
||||
}
|
||||
// SELECTS
|
||||
else if (tag === "SELECT"){
|
||||
let optionCustom = item.find("option[value^='custom']");
|
||||
let optionCustomNew = item.find("option[value^='change-custom']");
|
||||
|
||||
let resetMyValue = () => {
|
||||
if (!item.val(me.config[key]).val() && optionCustom.length === 1){
|
||||
item.val(optionCustom.attr("value"));
|
||||
optionCustom.text(getTextForCustom(key));
|
||||
optionCustomNew.show();
|
||||
}
|
||||
else{
|
||||
optionCustom.text("Custom");
|
||||
optionCustomNew.hide();
|
||||
}
|
||||
};
|
||||
|
||||
resetMyValue();
|
||||
|
||||
item.change(function(){
|
||||
let val = item.val();
|
||||
|
||||
if (val.endsWith("custom-px")){
|
||||
val = (prompt("Enter custom value (px):") || "").trim();
|
||||
|
||||
if (val){
|
||||
if (val.endsWith("px")){
|
||||
val = val.slice(0, -2).trim();
|
||||
}
|
||||
|
||||
if (/^[0-9]+$/.test(val)){
|
||||
updateKey(key, val+"px");
|
||||
}
|
||||
else{
|
||||
alert("Invalid value, only px values are supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
updateKey(key, item.val());
|
||||
}
|
||||
|
||||
resetMyValue();
|
||||
});
|
||||
}
|
||||
// CUSTOM ELEMENTS
|
||||
else{
|
||||
let value = item.attr("data-td-value");
|
||||
|
||||
if (value == me.config[key]){
|
||||
item.addClass("selected");
|
||||
}
|
||||
|
||||
item.click(function(){
|
||||
modal.find("[data-td-key='"+key+"']").removeClass("selected");
|
||||
item.addClass("selected");
|
||||
updateKey(key, value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// THEMES
|
||||
let selectedTheme = TD.settings.getTheme();
|
||||
|
||||
if (selectedTheme === "dark" && me.config.themeOverride === "black"){
|
||||
selectedTheme = me.config.themeOverride;
|
||||
}
|
||||
|
||||
modal.find("[data-td-theme='"+selectedTheme+"']").prop("checked", true);
|
||||
|
||||
modal.find("[data-td-theme]").change(function(){
|
||||
let theme = $(this).attr("data-td-theme");
|
||||
me.config._theme = theme;
|
||||
|
||||
if (theme === "black"){
|
||||
me.config.themeOverride = theme;
|
||||
theme = "dark";
|
||||
}
|
||||
else{
|
||||
me.config.themeOverride = false;
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
if (theme != TD.settings.getTheme()){
|
||||
TD.settings.setTheme(theme);
|
||||
}
|
||||
|
||||
me.saveConfig();
|
||||
me.reinjectAll();
|
||||
}, 1);
|
||||
});
|
||||
}).methods({
|
||||
_render: function(){
|
||||
return $(me.htmlModal);
|
||||
},
|
||||
destroy: function(){
|
||||
if (this.reloadPage){
|
||||
window.TDPF_requestReload();
|
||||
return;
|
||||
}
|
||||
|
||||
delete me.htmlModal;
|
||||
|
||||
$("#td-design-plugin-modal").hide();
|
||||
this.supr();
|
||||
}
|
||||
});
|
||||
|
||||
// animation optimization
|
||||
this.optimizations = null;
|
||||
this.optimizationTimer = null;
|
||||
|
||||
let clearOptimizationTimer = () => {
|
||||
if (this.optimizationTimer){
|
||||
window.clearTimeout(this.optimizationTimer);
|
||||
this.optimizationTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
let runOptimizationTimer = timeout => {
|
||||
if (!this.optimizationTimer){
|
||||
this.optimizationTimer = window.setTimeout(optimizationTimerFunc, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
let optimizationTimerFunc = () => {
|
||||
this.optimizationTimer = null;
|
||||
|
||||
if (this.config.optimizeAnimations){
|
||||
$TD.getIdleSeconds().then(s => {
|
||||
if (s >= 16){
|
||||
disableOptimizations();
|
||||
runOptimizationTimer(2500);
|
||||
}
|
||||
else{
|
||||
injectOptimizations();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let injectOptimizations = force => {
|
||||
if (!this.optimizations && (force || document.hasFocus())){
|
||||
this.optimizations = window.TDPF_createCustomStyle(this);
|
||||
this.optimizations.insert(".app-content { will-change: transform }");
|
||||
this.optimizations.insert(".column-holder { will-change: transform }");
|
||||
}
|
||||
|
||||
clearOptimizationTimer();
|
||||
runOptimizationTimer(10000);
|
||||
};
|
||||
|
||||
let disableOptimizations = () => {
|
||||
if (this.optimizations){
|
||||
this.optimizations.remove();
|
||||
this.optimizations = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.onWindowFocusEvent = () => {
|
||||
if (this.config && this.config.optimizeAnimations){
|
||||
injectOptimizations(true);
|
||||
}
|
||||
};
|
||||
|
||||
this.onWindowBlurEvent = () => {
|
||||
if (this.config && this.config.optimizeAnimations){
|
||||
disableOptimizations();
|
||||
clearOptimizationTimer();
|
||||
}
|
||||
};
|
||||
|
||||
// css and layout injection
|
||||
this.resetDesign = () => {
|
||||
if (this.css){
|
||||
this.css.remove();
|
||||
}
|
||||
|
||||
this.css = window.TDPF_createCustomStyle(this);
|
||||
|
||||
if (this.theme){
|
||||
this.theme.remove();
|
||||
}
|
||||
|
||||
if (this.config.themeOverride){
|
||||
this.theme = window.TDPF_createCustomStyle(this);
|
||||
}
|
||||
|
||||
if (this.icons){
|
||||
document.head.removeChild(this.icons);
|
||||
this.icons = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.reinjectAll = () => {
|
||||
this.resetDesign();
|
||||
|
||||
clearOptimizationTimer();
|
||||
|
||||
if (this.config.optimizeAnimations){
|
||||
injectOptimizations();
|
||||
}
|
||||
else{
|
||||
disableOptimizations();
|
||||
}
|
||||
|
||||
this.css.insert("#general_settings .cf { display: none !important }");
|
||||
this.css.insert("#settings-modal .js-setting-list li:nth-child(3) { border-bottom: 1px solid #ccd6dd }");
|
||||
|
||||
this.css.insert(`html[data-td-font] { font-size: ${this.config.fontSize} !important }`);
|
||||
this.css.insert(`.avatar { border-radius: ${this.config.avatarRadius}% !important }`);
|
||||
|
||||
if (this.config.composerWidth !== "default"){
|
||||
const width = this.config.composerWidth;
|
||||
this.css.insert(`.js-app-content.is-open { margin-right: ${width} !important; transform: translateX(${width}) !important }`);
|
||||
this.css.insert(`#tduck .js-app-content.tduck-is-opening { margin-right: 0 !important }`);
|
||||
this.css.insert(`.js-drawer { width: ${width} !important; left: -${width} !important }`);
|
||||
}
|
||||
|
||||
let currentTheme = TD.settings.getTheme();
|
||||
|
||||
if (currentTheme === "dark" && this.config.themeOverride){
|
||||
currentTheme = this.config.themeOverride;
|
||||
}
|
||||
|
||||
let notificationScrollbarColor = null;
|
||||
|
||||
if (this.config.themeColorTweaks){
|
||||
switch(currentTheme){
|
||||
case "black":
|
||||
this.css.insert(".app-content, .app-columns-container { background-color: #444448 !important }");
|
||||
this.css.insert(".column-header-temp { background-color: transparent !important }");
|
||||
this.css.insert(".column-drag-handle { opacity: 0.5 !important }");
|
||||
this.css.insert(".column-drag-handle:hover { opacity: 1 !important }");
|
||||
this.css.insert(".column-message.is-actionable span:hover > .icon-small-valigned { filter: saturate(20) }");
|
||||
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-thumb:not(:hover), .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-thumb:not(:hover) { background-color: #666 !important }");
|
||||
notificationScrollbarColor = "666";
|
||||
break;
|
||||
|
||||
case "dark":
|
||||
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-track, .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-track { border-left-color: #14171A !important }");
|
||||
break;
|
||||
|
||||
case "light":
|
||||
this.css.insert(".scroll-styled-v:not(.scroll-alt)::-webkit-scrollbar-thumb:not(:hover), .scroll-styled-h:not(.scroll-alt)::-webkit-scrollbar-thumb:not(:hover) { background-color: #d2d6da !important }");
|
||||
this.css.insert(".app-columns-container.scroll-styled-h::-webkit-scrollbar-thumb:not(:hover) { background-color: #a5aeb5 !important }");
|
||||
notificationScrollbarColor = "a5aeb5";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.showCharacterCount){
|
||||
this.css.insert("#tduck .js-character-count.is-hidden { display: inline !important }");
|
||||
}
|
||||
|
||||
if (this.config.hideTweetActions){
|
||||
this.css.insert(".tweet-action { opacity: 0; }");
|
||||
this.css.insert(".tweet-actions.is-visible .tweet-action { opacity: 0.5 }");
|
||||
this.css.insert(".is-favorite .tweet-action, .is-retweet .tweet-action { opacity: 0.5; visibility: visible !important }");
|
||||
this.css.insert(".tweet:hover .tweet-action, .tweet-action.is-selected, .is-favorite .tweet-action[rel='favorite'], .is-retweet .tweet-action[rel='retweet'] { opacity: 1 !important; visibility: visible !important }");
|
||||
}
|
||||
|
||||
if (this.config.moveTweetActionsToRight){
|
||||
this.css.insert("#tduck .tweet-actions { float: right !important; width: auto !important }");
|
||||
this.css.insert("#tduck .tweet-actions > li:nth-child(4) { margin-right: 2px !important }");
|
||||
}
|
||||
|
||||
if (this.config.forceArialFont){
|
||||
this.css.insert("#tduck { font-family: Arial, sans-serif; font-weight: 400 }");
|
||||
this.css.insert("#tduck input, #tduck label, #tduck select, #tduck textarea { font-family: Arial }")
|
||||
}
|
||||
|
||||
if (this.config.increaseQuoteTextSize){
|
||||
this.css.insert(".quoted-tweet { font-size: 1em !important }");
|
||||
}
|
||||
|
||||
if (this.config.smallComposeTextSize){
|
||||
this.css.insert("#tduck .compose-text { font-size: 12px !important; height: 120px !important }");
|
||||
}
|
||||
|
||||
if (this.config.revertIcons){
|
||||
let iconData = [
|
||||
[ "twitter-bird", "00" ],
|
||||
[ "mention", "01" ],
|
||||
[ "following", "02" ],
|
||||
[ "message", "03" ],
|
||||
[ "home", "04" ],
|
||||
[ "hashtag", "05" ],
|
||||
[ "reply", "06" ],
|
||||
[ "favorite", "55" ],
|
||||
[ "retweet", "08" ],
|
||||
[ "drafts", "09" ],
|
||||
[ "search", "0a" ],
|
||||
[ "trash", "0c" ],
|
||||
[ "close", "0d" ],
|
||||
[ "arrow-r:before,.Icon--caretRight", "0e" ],
|
||||
[ "arrow-l:before,.Icon--caretLeft", "0f" ],
|
||||
[ "protected", "13" ],
|
||||
[ "list", "14" ],
|
||||
[ "camera", "15" ],
|
||||
[ "more", "16" ],
|
||||
[ "settings", "18" ],
|
||||
[ "notifications", "19" ],
|
||||
[ "user-dd", "1a" ],
|
||||
[ "activity", "1c" ],
|
||||
[ "trending", "1d" ],
|
||||
[ "minus", "1e" ],
|
||||
[ "plus", "1f" ],
|
||||
[ "geo", "20" ],
|
||||
[ "check", "21" ],
|
||||
[ "schedule", "22" ],
|
||||
[ "dot", "23" ],
|
||||
[ "user", "24" ],
|
||||
[ "content", "25" ],
|
||||
[ "arrow-d:before,.Icon--caretDown", "26" ],
|
||||
[ "arrow-u", "27" ],
|
||||
[ "share", "28" ],
|
||||
[ "info", "29" ],
|
||||
[ "verified", "2a" ],
|
||||
[ "translator", "2b" ],
|
||||
[ "blocked", "2c" ],
|
||||
[ "constrain", "2d" ],
|
||||
[ "play-video", "2e" ],
|
||||
[ "empty", "2f" ],
|
||||
[ "clear-input", "30" ],
|
||||
[ "compose", "31" ],
|
||||
[ "mark-read", "32" ],
|
||||
[ "arrow-r-double", "33" ],
|
||||
[ "arrow-l-double", "34" ],
|
||||
[ "follow", "35" ],
|
||||
[ "image", "36" ],
|
||||
[ "popout", "37" ],
|
||||
[ "move", "39" ],
|
||||
[ "compose-grid", "3a" ],
|
||||
[ "compose-minigrid", "3b" ],
|
||||
[ "compose-list", "3c" ],
|
||||
[ "edit", "40" ],
|
||||
[ "clear-timeline", "41" ],
|
||||
[ "sliders", "42" ],
|
||||
[ "custom-timeline", "43" ],
|
||||
[ "compose-dm", "44" ],
|
||||
[ "bg-dot", "45" ],
|
||||
[ "user-team-mgr", "46" ],
|
||||
[ "user-switch", "47" ],
|
||||
[ "conversation", "48" ],
|
||||
[ "dataminr", "49" ],
|
||||
[ "link", "4a", ],
|
||||
[ "flash", "50" ],
|
||||
[ "pointer-u", "51" ],
|
||||
[ "analytics", "54" ],
|
||||
[ "heart", "55" ],
|
||||
[ "calendar", "56" ],
|
||||
[ "attachment", "57" ],
|
||||
[ "play", "58" ],
|
||||
[ "bookmark", "59" ],
|
||||
[ "play-badge", "60" ],
|
||||
[ "gif-badge", "61" ],
|
||||
[ "poll", "62" ],
|
||||
|
||||
[ "heart-filled", "55" ],
|
||||
[ "retweet-filled", "08" ],
|
||||
[ "list-filled", "14" ],
|
||||
[ "user-filled", "35" ],
|
||||
];
|
||||
|
||||
this.icons = document.createElement("style");
|
||||
this.icons.innerHTML = `
|
||||
@font-face {
|
||||
font-family: '_of';
|
||||
src: url("https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
${iconData.map(entry => `#tduck .icon-${entry[0]}:before{content:\"\\f0${entry[1]}\";font-family:_of!important}`).join("")}
|
||||
|
||||
.drawer .btn .icon, .app-header .btn .icon { line-height: 1em !important }
|
||||
.app-search-fake .icon { margin-top: -3px !important }
|
||||
#tduck .search-input-control .icon { font-size: 20px !important; top: -4px !important }
|
||||
#tduck .js-docked-compose .js-drawer-close { margin: 20px 0 0 !important }
|
||||
#tduck .compose-media-bar-remove .icon-close, #tduck .compose-media-grid-remove .icon-close { padding: 3px 2px 1px !important }
|
||||
|
||||
.js-column-header .column-type-icon { margin-top: 0 !important }
|
||||
.inline-reply .pull-left .Button--link { margin-top: 3px !important }
|
||||
.js-inline-compose-pop .icon-popout { font-size: 23px !important }
|
||||
|
||||
.tweet-action-item .icon-favorite-toggle { font-size: 16px !important; }
|
||||
.tweet-action-item .heartsprite { top: -260% !important; left: -260% !important; transform: scale(0.4, 0.39) translateY(0.5px) !important; }
|
||||
.tweet-footer { margin-top: 6px !important }`;
|
||||
|
||||
document.head.appendChild(this.icons);
|
||||
}
|
||||
|
||||
if (currentTheme === "black"){
|
||||
$TDP.readFileRoot(this.$token, "theme.black.css").then(contents => {
|
||||
if (this.theme){
|
||||
this.theme.element.innerHTML = contents;
|
||||
TD.settings.setTheme("dark"); // forces refresh of notification head tag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.columnWidth[0] === '/'){
|
||||
let cols = this.config.columnWidth.slice(1);
|
||||
|
||||
this.css.insert(".column { width: calc((100vw - 205px) / "+cols+" - 6px) !important; min-width: 160px }");
|
||||
this.css.insert(".is-condensed .column { width: calc((100vw - 65px) / "+cols+" - 6px) !important }");
|
||||
}
|
||||
else{
|
||||
this.css.insert(".column { width: "+this.config.columnWidth+" !important }");
|
||||
}
|
||||
|
||||
switch(this.config.columnWidth){
|
||||
case "/6":
|
||||
TD.settings.setColumnWidth("narrow");
|
||||
break;
|
||||
|
||||
case "310px":
|
||||
case "/5":
|
||||
TD.settings.setColumnWidth("medium");
|
||||
break;
|
||||
|
||||
default:
|
||||
TD.settings.setColumnWidth(parseInt(this.config.columnWidth, 10) < 310 ? "narrow" : "wide"); // NaN will give "wide"
|
||||
break;
|
||||
}
|
||||
|
||||
switch(this.config.fontSize){
|
||||
case "13px": TD.settings.setFontSize("small"); break;
|
||||
case "14px": TD.settings.setFontSize("medium"); break;
|
||||
case "15px": TD.settings.setFontSize("large"); break;
|
||||
default: TD.settings.setFontSize(parseInt(this.config.fontSize, 10) >= 16 ? "largest" : "smallest"); break;
|
||||
}
|
||||
|
||||
$TDP.injectIntoNotificationsBefore(this.$token, "css", "</head>", `
|
||||
<style type='text/css'>
|
||||
html[data-td-font] { font-size: ${this.config.fontSize} !important }
|
||||
.avatar { border-radius: ${this.config.avatarRadius}% !important }
|
||||
|
||||
${this.config.forceArialFont ? `
|
||||
#tduck { font-family: Arial, sans-serif; font-weight: 400 }
|
||||
` : ``}
|
||||
|
||||
${this.config.increaseQuoteTextSize ? `
|
||||
.quoted-tweet { font-size: 1em !important }
|
||||
` : ``}
|
||||
|
||||
${this.config.revertIcons ? `
|
||||
@font-face { font-family: '_of'; src: url(\"https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff\") format(\"woff\"); font-weight: normal; font-style: normal }
|
||||
#tduck .icon-reply:before{content:"\\f006";font-family:_of!important}
|
||||
#tduck .icon-heart-filled:before{content:"\\f055";font-family:_of!important}
|
||||
#tduck .icon-retweet-filled:before{content:"\\f008";font-family:_of!important}
|
||||
#tduck .icon-list-filled:before{content:"\\f014";font-family:_of!important}
|
||||
#tduck .icon-user-filled:before{content:"\\f035";font-family:_of!important}
|
||||
#tduck .icon-user-dd:before{content:"\\f01a";font-family:_of!important}
|
||||
` : ``}
|
||||
|
||||
${currentTheme === "black" ? `
|
||||
html.dark a, html.dark a:hover, html.dark a:focus, html.dark a:active { color: #8bd }
|
||||
#tduck-show-thread, .other-replies-link { color: #8bd !important }
|
||||
.quoted-tweet { border-color: #292f33 !important }
|
||||
` : ``}
|
||||
|
||||
${notificationScrollbarColor ? `
|
||||
.scroll-styled-v::-webkit-scrollbar-thumb:not(:hover), .scroll-styled-h::-webkit-scrollbar-thumb:not(:hover) { background-color: #${notificationScrollbarColor} !important }
|
||||
` : ``}
|
||||
</style>`);
|
||||
};
|
||||
|
||||
this.uiShowActionsMenuEvent = () => {
|
||||
if (this.config.moveTweetActionsToRight){
|
||||
$(".js-dropdown.pos-r").toggleClass("pos-r pos-l");
|
||||
}
|
||||
};
|
||||
|
||||
this.uiDrawerActiveEvent = (e, data) => {
|
||||
if (data.activeDrawer === null || this.config.composerWidth === "default") {
|
||||
return;
|
||||
}
|
||||
|
||||
const ele = $(".js-app-content").addClass("tduck-is-opening");
|
||||
setTimeout(() => ele.removeClass("tduck-is-opening"), 250);
|
||||
};
|
||||
}
|
||||
|
||||
ready(){
|
||||
// optimization events
|
||||
$(window).on("focus", this.onWindowFocusEvent);
|
||||
$(window).on("blur", this.onWindowBlurEvent);
|
||||
|
||||
// layout events
|
||||
$(document).on("uiShowActionsMenu", this.uiShowActionsMenuEvent);
|
||||
$(document).on("uiDrawerActive", this.uiDrawerActiveEvent);
|
||||
|
||||
// modal
|
||||
$("[data-action='settings-menu']").on("click", this.onSettingsMenuClickedEvent);
|
||||
$(".js-app").append('<div id="td-design-plugin-modal" class="js-modal settings-modal ovl scroll-v scroll-styled-v"></div>');
|
||||
|
||||
// global settings override
|
||||
const me = this;
|
||||
|
||||
this.prevFuncSettingsGetInfo = TD.components.GlobalSettings.prototype.getInfo;
|
||||
this.prevFuncSettingsSwitchTab = TD.components.GlobalSettings.prototype.switchTab;
|
||||
|
||||
TD.components.GlobalSettings.prototype.getInfo = function(){
|
||||
let data = me.prevFuncSettingsGetInfo.apply(this, arguments);
|
||||
|
||||
data.tabs.push({
|
||||
title: "Layout & Design",
|
||||
action: "tdp-edit-design"
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
TD.components.GlobalSettings.prototype.switchTab = function(tab){
|
||||
if (tab === "tdp-edit-design"){
|
||||
me.configure();
|
||||
}
|
||||
else{
|
||||
return me.prevFuncSettingsSwitchTab.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
configure(){
|
||||
$TDP.readFileRoot(this.$token, "modal.html").then(contents => {
|
||||
this.htmlModal = contents;
|
||||
new this.customDesignModal();
|
||||
}).catch(err => {
|
||||
$TD.alert("error", "Error loading the configuration dialog: "+err.message);
|
||||
});
|
||||
}
|
||||
|
||||
disabled(){
|
||||
if (this.css){
|
||||
this.css.remove();
|
||||
}
|
||||
|
||||
if (this.theme){
|
||||
this.theme.remove();
|
||||
}
|
||||
|
||||
if (this.icons){
|
||||
document.head.removeChild(this.icons);
|
||||
}
|
||||
|
||||
if (this.optimizations){
|
||||
this.optimizations.remove();
|
||||
}
|
||||
|
||||
if (this.optimizationTimer){
|
||||
window.clearTimeout(this.optimizationTimer);
|
||||
}
|
||||
|
||||
$(window).off("focus", this.onWindowFocusEvent);
|
||||
$(window).off("blur", this.onWindowBlurEvent);
|
||||
|
||||
$(document).off("uiShowActionsMenu", this.uiShowActionsMenuEvent);
|
||||
$(document).off("uiDrawerActive", this.uiDrawerActiveEvent);
|
||||
|
||||
TD.components.GlobalSettings.prototype.getInfo = this.prevFuncSettingsGetInfo;
|
||||
TD.components.GlobalSettings.prototype.switchTab = this.prevFuncSettingsSwitchTab;
|
||||
|
||||
$("[data-action='settings-menu']").off("click", this.onSettingsMenuClickedEvent);
|
||||
$("#td-design-plugin-modal").remove();
|
||||
}
|
@@ -1,565 +0,0 @@
|
||||
enabled(){
|
||||
this.ENABLE_CUSTOM_KEYBOARD = false;
|
||||
|
||||
this.selectedSkinTone = "";
|
||||
this.currentKeywords = [];
|
||||
|
||||
this.skinToneList = [
|
||||
"", "1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"
|
||||
];
|
||||
|
||||
this.skinToneNonDefaultList = [
|
||||
"1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"
|
||||
];
|
||||
|
||||
this.skinToneData = [
|
||||
[ "", "#FFDD67" ],
|
||||
[ "1F3FB", "#FFE1BD" ],
|
||||
[ "1F3FC", "#FED0AC" ],
|
||||
[ "1F3FD", "#D6A57C" ],
|
||||
[ "1F3FE", "#B47D56" ],
|
||||
[ "1F3FF", "#8A6859" ],
|
||||
];
|
||||
|
||||
this.emojiData1 = []; // no skin tones, prepended
|
||||
this.emojiData2 = {}; // contains emojis with skin tones
|
||||
this.emojiData3 = []; // no skin tones, appended
|
||||
this.emojiNames = [];
|
||||
|
||||
const me = this;
|
||||
|
||||
// styles
|
||||
|
||||
this.css = window.TDPF_createCustomStyle(this);
|
||||
this.css.insert(".emoji-keyboard { position: absolute; width: 15.35em; background-color: white; border-radius: 1px; font-size: 24px; z-index: 9999 }");
|
||||
this.css.insert(".emoji-keyboard-popup-btn { height: 36px !important }");
|
||||
this.css.insert(".emoji-keyboard-popup-btn .icon { vertical-align: -4px !important }");
|
||||
|
||||
this.css.insert(".emoji-keyboard-list { height: 10.14em; padding: 0.1em; box-sizing: border-box; overflow-y: auto }");
|
||||
this.css.insert(".emoji-keyboard-list .separator { height: 26px }");
|
||||
this.css.insert(".emoji-keyboard-list img { padding: 0.1em !important; width: 1em; height: 1em; vertical-align: -0.1em; cursor: pointer }");
|
||||
|
||||
this.css.insert(".emoji-keyboard-search { height: auto; padding: 4px 10px 8px; background-color: #292f33; border-radius: 1px 1px 0 0 }");
|
||||
this.css.insert(".emoji-keyboard-search input { width: 100%; border-radius: 1px; }");
|
||||
|
||||
this.css.insert(".emoji-keyboard-skintones { height: 1.3em; text-align: center; background-color: #292f33; border-radius: 0 0 1px 1px }");
|
||||
this.css.insert(".emoji-keyboard-skintones div { width: 0.8em; height: 0.8em; margin: 0.25em 0.1em; border-radius: 50%; display: inline-block; box-sizing: border-box; cursor: pointer }");
|
||||
this.css.insert(".emoji-keyboard-skintones .sel { border: 2px solid rgba(0, 0, 0, 0.35); box-shadow: 0 0 2px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.4) inset }");
|
||||
this.css.insert(".emoji-keyboard-skintones :hover { border: 2px solid rgba(0, 0, 0, 0.25); box-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.25) inset }");
|
||||
|
||||
this.css.insert(".js-compose-text { font-family: \"Twitter Color Emoji\", Helvetica, Arial, Verdana, sans-serif; }");
|
||||
|
||||
// layout
|
||||
|
||||
let buttonHTML = '<button class="needsclick btn btn-on-blue txt-left padding-v--6 padding-h--8 emoji-keyboard-popup-btn"><i class="icon icon-heart"></i></button>';
|
||||
|
||||
this.prevComposeMustache = TD.mustaches["compose/docked_compose.mustache"];
|
||||
window.TDPF_injectMustache("compose/docked_compose.mustache", "append", '<div class="cf margin-t--12 margin-b--30">', buttonHTML);
|
||||
|
||||
this.getDrawerInput = () => {
|
||||
return $(".js-compose-text", me.composeDrawer);
|
||||
};
|
||||
|
||||
this.getDrawerScroller = () => {
|
||||
return $(".js-compose-scroller > .scroll-v", me.composeDrawer);
|
||||
};
|
||||
|
||||
// keyboard generation
|
||||
|
||||
this.currentKeyboard = null;
|
||||
this.currentSpanner = null;
|
||||
|
||||
let wasSearchFocused = false;
|
||||
let lastEmojiKeyword, lastEmojiPosition, lastEmojiLength;
|
||||
|
||||
const hideKeyboard = (refocus) => {
|
||||
$(this.currentKeyboard).remove();
|
||||
this.currentKeyboard = null;
|
||||
|
||||
$(this.currentSpanner).remove();
|
||||
this.currentSpanner = null;
|
||||
|
||||
this.currentKeywords = [];
|
||||
|
||||
this.getDrawerScroller().trigger("scroll");
|
||||
|
||||
$(".emoji-keyboard-popup-btn").removeClass("is-selected");
|
||||
|
||||
if (refocus){
|
||||
this.getDrawerInput().focus();
|
||||
|
||||
if (lastEmojiKeyword && lastEmojiPosition === 0){
|
||||
document.execCommand("insertText", false, lastEmojiKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
lastEmojiKeyword = null;
|
||||
};
|
||||
|
||||
const generateEmojiHTML = skinTone => {
|
||||
let index = 0;
|
||||
let html = [ "<p style='font-size:13px;color:#444;margin:4px;text-align:center'>Please, note that some emoji may not show up correctly in the text box above, but they will display in the tweet.</p>" ];
|
||||
|
||||
for(let array of [ this.emojiData1, this.emojiData2[skinTone], this.emojiData3 ]){
|
||||
for(let emoji of array){
|
||||
if (emoji === "___"){
|
||||
html.push("<div class='separator'></div>");
|
||||
}
|
||||
else{
|
||||
html.push(TD.util.cleanWithEmoji(emoji).replace(' class="emoji" draggable="false"', ''));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return html.join("");
|
||||
};
|
||||
|
||||
const updateFilters = () => {
|
||||
let keywords = this.currentKeywords;
|
||||
let container = $(this.currentKeyboard.children[1]);
|
||||
|
||||
let emoji = container.children("img");
|
||||
let info = container.children("p:first");
|
||||
let separators = container.children("div");
|
||||
|
||||
if (keywords.length === 0){
|
||||
info.css("display", "block");
|
||||
separators.css("display", "block");
|
||||
emoji.css("display", "inline");
|
||||
}
|
||||
else{
|
||||
info.css("display", "none");
|
||||
separators.css("display", "none");
|
||||
|
||||
emoji.css("display", "none");
|
||||
emoji.filter(index => keywords.every(kw => me.emojiNames[index].includes(kw))).css("display", "inline");
|
||||
}
|
||||
};
|
||||
|
||||
const selectSkinTone = skinTone => {
|
||||
let selectedEle = this.currentKeyboard.children[2].querySelector("[data-tone='"+this.selectedSkinTone+"']");
|
||||
selectedEle && selectedEle.classList.remove("sel");
|
||||
|
||||
this.selectedSkinTone = skinTone;
|
||||
this.currentKeyboard.children[1].innerHTML = generateEmojiHTML(skinTone);
|
||||
this.currentKeyboard.children[2].querySelector("[data-tone='"+this.selectedSkinTone+"']").classList.add("sel");
|
||||
updateFilters();
|
||||
};
|
||||
|
||||
this.generateKeyboard = (left, top) => {
|
||||
let outer = document.createElement("div");
|
||||
outer.classList.add("emoji-keyboard");
|
||||
outer.style.left = left+"px";
|
||||
outer.style.top = top+"px";
|
||||
|
||||
let keyboard = document.createElement("div");
|
||||
keyboard.classList.add("emoji-keyboard-list");
|
||||
|
||||
keyboard.addEventListener("click", function(e){
|
||||
let ele = e.target;
|
||||
|
||||
if (ele.tagName === "IMG"){
|
||||
insertEmoji(ele.getAttribute("src"), ele.getAttribute("alt"));
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
let search = document.createElement("div");
|
||||
search.innerHTML = "<input type='text' placeholder='Search...'>";
|
||||
search.classList.add("emoji-keyboard-search");
|
||||
|
||||
let skintones = document.createElement("div");
|
||||
skintones.innerHTML = me.skinToneData.map(entry => "<div data-tone='"+entry[0]+"' style='background-color:"+entry[1]+"'></div>").join("");
|
||||
skintones.classList.add("emoji-keyboard-skintones");
|
||||
|
||||
outer.appendChild(search);
|
||||
outer.appendChild(keyboard);
|
||||
outer.appendChild(skintones);
|
||||
$(".js-app").append(outer);
|
||||
|
||||
skintones.addEventListener("click", function(e){
|
||||
if (e.target.hasAttribute("data-tone")){
|
||||
selectSkinTone(e.target.getAttribute("data-tone") || "");
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
search.addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
let searchInput = search.children[0];
|
||||
searchInput.focus();
|
||||
|
||||
wasSearchFocused = false;
|
||||
|
||||
searchInput.addEventListener("input", function(e){
|
||||
me.currentKeywords = e.target.value.split(" ").filter(kw => kw.length > 0).map(kw => kw.toLowerCase());
|
||||
updateFilters();
|
||||
|
||||
wasSearchFocused = $(this).val().length > 0;
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keydown", function(e){
|
||||
if (e.keyCode === 13 && $(this).val().length){ // enter
|
||||
let ele = $(".emoji-keyboard-list").children("img").filter(":visible").first();
|
||||
|
||||
if (ele.length > 0){
|
||||
insertEmoji(ele[0].getAttribute("src"), ele[0].getAttribute("alt"));
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("click", function(){
|
||||
$(this).select();
|
||||
});
|
||||
|
||||
searchInput.addEventListener("focus", function(){
|
||||
wasSearchFocused = true;
|
||||
});
|
||||
|
||||
this.currentKeyboard = outer;
|
||||
selectSkinTone(this.selectedSkinTone);
|
||||
|
||||
this.currentSpanner = document.createElement("div");
|
||||
this.currentSpanner.style.height = ($(this.currentKeyboard).height()-10)+"px";
|
||||
$(".emoji-keyboard-popup-btn").parent().after(this.currentSpanner);
|
||||
|
||||
this.getDrawerScroller().trigger("scroll");
|
||||
};
|
||||
|
||||
const getKeyboardTop = () => {
|
||||
let button = $(".emoji-keyboard-popup-btn");
|
||||
return button.offset().top + button.outerHeight() + me.getDrawerScroller().scrollTop() + 8;
|
||||
};
|
||||
|
||||
const insertEmoji = (src, alt) => {
|
||||
let input = this.getDrawerInput();
|
||||
|
||||
let val = input.val();
|
||||
let posStart = input[0].selectionStart;
|
||||
let posEnd = input[0].selectionEnd;
|
||||
|
||||
input.val(val.slice(0, posStart)+alt+val.slice(posEnd));
|
||||
input.trigger("change");
|
||||
|
||||
input[0].selectionStart = posStart+alt.length;
|
||||
input[0].selectionEnd = posStart+alt.length;
|
||||
|
||||
lastEmojiKeyword = null;
|
||||
|
||||
if (wasSearchFocused){
|
||||
$(".emoji-keyboard-search").children("input").focus();
|
||||
}
|
||||
else{
|
||||
input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// general event handlers
|
||||
|
||||
this.emojiKeyboardButtonClickEvent = function(e){
|
||||
if (me.currentKeyboard){
|
||||
$(this).blur();
|
||||
hideKeyboard(true);
|
||||
}
|
||||
else{
|
||||
me.generateKeyboard($(this).offset().left, getKeyboardTop());
|
||||
$(this).addClass("is-selected");
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
this.composerScrollEvent = function(e){
|
||||
if (me.currentKeyboard){
|
||||
me.currentKeyboard.style.marginTop = (-$(this).scrollTop())+"px";
|
||||
}
|
||||
};
|
||||
|
||||
this.composeInputKeyDownEvent = function(e){
|
||||
if (lastEmojiKeyword && (e.keyCode === 8 || e.keyCode === 27)){ // backspace, escape
|
||||
let ele = $(this)[0];
|
||||
|
||||
if (ele.selectionStart === lastEmojiPosition){
|
||||
ele.selectionStart -= lastEmojiLength; // selects the emoji
|
||||
document.execCommand("insertText", false, lastEmojiKeyword);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
lastEmojiKeyword = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.composeInputKeyPressEvent = function(e){
|
||||
if (String.fromCharCode(e.which) === ':'){
|
||||
let ele = $(this);
|
||||
let val = ele.val();
|
||||
|
||||
let firstColon = val.lastIndexOf(':', ele[0].selectionStart);
|
||||
if (firstColon === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let search = val.substring(firstColon+1, ele[0].selectionStart).toLowerCase();
|
||||
if (!/^[a-z_]+$/.test(search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let keywords = search.split("_").filter(kw => kw.length > 0).map(kw => kw.toLowerCase());
|
||||
if (keywords.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let foundNames = me.emojiNames.filter(name => keywords.every(kw => name.includes(kw)));
|
||||
|
||||
if (foundNames.length === 0){
|
||||
return;
|
||||
}
|
||||
else if (foundNames.length > 1 && foundNames.includes(search)){
|
||||
foundNames = [ search ];
|
||||
}
|
||||
|
||||
lastEmojiKeyword = `:${search}:`;
|
||||
lastEmojiPosition = lastEmojiLength = 0;
|
||||
|
||||
if (foundNames.length === 1){
|
||||
let foundIndex = me.emojiNames.indexOf(foundNames[0]);
|
||||
let foundEmoji;
|
||||
|
||||
for(let array of [ me.emojiData1, me.emojiData2[me.selectedSkinTone], me.emojiData3 ]){
|
||||
let realArray = array.filter(ele => ele !== "___");
|
||||
|
||||
if (foundIndex >= realArray.length){
|
||||
foundIndex -= realArray.length;
|
||||
}
|
||||
else{
|
||||
foundEmoji = realArray[foundIndex];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundEmoji){
|
||||
e.preventDefault();
|
||||
|
||||
ele.val(val.substring(0, firstColon)+foundEmoji+val.substring(ele[0].selectionStart));
|
||||
ele[0].selectionEnd = ele[0].selectionStart = firstColon+foundEmoji.length;
|
||||
ele.trigger("change");
|
||||
ele.focus();
|
||||
|
||||
lastEmojiPosition = firstColon+foundEmoji.length;
|
||||
lastEmojiLength = foundEmoji.length;
|
||||
}
|
||||
}
|
||||
else if (foundNames.length > 1 && $(".js-app-content").is(".is-open")){
|
||||
e.preventDefault();
|
||||
ele.val(val.substring(0, firstColon)+val.substring(ele[0].selectionStart));
|
||||
ele[0].selectionEnd = ele[0].selectionStart = firstColon;
|
||||
ele.trigger("change");
|
||||
|
||||
if (!me.currentKeyboard){
|
||||
$(".emoji-keyboard-popup-btn").click();
|
||||
}
|
||||
|
||||
$(".emoji-keyboard-search").children("input").focus().val("");
|
||||
document.execCommand("insertText", false, keywords.join(" "));
|
||||
}
|
||||
}
|
||||
else{
|
||||
lastEmojiKeyword = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.composeInputFocusEvent = function(e){
|
||||
wasSearchFocused = false;
|
||||
};
|
||||
|
||||
this.composerSendingEvent = function(e){
|
||||
hideKeyboard();
|
||||
};
|
||||
|
||||
this.composerActiveEvent = function(e){
|
||||
$(".emoji-keyboard-popup-btn", me.composeDrawer).on("click", me.emojiKeyboardButtonClickEvent);
|
||||
$(".js-docked-compose .js-compose-scroller > .scroll-v", me.composeDrawer).on("scroll", me.composerScrollEvent);
|
||||
};
|
||||
|
||||
this.documentClickEvent = function(e){
|
||||
if (me.currentKeyboard && $(e.target).closest(".compose-text-container").length === 0){
|
||||
hideKeyboard();
|
||||
}
|
||||
};
|
||||
|
||||
this.documentKeyEvent = function(e){
|
||||
if (me.currentKeyboard && e.keyCode === 27){ // escape
|
||||
hideKeyboard(true);
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
this.uploadFilesEvent = function(e){
|
||||
if (me.currentKeyboard){
|
||||
me.currentKeyboard.style.top = getKeyboardTop()+"px";
|
||||
}
|
||||
};
|
||||
|
||||
// re-enabling
|
||||
|
||||
let maybeDockedComposePanel = $(".js-docked-compose");
|
||||
|
||||
if (maybeDockedComposePanel.length){
|
||||
maybeDockedComposePanel.find(".cf.margin-t--12.margin-b--30").first().append(buttonHTML);
|
||||
this.composerActiveEvent();
|
||||
}
|
||||
}
|
||||
|
||||
ready(){
|
||||
this.composeDrawer = $(".js-drawer[data-drawer='compose']");
|
||||
this.composeSelector = ".js-compose-text,.js-reply-tweetbox";
|
||||
|
||||
$(document).on("click", this.documentClickEvent);
|
||||
$(document).on("keydown", this.documentKeyEvent);
|
||||
$(document).on("tduckOldComposerActive", this.composerActiveEvent);
|
||||
$(document).on("uiComposeImageAdded", this.uploadFilesEvent);
|
||||
|
||||
this.composeDrawer.on("uiComposeTweetSending", this.composerSendingEvent);
|
||||
|
||||
$(document).on("keydown", this.composeSelector, this.composeInputKeyDownEvent);
|
||||
$(document).on("keypress", this.composeSelector, this.composeInputKeyPressEvent);
|
||||
$(document).on("focus", this.composeSelector, this.composeInputFocusEvent);
|
||||
|
||||
// HTML generation
|
||||
|
||||
const convUnicode = function(codePt){
|
||||
if (codePt > 0xFFFF){
|
||||
codePt -= 0x10000;
|
||||
return String.fromCharCode(0xD800+(codePt>>10), 0xDC00+(codePt&0x3FF));
|
||||
}
|
||||
else{
|
||||
return String.fromCharCode(codePt);
|
||||
}
|
||||
};
|
||||
|
||||
$TDP.readFileRoot(this.$token, "emoji-ordering.txt").then(contents => {
|
||||
for(let skinTone of this.skinToneList){
|
||||
this.emojiData2[skinTone] = [];
|
||||
}
|
||||
|
||||
// declaration inserters
|
||||
|
||||
let mapUnicode = pt => convUnicode(parseInt(pt, 16));
|
||||
|
||||
let addDeclaration1 = decl => {
|
||||
this.emojiData1.push(decl.split(" ").map(mapUnicode).join(""));
|
||||
};
|
||||
|
||||
let addDeclaration2 = (tone, decl) => {
|
||||
let gen = decl.split(" ").map(mapUnicode).join("");
|
||||
|
||||
if (tone === null){
|
||||
for(let skinTone of this.skinToneList){
|
||||
this.emojiData2[skinTone].push(gen);
|
||||
}
|
||||
}
|
||||
else{
|
||||
this.emojiData2[tone].push(gen);
|
||||
}
|
||||
};
|
||||
|
||||
let addDeclaration3 = decl => {
|
||||
this.emojiData3.push(decl.split(" ").map(mapUnicode).join(""));
|
||||
};
|
||||
|
||||
// line reading
|
||||
|
||||
let skinToneState = 0;
|
||||
|
||||
for(let line of contents.split("\n")){
|
||||
if (line[0] === '@'){
|
||||
switch(skinToneState){
|
||||
case 0: this.emojiData1.push("___"); break;
|
||||
case 1: this.skinToneList.forEach(skinTone => this.emojiData2[skinTone].push("___")); break;
|
||||
case 2: this.emojiData3.push("___"); break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (line[0] === '#'){
|
||||
if (line[1] === '1'){
|
||||
skinToneState = 1;
|
||||
}
|
||||
else if (line[1] === '2'){
|
||||
skinToneState = 2;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let semicolon = line.indexOf(';');
|
||||
let decl = line.slice(0, semicolon);
|
||||
let desc = line.slice(semicolon+1).toLowerCase();
|
||||
|
||||
if (skinToneState === 1){
|
||||
let skinIndex = decl.indexOf('$');
|
||||
|
||||
if (skinIndex !== -1){
|
||||
let declPre = decl.slice(0, skinIndex);
|
||||
let declPost = decl.slice(skinIndex+1);
|
||||
|
||||
for(let skinTone of this.skinToneNonDefaultList){
|
||||
this.emojiData2[skinTone].pop();
|
||||
addDeclaration2(skinTone, declPre+skinTone+declPost);
|
||||
}
|
||||
}
|
||||
else{
|
||||
addDeclaration2(null, decl);
|
||||
this.emojiNames.push(desc);
|
||||
}
|
||||
}
|
||||
else if (skinToneState === 2){
|
||||
addDeclaration3(decl);
|
||||
this.emojiNames.push(desc);
|
||||
}
|
||||
else if (skinToneState === 0){
|
||||
addDeclaration1(decl);
|
||||
this.emojiNames.push(desc);
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
$TD.alert("error", "Problem loading emoji keyboard: "+err.message);
|
||||
});
|
||||
}
|
||||
|
||||
disabled(){
|
||||
this.css.remove();
|
||||
|
||||
if (this.currentKeyboard){
|
||||
$(this.currentKeyboard).remove();
|
||||
}
|
||||
|
||||
if (this.currentSpanner){
|
||||
$(this.currentSpanner).remove();
|
||||
}
|
||||
|
||||
$(".emoji-keyboard-popup-btn").remove();
|
||||
|
||||
$(document).off("click", this.documentClickEvent);
|
||||
$(document).off("keydown", this.documentKeyEvent);
|
||||
$(document).off("tduckOldComposerActive", this.composerActiveEvent);
|
||||
$(document).off("uiComposeImageAdded", this.uploadFilesEvent);
|
||||
|
||||
this.composeDrawer.off("uiComposeTweetSending", this.composerSendingEvent);
|
||||
|
||||
$(document).off("keydown", this.composeSelector, this.composeInputKeyDownEvent);
|
||||
$(document).off("keypress", this.composeSelector, this.composeInputKeyPressEvent);
|
||||
$(document).off("focus", this.composeSelector, this.composeInputFocusEvent);
|
||||
|
||||
TD.mustaches["compose/docked_compose.mustache"] = this.prevComposeMustache;
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
enabled(){
|
||||
let configuration = { defaultAccount: "#preferred" };
|
||||
|
||||
window.TDPF_loadConfigurationFile(this, "configuration.js", "configuration.default.js", obj => configuration = obj);
|
||||
|
||||
this.lastSelectedAccount = null;
|
||||
|
||||
this.uiComposeTweetEvent = (e, data) => {
|
||||
if (!(data.type === "reply" || (data.type === "tweet" && "quotedTweet" in data)) || data.popFromInline || !("element" in data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let query;
|
||||
|
||||
if (configuration.useAdvancedSelector){
|
||||
if (configuration.customSelector){
|
||||
let customSelectorDef = configuration.customSelector.toString();
|
||||
|
||||
if (customSelectorDef.startsWith("function (column){")){
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: customSelector needs to be updated due to TweetDeck changes, please read the default configuration file for the updated guide");
|
||||
return;
|
||||
}
|
||||
else if (customSelectorDef.startsWith("function (type,")){
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: the type parameter is no longer present due to TweetDeck changes, please read the default configuration file for the updated guide");
|
||||
return;
|
||||
}
|
||||
|
||||
let section = data.element.closest("section.js-column");
|
||||
let column = TD.controller.columnManager.get(section.attr("data-column"));
|
||||
|
||||
let feeds = column.getFeeds();
|
||||
let accountText = "";
|
||||
|
||||
if (feeds.length === 1){
|
||||
let metadata = feeds[0].getMetadata();
|
||||
let id = metadata.ownerId || metadata.id;
|
||||
|
||||
if (id){
|
||||
accountText = TD.cache.names.getScreenName(id);
|
||||
}
|
||||
else{
|
||||
let account = TD.storage.accountController.get(feeds[0].getAccountKey());
|
||||
|
||||
if (account){
|
||||
accountText = "@"+account.getUsername();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let header = $(".column-header-title", section);
|
||||
let title = header.children(".column-heading");
|
||||
let titleText = title.length ? title.text() : header.children(".column-title-edit-box").val();
|
||||
|
||||
try{
|
||||
query = configuration.customSelector(titleText, accountText, column, section.hasClass("column-temp"));
|
||||
}catch(e){
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: customSelector threw an error: "+e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else{
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: useAdvancedSelector is true, but customSelector function is missing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else{
|
||||
query = configuration.defaultAccount;
|
||||
|
||||
if (query === ""){
|
||||
query = "#preferred";
|
||||
}
|
||||
else if (typeof query !== "string"){
|
||||
query = "#default";
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof query === "undefined"){
|
||||
query = "#preferred";
|
||||
}
|
||||
|
||||
if (typeof query !== "string"){
|
||||
return;
|
||||
}
|
||||
else if (query.length === 0){
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: the requested account is empty");
|
||||
return;
|
||||
}
|
||||
else if (query[0] !== '@' && query[0] !== '#'){
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: the requested account does not begin with @ or #: "+query);
|
||||
return;
|
||||
}
|
||||
|
||||
let identifier = null;
|
||||
|
||||
switch(query){
|
||||
case "#preferred":
|
||||
identifier = TD.storage.clientController.client.getDefaultAccount();
|
||||
break;
|
||||
|
||||
case "#last":
|
||||
if (this.lastSelectedAccount === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
identifier = this.lastSelectedAccount;
|
||||
break;
|
||||
|
||||
case "#default":
|
||||
return;
|
||||
|
||||
default:
|
||||
if (query[0] === '@'){
|
||||
let obj = TD.storage.accountController.getAccountFromUsername(query.substring(1));
|
||||
|
||||
if (obj.length === 0){
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: requested account not found: "+query);
|
||||
return;
|
||||
}
|
||||
else{
|
||||
identifier = obj[0].privateState.key;
|
||||
}
|
||||
}
|
||||
else{
|
||||
$TD.alert("warning", "Plugin reply-account has invalid configuration: unknown requested account query: "+query);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
data.singleFrom = data.from = [ identifier ];
|
||||
};
|
||||
|
||||
this.onSelectedAccountChanged = () => {
|
||||
let selected = $(".js-account-item.is-selected", ".js-account-list");
|
||||
this.lastSelectedAccount = selected.length === 1 ? selected.attr("data-account-key") : null;
|
||||
};
|
||||
}
|
||||
|
||||
ready(){
|
||||
for(let event of [ "uiInlineComposeTweet", "uiDockedComposeTweet" ]){
|
||||
$(document).on(event, this.uiComposeTweetEvent);
|
||||
window.TDPF_prioritizeNewestEvent(document, event);
|
||||
}
|
||||
|
||||
$(document).on("click", ".js-account-list .js-account-item", this.onSelectedAccountChanged);
|
||||
}
|
||||
|
||||
disabled(){
|
||||
$(document).off("uiInlineComposeTweet", this.uiComposeTweetEvent);
|
||||
$(document).off("uiDockedComposeTweet", this.uiComposeTweetEvent);
|
||||
$(document).off("click", ".js-account-list .js-account-item", this.onSelectedAccountChanged);
|
||||
}
|
@@ -1,403 +0,0 @@
|
||||
enabled(){
|
||||
let me = this;
|
||||
|
||||
// configuration
|
||||
|
||||
this.config = {
|
||||
templates: {} // identifier: { name, contents }
|
||||
};
|
||||
|
||||
const configFile = "config.json";
|
||||
|
||||
$TDP.checkFileExists(this.$token, configFile).then(exists => {
|
||||
if (!exists){
|
||||
$TDP.writeFile(this.$token, configFile, JSON.stringify(this.config));
|
||||
}
|
||||
else{
|
||||
$TDP.readFile(this.$token, configFile, true).then(contents => {
|
||||
try{
|
||||
$.extend(true, this.config, JSON.parse(contents));
|
||||
}catch(err){
|
||||
// why :(
|
||||
}
|
||||
}).catch(err => {
|
||||
$TD.alert("error", "Problem loading configuration for the template plugin: "+err.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.saveConfig = () => {
|
||||
$TDP.writeFile(this.$token, configFile, JSON.stringify(this.config)).catch(err => {
|
||||
$TD.alert("error", "Problem saving configuration for the template plugin: "+err.message);
|
||||
});
|
||||
};
|
||||
|
||||
// setup
|
||||
|
||||
this.htmlModal = null;
|
||||
|
||||
$TDP.readFileRoot(this.$token, "modal.html").then(contents => {
|
||||
this.htmlModal = contents;
|
||||
}).catch(err => {
|
||||
$TD.alert("error", "Problem loading data for the template plugin: "+err.message);
|
||||
});
|
||||
|
||||
// button
|
||||
|
||||
let buttonHTML = '<button class="manage-templates-btn needsclick btn btn-on-blue full-width txt-left margin-b--12 padding-v--6 padding-h--12"><i class="icon icon-bookmark"></i><span class="label padding-ls">Manage templates</span></button>';
|
||||
|
||||
this.prevComposeMustache = TD.mustaches["compose/docked_compose.mustache"];
|
||||
window.TDPF_injectMustache("compose/docked_compose.mustache", "prepend", '<div class="js-tweet-type-button">', buttonHTML);
|
||||
|
||||
let dockedComposePanel = $(".js-docked-compose");
|
||||
|
||||
if (dockedComposePanel.length){
|
||||
dockedComposePanel.find(".js-tweet-type-button").first().before(buttonHTML);
|
||||
}
|
||||
|
||||
// template implementation
|
||||
|
||||
const readTemplateTokens = (contents, tokenData) => {
|
||||
let startIndex = -1;
|
||||
let endIndex = -1;
|
||||
|
||||
let data = [];
|
||||
let tokenNames = Object.keys(tokenData);
|
||||
|
||||
for(let currentIndex = 0; currentIndex < contents.length; currentIndex++){
|
||||
if (contents[currentIndex] === '\\'){
|
||||
contents = contents.substring(0, currentIndex)+contents.substring(currentIndex+1);
|
||||
continue;
|
||||
}
|
||||
else if (contents[currentIndex] !== '{'){
|
||||
continue;
|
||||
}
|
||||
|
||||
startIndex = currentIndex+1;
|
||||
|
||||
for(; startIndex < contents.length; startIndex++){
|
||||
if (!tokenNames.some(name => contents[startIndex] === name[startIndex-currentIndex-1])){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
endIndex = startIndex;
|
||||
|
||||
let token = contents.substring(currentIndex+1, startIndex);
|
||||
let replacement = tokenData[token] || "";
|
||||
|
||||
let entry = [ token, currentIndex ];
|
||||
|
||||
if (contents[endIndex] === '#'){
|
||||
++endIndex;
|
||||
|
||||
let bracketCount = 1;
|
||||
|
||||
for(; endIndex < contents.length; endIndex++){
|
||||
if (contents[endIndex] === '{'){
|
||||
++bracketCount;
|
||||
}
|
||||
else if (contents[endIndex] === '}'){
|
||||
if (--bracketCount === 0){
|
||||
entry.push(contents.substring(startIndex+1, endIndex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (contents[endIndex] === '#'){
|
||||
entry.push(contents.substring(startIndex+1, endIndex));
|
||||
startIndex = endIndex;
|
||||
}
|
||||
else if (contents[endIndex] === '\\'){
|
||||
contents = contents.substring(0, endIndex)+contents.substring(endIndex+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (contents[endIndex] !== '}'){
|
||||
continue;
|
||||
}
|
||||
|
||||
data.push(entry);
|
||||
|
||||
contents = contents.substring(0, currentIndex)+replacement+contents.substring(endIndex+1);
|
||||
currentIndex += replacement.length;
|
||||
}
|
||||
|
||||
return [ contents, data ];
|
||||
};
|
||||
|
||||
const doAjaxRequest = (index, url, evaluator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!url){
|
||||
resolve([ index, "{ajax}" ]);
|
||||
return;
|
||||
}
|
||||
|
||||
$TD.makeGetRequest(url, function(data){
|
||||
if (evaluator){
|
||||
resolve([ index, eval(evaluator.replace(/\$/g, "'"+data.replace(/(["'\\\n\r\u2028\u2029])/g, "\\$1")+"'"))]);
|
||||
}
|
||||
else{
|
||||
resolve([ index, data ]);
|
||||
}
|
||||
}, function(err){
|
||||
resolve([ index, "" ]);
|
||||
$TD.alert("error", "Error executing AJAX request: "+err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const useTemplate = (contents, append) => {
|
||||
let ele = $(".js-compose-text");
|
||||
if (ele.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = append ? ele.val()+contents : contents;
|
||||
let prevLength = value.length;
|
||||
|
||||
let tokens = null;
|
||||
|
||||
[value, tokens] = readTemplateTokens(value, {
|
||||
"cursor": "",
|
||||
"ajax": "(...)"
|
||||
});
|
||||
|
||||
ele.val(value);
|
||||
ele.trigger("change");
|
||||
ele.focus();
|
||||
|
||||
ele[0].selectionStart = ele[0].selectionEnd = value.length;
|
||||
|
||||
let promises = [];
|
||||
let indexOffset = 0;
|
||||
|
||||
for(let token of tokens){
|
||||
switch(token[0]){
|
||||
case "cursor":
|
||||
let [, index1, length ] = token;
|
||||
ele[0].selectionStart = index1;
|
||||
ele[0].selectionEnd = index1+(length | 0 || 0);
|
||||
break;
|
||||
|
||||
case "ajax":
|
||||
let [, index2, evaluator, url ] = token;
|
||||
|
||||
if (!url){
|
||||
url = evaluator;
|
||||
evaluator = null;
|
||||
}
|
||||
|
||||
promises.push(doAjaxRequest(index2, url, evaluator));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length > 0){
|
||||
let selStart = ele[0].selectionStart;
|
||||
let selEnd = ele[0].selectionEnd;
|
||||
|
||||
ele.prop("disabled", true);
|
||||
|
||||
Promise.all(promises).then(values => {
|
||||
const placeholderLen = 5; // "(...)".length
|
||||
let indexOffset = 0;
|
||||
|
||||
for(let value of values){
|
||||
let diff = value[1].length-placeholderLen;
|
||||
let realIndex = indexOffset+value[0];
|
||||
|
||||
let val = ele.val();
|
||||
ele.val(val.substring(0, realIndex)+value[1]+val.substring(realIndex+placeholderLen));
|
||||
|
||||
indexOffset += diff;
|
||||
}
|
||||
|
||||
ele.prop("disabled", false);
|
||||
ele.trigger("change");
|
||||
ele.focus();
|
||||
|
||||
ele[0].selectionStart = selStart+indexOffset;
|
||||
ele[0].selectionEnd = selEnd+indexOffset;
|
||||
});
|
||||
}
|
||||
|
||||
if (!append){
|
||||
hideTemplateModal();
|
||||
}
|
||||
};
|
||||
|
||||
// modal dialog
|
||||
|
||||
this.editingTemplate = null;
|
||||
|
||||
const showTemplateModal = () => {
|
||||
$(".js-app-content").prepend(this.htmlModal);
|
||||
|
||||
/* TODO possibly implement this later
|
||||
|
||||
<li>{paste}</li>
|
||||
<li>Paste text or an image from clipboard</li>
|
||||
<li>{paste#text}</li>
|
||||
<li>Paste only if clipboard has text</li>
|
||||
<li>{paste#image}</li>
|
||||
<li>Paste only if clipboard has an image</li>
|
||||
|
||||
*/
|
||||
|
||||
let ele = $("#templates-modal-wrap").first();
|
||||
|
||||
ele.on("click", "li[data-template]", function(e){
|
||||
let template = me.config.templates[$(this).attr("data-template")];
|
||||
useTemplate(template.contents, e.shiftKey);
|
||||
});
|
||||
|
||||
ele.on("click", "li[data-template] i[data-action]", function(e){
|
||||
let identifier = $(this).closest("li").attr("data-template");
|
||||
|
||||
switch($(this).attr("data-action")){
|
||||
case "edit-template":
|
||||
let editor = $("#template-editor");
|
||||
|
||||
if (editor.hasClass("invisible")){
|
||||
toggleEditor();
|
||||
}
|
||||
|
||||
let template = me.config.templates[identifier];
|
||||
$("[name='template-name']", editor).val(template.name);
|
||||
$("[name='template-contents']", editor).val(template.contents);
|
||||
|
||||
me.editingTemplate = identifier;
|
||||
break;
|
||||
|
||||
case "delete-template":
|
||||
delete me.config.templates[identifier];
|
||||
onTemplatesUpdated(true);
|
||||
|
||||
if (me.editingTemplate === identifier){
|
||||
me.editingTemplate = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
ele.on("click", ".template-editor-tips-button", function(e){
|
||||
$(this).children(".icon").toggleClass("icon-arrow-d icon-arrow-u");
|
||||
ele.find(".template-editor-tips").toggle();
|
||||
});
|
||||
|
||||
ele.on("click", "button", function(e){
|
||||
switch($(this).attr("data-action")){
|
||||
case "new-template":
|
||||
case "editor-cancel":
|
||||
toggleEditor();
|
||||
break;
|
||||
|
||||
case "editor-confirm":
|
||||
let editor = $("#template-editor");
|
||||
|
||||
if (me.editingTemplate !== null){
|
||||
delete me.config.templates[me.editingTemplate];
|
||||
}
|
||||
|
||||
let name = $("[name='template-name']", editor).val();
|
||||
let identifier = name.toLowerCase().replace(/[^a-z0-9]/g, "")+"-"+(Math.random().toString(36).substring(2, 7));
|
||||
|
||||
if (name.trim().length === 0){
|
||||
alert("Please, include a name for your template.");
|
||||
$("[name='template-name']", editor).focus();
|
||||
return;
|
||||
}
|
||||
|
||||
me.config.templates[identifier] = {
|
||||
name: name,
|
||||
contents: $("[name='template-contents']", editor).val()
|
||||
};
|
||||
|
||||
toggleEditor();
|
||||
onTemplatesUpdated(true);
|
||||
break;
|
||||
|
||||
case "close":
|
||||
hideTemplateModal();
|
||||
break;
|
||||
}
|
||||
|
||||
$(this).blur();
|
||||
});
|
||||
|
||||
onTemplatesUpdated(false);
|
||||
};
|
||||
|
||||
const hideTemplateModal = () => {
|
||||
$("#templates-modal-wrap").remove();
|
||||
};
|
||||
|
||||
const toggleEditor = () => {
|
||||
let editor = $("#template-editor");
|
||||
$("[name]", editor).val("");
|
||||
|
||||
if ($("button[data-action='new-template']", "#template-list").add(editor).toggleClass("invisible").hasClass("invisible")){
|
||||
me.editingTemplate = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onTemplatesUpdated = (save) => {
|
||||
let eles = [];
|
||||
|
||||
for(let identifier of Object.keys(this.config.templates)){
|
||||
eles.push(`<li data-template="${identifier}">
|
||||
<span class="template-name">${this.config.templates[identifier].name}</span>
|
||||
<span class="template-actions"><i class="icon icon-edit" data-action="edit-template"></i><i class="icon icon-close" data-action="delete-template"></i></span>
|
||||
</li>`);
|
||||
}
|
||||
|
||||
if (eles.length === 0){
|
||||
eles.push("<li>No templates available</li>");
|
||||
}
|
||||
|
||||
$("#template-list").children("ul").html(eles.join(""));
|
||||
|
||||
if (save){
|
||||
this.saveConfig();
|
||||
}
|
||||
};
|
||||
|
||||
// event handlers
|
||||
|
||||
this.manageTemplatesButtonClickEvent = function(e){
|
||||
if ($("#templates-modal-wrap").length){
|
||||
hideTemplateModal();
|
||||
}
|
||||
else{
|
||||
showTemplateModal();
|
||||
}
|
||||
|
||||
$(this).blur();
|
||||
};
|
||||
|
||||
this.drawerToggleEvent = function(e, data){
|
||||
if (typeof data === "undefined" || data.activeDrawer !== "compose"){
|
||||
hideTemplateModal();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ready(){
|
||||
$(".js-drawer[data-drawer='compose']").on("click", ".manage-templates-btn", this.manageTemplatesButtonClickEvent);
|
||||
$(document).on("uiDrawerActive", this.drawerToggleEvent);
|
||||
$(document).on("click", ".js-new-composer-opt-in", this.drawerToggleEvent);
|
||||
}
|
||||
|
||||
disabled(){
|
||||
$(".manage-templates-btn").remove();
|
||||
$("#templates-modal-wrap").remove();
|
||||
|
||||
$(".js-drawer[data-drawer='compose']").off("click", ".manage-templates-btn", this.manageTemplatesButtonClickEvent);
|
||||
$(document).off("uiDrawerActive", this.drawerToggleEvent);
|
||||
$(document).off("click", ".js-new-composer-opt-in", this.drawerToggleEvent);
|
||||
|
||||
TD.mustaches["compose/docked_compose.mustache"] = this.prevComposeMustache;
|
||||
}
|
@@ -1,76 +0,0 @@
|
||||
enabled(){
|
||||
// styles
|
||||
|
||||
this.css = window.TDPF_createCustomStyle(this);
|
||||
this.css.insert("html[data-td-theme='dark'] .quoted-tweet .td-timeline-poll { color: #e1e8ed; }");
|
||||
|
||||
// utility functions
|
||||
|
||||
const hasPoll = function(tweet){
|
||||
return tweet.hasPoll && tweet.hasPoll();
|
||||
};
|
||||
|
||||
const renderTweetPoll = function(tweet){
|
||||
return `<div class='td-timeline-poll'>${TD.ui.template.render("status/poll", $.extend({}, tweet, {
|
||||
chirp: tweet
|
||||
}))}</div>`;
|
||||
};
|
||||
|
||||
const renderPollHook = function(tweet, html){
|
||||
let ele = null;
|
||||
|
||||
if (hasPoll(tweet)){
|
||||
(ele || (ele = $(html))).find(".js-tweet-body").first().children("div").last().after(renderTweetPoll(tweet));
|
||||
}
|
||||
|
||||
if (tweet.quotedTweet && hasPoll(tweet.quotedTweet)){
|
||||
(ele || (ele = $(html))).find(".js-quoted-tweet-text").first().after(renderTweetPoll(tweet.quotedTweet));
|
||||
}
|
||||
|
||||
if (ele){
|
||||
ele.find(".js-card-container").css("display", "none");
|
||||
return ele.prop("outerHTML");
|
||||
}
|
||||
else{
|
||||
return html;
|
||||
}
|
||||
};
|
||||
|
||||
// hooks
|
||||
|
||||
const funcs = {
|
||||
TwitterStatus: TD.services.TwitterStatus.prototype.render,
|
||||
TwitterActionOnTweet: TD.services.TwitterActionOnTweet.prototype.render,
|
||||
TweetDetailView: TD.components.TweetDetailView.prototype._renderChirp
|
||||
};
|
||||
|
||||
TD.services.TwitterStatus.prototype.render = function(e){
|
||||
return renderPollHook(this, funcs.TwitterStatus.apply(this, arguments));
|
||||
};
|
||||
|
||||
TD.services.TwitterActionOnTweet.prototype.render = function(e){
|
||||
return renderPollHook(this.targetTweet, funcs.TwitterActionOnTweet.apply(this, arguments));
|
||||
};
|
||||
|
||||
TD.components.TweetDetailView.prototype._renderChirp = function(){
|
||||
let result = funcs.TweetDetailView.apply(this, arguments);
|
||||
|
||||
if (this.mainChirp.quotedTweet && hasPoll(this.mainChirp.quotedTweet)){
|
||||
$(this.$tweetDetail).find(".js-quoted-tweet-text").first().after(renderTweetPoll(this.mainChirp.quotedTweet));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
this.prevRenderFuncs = funcs;
|
||||
window.TDPF_reloadColumns();
|
||||
}
|
||||
|
||||
disabled(){
|
||||
TD.services.TwitterStatus.prototype.render = this.prevRenderFuncs.TwitterStatus;
|
||||
TD.services.TwitterActionOnTweet.prototype.render = this.prevRenderFuncs.TwitterActionOnTweet;
|
||||
TD.components.TweetDetailView.prototype._renderChirp = this.prevRenderFuncs.TweetDetailView;
|
||||
|
||||
this.css.remove();
|
||||
window.TDPF_reloadColumns();
|
||||
}
|
@@ -1,195 +0,0 @@
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open System.Diagnostics
|
||||
open System.IO
|
||||
open System.Threading.Tasks
|
||||
|
||||
// "$(DevEnvDir)CommonExtensions\Microsoft\FSharp\fsi.exe" "$(ProjectDir)Resources\PostBuild.fsx" --exec --nologo -- "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)"
|
||||
// "$(ProjectDir)bld\post_build.exe" "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)"
|
||||
|
||||
exception ArgumentUsage of string
|
||||
|
||||
#if !INTERACTIVE
|
||||
[<EntryPoint>]
|
||||
#endif
|
||||
let main (argv: string[]) =
|
||||
try
|
||||
if argv.Length < 2 then
|
||||
#if INTERACTIVE
|
||||
raise (ArgumentUsage "fsi.exe PostBuild.fsx --exec --nologo -- <TargetDir> <ProjectDir> [ConfigurationName] [VersionTag]")
|
||||
#else
|
||||
raise (ArgumentUsage "PostBuild.exe <TargetDir> <ProjectDir> [ConfigurationName] [VersionTag]")
|
||||
#endif
|
||||
|
||||
let _time name func =
|
||||
let sw = Stopwatch.StartNew()
|
||||
func()
|
||||
sw.Stop()
|
||||
printfn "[%s took %i ms]" name (int (Math.Round(sw.Elapsed.TotalMilliseconds)))
|
||||
|
||||
let (+/) path1 path2 =
|
||||
Path.Combine(path1, path2)
|
||||
|
||||
let sw = Stopwatch.StartNew()
|
||||
|
||||
// Setup
|
||||
|
||||
let targetDir = argv.[0]
|
||||
let projectDir = argv.[1]
|
||||
|
||||
let configuration = if argv.Length >= 3 then argv.[2]
|
||||
else "Release"
|
||||
|
||||
let version = if argv.Length >= 4 then argv.[3]
|
||||
else ((targetDir +/ "TweetDuck.exe") |> FileVersionInfo.GetVersionInfo).FileVersion
|
||||
|
||||
printfn "--------------------------"
|
||||
printfn "TweetDuck version %s" version
|
||||
printfn "--------------------------"
|
||||
|
||||
let guideDir = targetDir +/ "guide"
|
||||
let localesDir = targetDir +/ "locales"
|
||||
let pluginsDir = targetDir +/ "plugins"
|
||||
let resourcesDir = targetDir +/ "resources"
|
||||
|
||||
// Functions (File Management)
|
||||
|
||||
let copyFile source target =
|
||||
File.Copy(source, target, true)
|
||||
|
||||
let createDirectory path =
|
||||
Directory.CreateDirectory(path) |> ignore
|
||||
|
||||
let rec copyDirectoryContentsFiltered source target (filter: string -> bool) =
|
||||
if not (Directory.Exists(target)) then
|
||||
Directory.CreateDirectory(target) |> ignore
|
||||
|
||||
let src = DirectoryInfo(source)
|
||||
|
||||
for file in src.EnumerateFiles() do
|
||||
if filter file.Name then
|
||||
file.CopyTo(target +/ file.Name) |> ignore
|
||||
|
||||
for dir in src.EnumerateDirectories() do
|
||||
if filter dir.Name then
|
||||
copyDirectoryContentsFiltered dir.FullName (target +/ dir.Name) filter
|
||||
|
||||
let copyDirectoryContents source target =
|
||||
copyDirectoryContentsFiltered source target (fun _ -> true)
|
||||
|
||||
// Functions (File Processing)
|
||||
|
||||
let byPattern path pattern =
|
||||
Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories)
|
||||
|
||||
let exceptEndingWith (name: string) =
|
||||
Seq.filter (fun (file: string) -> not (file.EndsWith(name)))
|
||||
|
||||
let iterateFiles (files: string seq) (func: string -> unit) =
|
||||
Parallel.ForEach(files, func) |> ignore
|
||||
|
||||
let readFile file = seq {
|
||||
use stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 0x1000, FileOptions.SequentialScan)
|
||||
use reader = new StreamReader(stream)
|
||||
let mutable cont = true
|
||||
|
||||
while cont do
|
||||
let line = reader.ReadLine()
|
||||
|
||||
if line = null then
|
||||
cont <- false
|
||||
else
|
||||
yield line
|
||||
}
|
||||
|
||||
let writeFile (fullPath: string) (lines: string seq) =
|
||||
let relativePath = fullPath.[(targetDir.Length)..]
|
||||
File.WriteAllLines(fullPath, lines |> Seq.toArray)
|
||||
printfn "Processed %s" relativePath
|
||||
|
||||
let processFiles (files: string seq) (extProcessors: IDictionary<string, (string seq -> string seq)>) =
|
||||
let rec processFileContents file =
|
||||
readFile file
|
||||
|> extProcessors.[Path.GetExtension(file)]
|
||||
|
||||
iterateFiles files (fun file ->
|
||||
processFileContents file
|
||||
|> (writeFile file)
|
||||
)
|
||||
|
||||
// Build
|
||||
|
||||
copyFile (projectDir +/ "bld/Resources/LICENSES.txt") (targetDir +/ "LICENSES.txt")
|
||||
|
||||
copyDirectoryContents (projectDir +/ "Resources/Guide") guideDir
|
||||
copyDirectoryContents (projectDir +/ "Resources/Content") resourcesDir
|
||||
|
||||
createDirectory (pluginsDir +/ "official")
|
||||
createDirectory (pluginsDir +/ "user")
|
||||
|
||||
copyDirectoryContentsFiltered
|
||||
(projectDir +/ "Resources/Plugins")
|
||||
(pluginsDir +/ "official")
|
||||
(fun name -> name <> ".debug" && name <> "emoji-instructions.txt")
|
||||
|
||||
if configuration = "Debug" then
|
||||
copyDirectoryContents
|
||||
(projectDir +/ "Resources/Plugins/.debug")
|
||||
(pluginsDir +/ "user/.debug")
|
||||
|
||||
if Directory.Exists(localesDir) || configuration = "Release" then
|
||||
Directory.EnumerateFiles(localesDir, "*.pak")
|
||||
|> exceptEndingWith @"\en-US.pak"
|
||||
|> Seq.iter File.Delete
|
||||
|
||||
// Validation
|
||||
|
||||
if File.ReadAllText(pluginsDir +/ "official/emoji-keyboard/emoji-ordering.txt").IndexOf('\r') <> -1 then
|
||||
raise (FormatException("emoji-ordering.txt must not have any carriage return characters"))
|
||||
else
|
||||
printfn "Verified emoji-ordering.txt"
|
||||
|
||||
// Processing
|
||||
|
||||
let fileProcessors =
|
||||
dict [
|
||||
".js", id;
|
||||
".css", id;
|
||||
".html", id;
|
||||
".meta", (fun (lines: string seq) ->
|
||||
lines
|
||||
|> Seq.map (fun line -> line.Replace("{version}", version))
|
||||
);
|
||||
]
|
||||
|
||||
processFiles (byPattern targetDir "*.js") fileProcessors
|
||||
processFiles (byPattern targetDir "*.css") fileProcessors
|
||||
processFiles (byPattern targetDir "*.html") fileProcessors
|
||||
processFiles (byPattern pluginsDir "*.meta") fileProcessors
|
||||
|
||||
// Finished
|
||||
|
||||
sw.Stop()
|
||||
printfn "------------------"
|
||||
printfn "Finished in %i ms" (int (Math.Round(sw.Elapsed.TotalMilliseconds)))
|
||||
printfn "------------------"
|
||||
0
|
||||
|
||||
with
|
||||
| ArgumentUsage message ->
|
||||
printfn ""
|
||||
printfn "Build script usage:"
|
||||
printfn "%s" message
|
||||
printfn ""
|
||||
1
|
||||
| ex ->
|
||||
printfn ""
|
||||
printfn "Encountered an error while running PostBuild:"
|
||||
printfn "%A" ex
|
||||
printfn ""
|
||||
1
|
||||
|
||||
#if INTERACTIVE
|
||||
printfn "Running PostBuild in interpreter..."
|
||||
main (fsi.CommandLineArgs |> Array.skip (1 + (fsi.CommandLineArgs |> Array.findIndex (fun arg -> arg = "--"))))
|
||||
#endif
|
@@ -1,63 +0,0 @@
|
||||
#if DEBUG
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace TweetDuck.Resources {
|
||||
static class ResourceHotSwap {
|
||||
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../")));
|
||||
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
|
||||
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe");
|
||||
|
||||
private static string FixPathSlash(string path) {
|
||||
return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + '\\';
|
||||
}
|
||||
|
||||
public static void Run() {
|
||||
if (!File.Exists(HotSwapRebuildScript)) {
|
||||
Debug.WriteLine($"Failed resource hot swap, missing rebuild script: {HotSwapRebuildScript}");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine("Performing resource hot swap...");
|
||||
|
||||
DeleteHotSwapFolder();
|
||||
Directory.CreateDirectory(HotSwapTargetDir);
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
using (Process process = Process.Start(new ProcessStartInfo {
|
||||
FileName = HotSwapRebuildScript,
|
||||
Arguments = $"\"{HotSwapTargetDir}\\\" \"{HotSwapProjectRoot}\\\" \"Debug\" \"{Program.VersionTag}\"",
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
})) {
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
if (!process.WaitForExit(8000)) {
|
||||
Debug.WriteLine("Failed resource hot swap, script did not finish in time");
|
||||
return;
|
||||
}
|
||||
else if (process.ExitCode != 0) {
|
||||
Debug.WriteLine($"Failed resource hot swap, script exited with code {process.ExitCode}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Debug.WriteLine($"Finished rebuild script in {sw.ElapsedMilliseconds} ms");
|
||||
|
||||
Directory.Delete(Program.ResourcesPath, true);
|
||||
Directory.Delete(Program.PluginPath, true);
|
||||
|
||||
Directory.Move(Path.Combine(HotSwapTargetDir, "resources"), Program.ResourcesPath);
|
||||
Directory.Move(Path.Combine(HotSwapTargetDir, "plugins"), Program.PluginPath);
|
||||
|
||||
DeleteHotSwapFolder();
|
||||
}
|
||||
|
||||
private static void DeleteHotSwapFolder() {
|
||||
try {
|
||||
Directory.Delete(HotSwapTargetDir, true);
|
||||
} catch (DirectoryNotFoundException) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Browser;
|
||||
using TweetLib.Core.Utils;
|
||||
|
||||
namespace TweetDuck.Resources {
|
||||
public class ResourceSchemeFactory : ISchemeHandlerFactory {
|
||||
public const string Name = "td";
|
||||
|
||||
private readonly IResourceProvider<IResourceHandler> resourceProvider;
|
||||
|
||||
public ResourceSchemeFactory(IResourceProvider<IResourceHandler> resourceProvider) {
|
||||
this.resourceProvider = resourceProvider;
|
||||
}
|
||||
|
||||
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
|
||||
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) || uri.Scheme != Name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
string rootPath = uri.Authority switch {
|
||||
"resources" => Program.ResourcesPath,
|
||||
"guide" => Program.GuidePath,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (rootPath == null) {
|
||||
return resourceProvider.Status(HttpStatusCode.NotFound, "Invalid URL.");
|
||||
}
|
||||
|
||||
string filePath = FileUtils.ResolveRelativePathSafely(rootPath, uri.AbsolutePath.TrimStart('/'));
|
||||
return filePath.Length == 0 ? resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the root folder.") : resourceProvider.File(filePath);
|
||||
}
|
||||
}
|
||||
}
|
529
TweetDuck.csproj
529
TweetDuck.csproj
@@ -1,529 +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>
|
||||
<Compile Include="Browser\Handling\ResourceProvider.cs" />
|
||||
<Compile Include="Management\LockManager.cs" />
|
||||
<Compile Include="Application\SystemHandler.cs" />
|
||||
<Compile Include="Browser\Adapters\CefScriptExecutor.cs" />
|
||||
<Compile Include="Browser\Bridge\PropertyBridge.cs" />
|
||||
<Compile Include="Browser\Bridge\TweetDeckBridge.cs" />
|
||||
<Compile Include="Browser\Bridge\UpdateBridge.cs" />
|
||||
<Compile Include="Browser\Data\ContextInfo.cs" />
|
||||
<Compile Include="Browser\Data\ResourceHandlers.cs" />
|
||||
<Compile Include="Browser\Data\ResourceLink.cs" />
|
||||
<Compile Include="Browser\Data\WindowState.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\DragHandlerBrowser.cs" />
|
||||
<Compile Include="Browser\Handling\Filters\ResponseFilterBase.cs" />
|
||||
<Compile Include="Browser\Handling\Filters\ResponseFilterVendor.cs" />
|
||||
<Compile Include="Browser\Handling\General\BrowserProcessHandler.cs" />
|
||||
<Compile Include="Browser\Handling\General\FileDialogHandler.cs" />
|
||||
<Compile Include="Browser\Handling\General\JavaScriptDialogHandler.cs" />
|
||||
<Compile Include="Browser\Handling\General\CustomLifeSpanHandler.cs" />
|
||||
<Compile Include="Browser\Handling\KeyboardHandlerBase.cs" />
|
||||
<Compile Include="Browser\Handling\KeyboardHandlerBrowser.cs" />
|
||||
<Compile Include="Browser\Handling\KeyboardHandlerNotification.cs" />
|
||||
<Compile Include="Browser\Handling\RequestHandlerBase.cs" />
|
||||
<Compile Include="Browser\Handling\RequestHandlerBrowser.cs" />
|
||||
<Compile Include="Browser\Handling\ResourceHandlerNotification.cs" />
|
||||
<Compile Include="Browser\Handling\ResourceRequestHandler.cs" />
|
||||
<Compile Include="Browser\Handling\ResourceRequestHandlerBase.cs" />
|
||||
<Compile Include="Browser\Handling\ResourceRequestHandlerBrowser.cs" />
|
||||
<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\ConfigManager.cs" />
|
||||
<Compile Include="Configuration\PluginConfig.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\ProfileManager.cs" />
|
||||
<Compile Include="Management\VideoPlayer.cs" />
|
||||
<Compile Include="Plugins\PluginDispatcher.cs" />
|
||||
<Compile Include="Plugins\PluginSchemeFactory.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Reporter.cs" />
|
||||
<Compile Include="Resources\ResourcesSchemeFactory.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\TwitterUtils.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\Example\FormNotificationExample.cs">
|
||||
<SubType>Form</SubType>
|
||||
</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\error\error.html" />
|
||||
<None Include="Resources\Content\notification\example\example.html" />
|
||||
<None Include="Resources\Content\notification\notification.css" />
|
||||
<None Include="Resources\Content\notification\screenshot\screenshot.js" />
|
||||
<None Include="Resources\Content\plugins\notification\plugins.js" />
|
||||
<None Include="Resources\Content\plugins\tweetdeck\plugins.js" />
|
||||
<None Include="Resources\Images\avatar.png" />
|
||||
<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\Images\spinner.apng" />
|
||||
<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.fsx" />
|
||||
<None Include="Resources\PostCefUpdate.ps1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Redist Include="$(ProjectDir)bld\Redist\*.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj">
|
||||
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
|
||||
<Name>TweetLib.Core</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>
|
||||
<Content Include="Resources\Content\.all.js" />
|
||||
<Content Include="Resources\Content\api\bridge.js" />
|
||||
<Content Include="Resources\Content\api\jquery.js" />
|
||||
<Content Include="Resources\Content\api\ready.js" />
|
||||
<Content Include="Resources\Content\api\td.js" />
|
||||
<Content Include="Resources\Content\api\utils.js" />
|
||||
<Content Include="Resources\Content\bootstrap.js" />
|
||||
<Content Include="Resources\Content\introduction\introduction.css" />
|
||||
<Content Include="Resources\Content\introduction\introduction.js" />
|
||||
<Content Include="Resources\Content\load.js" />
|
||||
<Content Include="Resources\Content\login\hide_cookie_bar.js" />
|
||||
<Content Include="Resources\Content\login\login.css" />
|
||||
<Content Include="Resources\Content\login\setup_document_attributes.js" />
|
||||
<Content Include="Resources\Content\notification\add_skip_button.js" />
|
||||
<Content Include="Resources\Content\notification\disable_clipboard_formatting.js" />
|
||||
<Content Include="Resources\Content\notification\expand_links_or_show_tooltip.js" />
|
||||
<Content Include="Resources\Content\notification\handle_links.js" />
|
||||
<Content Include="Resources\Content\notification\handle_show_this_thread_link.js" />
|
||||
<Content Include="Resources\Content\notification\recalculate_tweet_sent_time.js" />
|
||||
<Content Include="Resources\Content\notification\reset_scroll_position_on_load.js" />
|
||||
<Content Include="Resources\Content\notification\scroll_smoothly.js" />
|
||||
<Content Include="Resources\Content\notification\setup_body_hover_class.js" />
|
||||
<Content Include="Resources\Content\plugins\base.js" />
|
||||
<Content Include="Resources\Content\plugins\setup.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\add_tweetduck_to_settings_menu.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\bypass_t.co_links.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\clear_search_input.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\configure_first_day_of_week.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\configure_language_for_translations.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\disable_clipboard_formatting.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\disable_td_metrics.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\drag_links_onto_columns.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\expand_links_or_show_tooltip.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_dm_input_box_focus.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_horizontal_scrolling_of_column_container.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_marking_dm_as_read_when_replying.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_media_preview_urls.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_missing_bing_translator_languages.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_os_name.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_scheduled_tweets_not_appearing.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\fix_youtube_previews.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\focus_composer_after_alt_tab.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\focus_composer_after_image_upload.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\focus_composer_after_switching_account.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\apply_rot13.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\get_class_style_property.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\get_column_name.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\get_hovered_column.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\get_hovered_tweet.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\inject_mustache.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\patch_functions.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\prioritize_newest_event.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\reload_browser.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\reload_columns.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\retrieve_tweet.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\globals\show_tweet_detail.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\handle_extra_mouse_buttons.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\hook_theme_settings.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\inject_css.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\keep_like_follow_dialogs_open.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\limit_loaded_dm_count.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\make_retweets_lowercase.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\middle_click_tweet_icon_actions.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\move_accounts_above_hashtags_in_search.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\offline_notification.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\open_search_externally.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\open_search_in_first_column.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\paste_images_from_clipboard.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\perform_search.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\pin_composer_icon.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\ready_plugins.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\register_composer_active_event.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\register_global_functions.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\register_global_functions_jquery.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\restore_cleared_column.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\screenshot_tweet.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_column_type_attributes.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_desktop_notifications.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_link_context_menu.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_sound_notifications.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_tweetduck_account_bamboozle.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_tweet_context_menu.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\setup_video_player.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\skip_pre_login_page.js" />
|
||||
<Content Include="Resources\Content\tweetdeck\tweetdeck.css" />
|
||||
<Content Include="Resources\Content\update\update.js" />
|
||||
<Content Include="Resources\Content\update\update.css" />
|
||||
<Content Include="Resources\Guide\img\app-menu.png" />
|
||||
<Content Include="Resources\Guide\img\column-clear-header.png" />
|
||||
<Content Include="Resources\Guide\img\column-clear-preferences.png" />
|
||||
<Content Include="Resources\Guide\img\column-preferences.png" />
|
||||
<Content Include="Resources\Guide\img\icon.ico" />
|
||||
<Content Include="Resources\Guide\img\new-tweet-emoji.png" />
|
||||
<Content Include="Resources\Guide\img\new-tweet-pin.png" />
|
||||
<Content Include="Resources\Guide\img\new-tweet-template-advanced.png" />
|
||||
<Content Include="Resources\Guide\img\new-tweet-template-basic.png" />
|
||||
<Content Include="Resources\Guide\img\options-manage-export.png" />
|
||||
<Content Include="Resources\Guide\img\options-manage-reset.png" />
|
||||
<Content Include="Resources\Guide\img\options-manage.png" />
|
||||
<Content Include="Resources\Guide\img\options-notifications-location.png" />
|
||||
<Content Include="Resources\Guide\img\options-notifications-size.png" />
|
||||
<Content Include="Resources\Guide\img\options-sounds.png" />
|
||||
<Content Include="Resources\Guide\img\settings-dropdown.png" />
|
||||
<Content Include="Resources\Guide\img\settings-editdesign.png" />
|
||||
<Content Include="Resources\Guide\index.html" />
|
||||
<Content Include="Resources\Guide\script.js" />
|
||||
<Content Include="Resources\Guide\style.css" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>rmdir "$(ProjectDir)bin\Debug"
|
||||
rmdir "$(ProjectDir)bin\Release"
|
||||
|
||||
rmdir "$(TargetDir)guide" /S /Q
|
||||
rmdir "$(TargetDir)plugins" /S /Q
|
||||
rmdir "$(TargetDir)resources" /S /Q
|
||||
|
||||
"$(ProjectDir)bld\post_build.exe" "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)"
|
||||
</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<Target Name="BeforeBuild" Condition="(!$([System.IO.File]::Exists("$(ProjectDir)bld\post_build.exe")) OR ($([System.IO.File]::GetLastWriteTime("$(ProjectDir)Resources\PostBuild.fsx").Ticks) > $([System.IO.File]::GetLastWriteTime("$(ProjectDir)bld\post_build.exe").Ticks)))">
|
||||
<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File "$(ProjectDir)bld\POST BUILD.ps1" "$(DevEnvDir)\CommonExtensions\Microsoft\FSharp"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
|
||||
</Target>
|
||||
<Target Name="AfterBuild" Condition="$(ConfigurationName) == Release">
|
||||
<Exec Command="del "$(TargetDir)*.pdb"" />
|
||||
<Exec Command="del "$(TargetDir)*.xml"" />
|
||||
<Delete Files="$(TargetDir)CefSharp.BrowserSubprocess.exe" />
|
||||
<Delete Files="$(TargetDir)widevinecdmadapter.dll" />
|
||||
<Copy SourceFiles="@(Redist)" DestinationFolder="$(TargetDir)" />
|
||||
<Exec Command="start "" /B "ISCC.exe" /Q "$(ProjectDir)bld\gen_upd.iss"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" />
|
||||
</Target>
|
||||
<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="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>
|
@@ -2,19 +2,29 @@ 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.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetTest.System", "lib\TweetTest.System\TweetTest.System.csproj", "{A958FA7A-4A2C-42A7-BFA0-159343483F4E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Unit", "lib\TweetTest.Unit\TweetTest.Unit.fsproj", "{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}"
|
||||
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}") = "TweetDuck.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -34,20 +44,42 @@ 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
|
||||
{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
|
||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.Build.0 = Release|x86
|
||||
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|x86.Build.0 = Debug|x86
|
||||
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Release|x86.ActiveCfg = Debug|x86
|
||||
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.Build.0 = Debug|x86
|
||||
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Release|x86.ActiveCfg = Debug|x86
|
||||
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Debug|x86.Build.0 = Debug|x86
|
||||
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Release|x86.ActiveCfg = Release|x86
|
||||
{93BA3CB4-A812-4949-B07D-8D393FB38937}.Release|x86.Build.0 = Release|x86
|
||||
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Debug|x86.Build.0 = Debug|x86
|
||||
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.ActiveCfg = Release|x86
|
||||
{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.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
|
||||
{476B1007-B12C-447F-B855-9886048201D6}.Release|x86.Build.0 = Release|x86
|
||||
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Debug|x86.Build.0 = Debug|x86
|
||||
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.ActiveCfg = Release|x86
|
||||
{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.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
|
||||
{07F6D350-B16F-44E2-804D-C1142E1E345F}.Release|x86.Build.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@@ -1,209 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Utils {
|
||||
static class BrowserUtils {
|
||||
public static string UserAgentVanilla => Program.BrandName + " " + System.Windows.Forms.Application.ProductVersion;
|
||||
public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + Cef.ChromiumVersion + " Safari/537.36";
|
||||
|
||||
private static UserConfig Config => Program.Config.User;
|
||||
private static SystemConfig SysConfig => Program.Config.System;
|
||||
|
||||
public static void SetupCefArgs(IDictionary<string, string> args) {
|
||||
if (!SysConfig.HardwareAcceleration) {
|
||||
args["disable-gpu"] = "1";
|
||||
args["disable-gpu-compositing"] = "1";
|
||||
}
|
||||
|
||||
if (!Config.EnableSmoothScrolling) {
|
||||
args["disable-smooth-scrolling"] = "1";
|
||||
}
|
||||
|
||||
if (!Config.EnableTouchAdjustment) {
|
||||
args["disable-touch-adjustment"] = "1";
|
||||
}
|
||||
|
||||
if (!Config.EnableColorProfileDetection) {
|
||||
args["force-color-profile"] = "srgb";
|
||||
}
|
||||
|
||||
args["disable-component-update"] = "1";
|
||||
args["disable-pdf-extension"] = "1";
|
||||
args["disable-plugins-discovery"] = "1";
|
||||
args["enable-system-flash"] = "0";
|
||||
|
||||
if (args.TryGetValue("js-flags", out string jsFlags)) {
|
||||
args["js-flags"] = "--expose-gc " + jsFlags;
|
||||
}
|
||||
else {
|
||||
args["js-flags"] = "--expose-gc";
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetupCustomScheme(this CefSettings settings, string name, ISchemeHandlerFactory factory) {
|
||||
settings.RegisterScheme(new CefCustomScheme {
|
||||
SchemeName = name,
|
||||
IsStandard = false,
|
||||
IsSecure = true,
|
||||
IsCorsEnabled = true,
|
||||
IsCSPBypassing = true,
|
||||
SchemeHandlerFactory = factory
|
||||
});
|
||||
}
|
||||
|
||||
public static ChromiumWebBrowser AsControl(this IWebBrowser browserControl) {
|
||||
return (ChromiumWebBrowser) browserControl;
|
||||
}
|
||||
|
||||
public static void SetupZoomEvents(this ChromiumWebBrowser browser) {
|
||||
static void SetZoomLevel(IBrowserHost host, int percentage) {
|
||||
host.SetZoomLevel(Math.Log(percentage / 100.0, 1.2));
|
||||
}
|
||||
|
||||
void UpdateZoomLevel(object sender, EventArgs args) {
|
||||
SetZoomLevel(browser.GetBrowserHost(), Config.ZoomLevel);
|
||||
}
|
||||
|
||||
Config.ZoomLevelChanged += UpdateZoomLevel;
|
||||
browser.Disposed += (sender, args) => Config.ZoomLevelChanged -= UpdateZoomLevel;
|
||||
|
||||
browser.FrameLoadStart += (sender, args) => {
|
||||
if (args.Frame.IsMain && Config.ZoomLevel != 100) {
|
||||
SetZoomLevel(args.Browser.GetHost(), Config.ZoomLevel);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void RegisterJsBridge(this IWebBrowser browserControl, string name, object bridge) {
|
||||
browserControl.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
|
||||
browserControl.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder);
|
||||
}
|
||||
|
||||
public static void ExecuteJsAsync(this IWebBrowser browserControl, string scriptOrMethodName, params object[] args) {
|
||||
if (args.Length == 0) {
|
||||
browserControl.BrowserCore.ExecuteScriptAsync(scriptOrMethodName);
|
||||
}
|
||||
else {
|
||||
browserControl.BrowserCore.ExecuteScriptAsync(scriptOrMethodName, args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenDevToolsCustom(this IWebBrowser browser, Point? inspectPoint = null) {
|
||||
var info = new WindowInfo();
|
||||
info.SetAsPopup(IntPtr.Zero, "Dev Tools");
|
||||
|
||||
if (Config.DevToolsWindowOnTop) {
|
||||
info.ExStyle |= 0x00000008; // WS_EX_TOPMOST
|
||||
}
|
||||
|
||||
Point p = inspectPoint ?? Point.Empty;
|
||||
browser.GetBrowserHost().ShowDevTools(info, p.X, p.Y);
|
||||
}
|
||||
|
||||
public static void OpenExternalBrowser(string url) {
|
||||
if (string.IsNullOrWhiteSpace(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (TwitterUrls.Check(url)) {
|
||||
case TwitterUrls.UrlType.Fine:
|
||||
string browserPath = Config.BrowserPath;
|
||||
|
||||
if (browserPath == null || !File.Exists(browserPath)) {
|
||||
App.SystemHandler.OpenAssociatedProgram(url);
|
||||
}
|
||||
else {
|
||||
string quotedUrl = '"' + url + '"';
|
||||
string browserArgs = Config.BrowserPathArgs == null ? quotedUrl : Config.BrowserPathArgs + ' ' + quotedUrl;
|
||||
|
||||
try {
|
||||
using (Process.Start(browserPath, browserArgs)) {}
|
||||
} catch (Exception e) {
|
||||
Program.Reporter.HandleException("Error Opening Browser", "Could not open the browser.", true, e);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TwitterUrls.UrlType.Tracking:
|
||||
if (Config.IgnoreTrackingUrlWarning) {
|
||||
goto case TwitterUrls.UrlType.Fine;
|
||||
}
|
||||
|
||||
using (FormMessage form = new FormMessage("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n" + url, MessageBoxIcon.Warning)) {
|
||||
form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel | ControlType.Focused);
|
||||
form.AddButton(FormMessage.Yes, DialogResult.Yes, ControlType.Accept);
|
||||
form.AddButton("Always Visit", DialogResult.Ignore);
|
||||
|
||||
DialogResult result = form.ShowDialog();
|
||||
|
||||
if (result == DialogResult.Ignore) {
|
||||
Config.IgnoreTrackingUrlWarning = true;
|
||||
Config.Save();
|
||||
}
|
||||
|
||||
if (result == DialogResult.Ignore || result == DialogResult.Yes) {
|
||||
goto case TwitterUrls.UrlType.Fine;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TwitterUrls.UrlType.Invalid:
|
||||
FormMessage.Warning("Blocked URL", "A potentially malicious or invalid URL was blocked from opening:\n" + url, FormMessage.OK);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenExternalSearch(string query) {
|
||||
if (string.IsNullOrWhiteSpace(query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
string searchUrl = Config.SearchEngineUrl;
|
||||
|
||||
if (string.IsNullOrEmpty(searchUrl)) {
|
||||
if (FormMessage.Question("Search Options", "You have not configured a default search engine yet, would you like to do it now?", FormMessage.Yes, FormMessage.No)) {
|
||||
bool wereSettingsOpen = FormManager.TryFind<FormSettings>() != null;
|
||||
|
||||
FormManager.TryFind<FormBrowser>()?.OpenSettings();
|
||||
|
||||
if (wereSettingsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
FormSettings settings = FormManager.TryFind<FormSettings>();
|
||||
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
settings.FormClosed += (sender, args) => {
|
||||
if (args.CloseReason == CloseReason.UserClosing && Config.SearchEngineUrl != searchUrl) {
|
||||
OpenExternalSearch(query);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
OpenExternalBrowser(searchUrl + Uri.EscapeUriString(query));
|
||||
}
|
||||
}
|
||||
|
||||
public static int Scale(int baseValue, double scaleFactor) {
|
||||
return (int) Math.Round(baseValue * scaleFactor);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,156 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using TweetLib.Core.Utils;
|
||||
using Cookie = CefSharp.Cookie;
|
||||
|
||||
namespace TweetDuck.Utils {
|
||||
static class TwitterUtils {
|
||||
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153);
|
||||
public const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
|
||||
|
||||
public static readonly ResourceLink LoadingSpinner = new ResourceLink("https://ton.twimg.com/tduck/spinner", ResourceHandlers.ForBytes(Properties.Resources.spinner, "image/apng"));
|
||||
|
||||
public static readonly string[] DictionaryWords = {
|
||||
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
|
||||
};
|
||||
|
||||
private static void DownloadTempImage(string url, ImageQuality quality, Action<string> process) {
|
||||
string file = Path.Combine(BrowserCache.CacheFolder, TwitterUrls.GetImageFileName(url) ?? Path.GetRandomFileName());
|
||||
|
||||
if (FileUtils.FileExistsAndNotEmpty(file)) {
|
||||
process(file);
|
||||
}
|
||||
else {
|
||||
DownloadFileAuth(TwitterUrls.GetMediaLink(url, quality), file, () => {
|
||||
process(file);
|
||||
}, ex => {
|
||||
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void ViewImage(string url, ImageQuality quality) {
|
||||
DownloadTempImage(url, quality, path => {
|
||||
string ext = Path.GetExtension(path);
|
||||
|
||||
if (ImageUrl.ValidExtensions.Contains(ext)) {
|
||||
App.SystemHandler.OpenAssociatedProgram(path);
|
||||
}
|
||||
else {
|
||||
FormMessage.Error("Image Download", "Invalid file extension " + ext, FormMessage.OK);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void CopyImage(string url, ImageQuality quality) {
|
||||
DownloadTempImage(url, quality, path => {
|
||||
Image image;
|
||||
|
||||
try {
|
||||
image = Image.FromFile(path);
|
||||
} catch (Exception ex) {
|
||||
FormMessage.Error("Copy Image", "An error occurred while copying the image: " + ex.Message, FormMessage.OK);
|
||||
return;
|
||||
}
|
||||
|
||||
ClipboardManager.SetImage(image);
|
||||
});
|
||||
}
|
||||
|
||||
public static void DownloadImage(string url, string username, ImageQuality quality) {
|
||||
DownloadImages(new string[] { url }, username, quality);
|
||||
}
|
||||
|
||||
public static void DownloadImages(string[] urls, string username, ImageQuality quality) {
|
||||
if (urls.Length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
string firstImageLink = TwitterUrls.GetMediaLink(urls[0], quality);
|
||||
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
|
||||
|
||||
string filename = TwitterUrls.GetImageFileName(firstImageLink);
|
||||
string ext = Path.GetExtension(filename); // includes dot
|
||||
|
||||
using SaveFileDialog dialog = new SaveFileDialog {
|
||||
AutoUpgradeEnabled = true,
|
||||
OverwritePrompt = urls.Length == 1,
|
||||
Title = "Save Image",
|
||||
FileName = qualityIndex == -1 ? filename : $"{username} {Path.ChangeExtension(filename, null)} {firstImageLink.Substring(qualityIndex + 1)}".Trim() + ext,
|
||||
Filter = (urls.Length == 1 ? "Image" : "Images") + (string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == DialogResult.OK) {
|
||||
static void OnFailure(Exception ex) {
|
||||
FormMessage.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, FormMessage.OK);
|
||||
}
|
||||
|
||||
if (urls.Length == 1) {
|
||||
DownloadFileAuth(firstImageLink, dialog.FileName, null, OnFailure);
|
||||
}
|
||||
else {
|
||||
string pathBase = Path.ChangeExtension(dialog.FileName, null);
|
||||
string pathExt = Path.GetExtension(dialog.FileName);
|
||||
|
||||
for (int index = 0; index < urls.Length; index++) {
|
||||
DownloadFileAuth(TwitterUrls.GetMediaLink(urls[index], quality), $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DownloadVideo(string url, string username) {
|
||||
string filename = TwitterUrls.GetFileNameFromUrl(url);
|
||||
string ext = Path.GetExtension(filename);
|
||||
|
||||
using SaveFileDialog dialog = new SaveFileDialog {
|
||||
AutoUpgradeEnabled = true,
|
||||
OverwritePrompt = true,
|
||||
Title = "Save Video",
|
||||
FileName = string.IsNullOrEmpty(username) ? filename : $"{username} {filename}".TrimStart(),
|
||||
Filter = "Video" + (string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == DialogResult.OK) {
|
||||
DownloadFileAuth(url, dialog.FileName, null, ex => {
|
||||
FormMessage.Error("Video Download", "An error occurred while downloading the video: " + ex.Message, FormMessage.OK);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void DownloadFileAuth(string url, string target, Action onSuccess, Action<Exception> onFailure) {
|
||||
const string authCookieName = "auth_token";
|
||||
|
||||
TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
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(target, onSuccess, onFailure);
|
||||
client.DownloadFileAsync(new Uri(url), target);
|
||||
}, scheduler);
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,6 @@ using TweetDuck;
|
||||
|
||||
namespace TweetDuck {
|
||||
internal static class Version {
|
||||
public const string Tag = "1.21";
|
||||
public const string Tag = "1.22.1";
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
if ([IO.File]::Exists("post_build.exe")) {
|
||||
[IO.File]::Delete("post_build.exe");
|
||||
}
|
||||
|
||||
$fs = $args[0];
|
||||
$fsc = "";
|
||||
|
||||
if ([IO.File]::Exists("$fs\fsc.exe")) {
|
||||
$fsc = "$fs\fsc.exe";
|
||||
}
|
||||
|
||||
if ([IO.File]::Exists("$fs\Tools\fsc.exe")) {
|
||||
$fsc = "$fs\Tools\fsc.exe";
|
||||
}
|
||||
|
||||
if ($fsc -eq "") {
|
||||
Write-Host "fsc.exe not found"
|
||||
$Host.SetShouldExit(1);
|
||||
exit
|
||||
}
|
||||
|
||||
& $fsc --standalone --deterministic --preferreduilang:en-US --platform:x86 --target:exe --out:post_build.exe "$PSScriptRoot\..\Resources\PostBuild.fsx"
|
@@ -7,7 +7,7 @@
|
||||
#define MyAppShortURL "https://td.chylex.com"
|
||||
#define MyAppExeName "TweetDuck.exe"
|
||||
|
||||
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
|
||||
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
||||
|
||||
[Setup]
|
||||
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
||||
@@ -43,8 +43,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "..\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||
|
@@ -7,7 +7,7 @@
|
||||
#define MyAppShortURL "https://td.chylex.com"
|
||||
#define MyAppExeName "TweetDuck.exe"
|
||||
|
||||
#define MyAppVersion GetFileVersion("..\bin\x86\Release\TweetDuck.exe")
|
||||
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
||||
|
||||
[Setup]
|
||||
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
||||
@@ -40,8 +40,8 @@ MinVersion=0,6.1
|
||||
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\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
|
||||
|
@@ -8,8 +8,8 @@
|
||||
#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 MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
||||
#define CefVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\libcef.dll")
|
||||
|
||||
[Setup]
|
||||
AppId={{{#MyAppID}}
|
||||
@@ -43,12 +43,13 @@ MinVersion=0,6.1
|
||||
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\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\TweetImpl.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\windows\TweetDuck\bin\x86\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||
@@ -67,6 +68,7 @@ Type: filesandordirs; Name: "{localappdata}\{#MyAppName}\GPUCache"
|
||||
[InstallDelete]
|
||||
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"
|
||||
|
111
lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
Normal file
111
lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using TweetLib.Browser.Base;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Browser.Events;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Component {
|
||||
public abstract class BrowserComponent<TFrame, TRequest> : IBrowserComponent where TFrame : IDisposable {
|
||||
public bool Ready { get; private set; }
|
||||
|
||||
public string Url => browser.Url;
|
||||
public abstract string CacheFolder { get; }
|
||||
|
||||
public event EventHandler<BrowserLoadedEventArgs>? BrowserLoaded;
|
||||
public event EventHandler<PageLoadEventArgs>? PageLoadStart;
|
||||
public event EventHandler<PageLoadEventArgs>? PageLoadEnd;
|
||||
|
||||
private readonly IBrowserWrapper<TFrame, TRequest> browser;
|
||||
private readonly ICefAdapter cefAdapter;
|
||||
private readonly IFrameAdapter<TFrame> frameAdapter;
|
||||
private readonly IRequestAdapter<TRequest> requestAdapter;
|
||||
|
||||
protected BrowserComponent(IBrowserWrapper<TFrame, TRequest> browser, ICefAdapter cefAdapter, IFrameAdapter<TFrame> frameAdapter, IRequestAdapter<TRequest> requestAdapter) {
|
||||
this.browser = browser;
|
||||
this.cefAdapter = cefAdapter;
|
||||
this.frameAdapter = frameAdapter;
|
||||
this.requestAdapter = requestAdapter;
|
||||
}
|
||||
|
||||
public abstract void Setup(BrowserSetup setup);
|
||||
public abstract void AttachBridgeObject(string name, object bridge);
|
||||
|
||||
private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs {
|
||||
private readonly IBrowserWrapper<TFrame, TRequest> browser;
|
||||
|
||||
public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame, TRequest> browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
public override void AddDictionaryWords(params string[] words) {
|
||||
foreach (string word in words) {
|
||||
browser.AddWordToDictionary(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnLoadingStateChanged(bool isLoading) {
|
||||
if (!isLoading && !Ready) {
|
||||
Ready = true;
|
||||
BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser));
|
||||
BrowserLoaded = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnLoadError<T>(string failedUrl, T errorCode, IErrorCodeAdapter<T> errorCodeAdapter) {
|
||||
if (errorCodeAdapter.IsAborted(errorCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!failedUrl.StartsWithOrdinal("td://resources/error/")) {
|
||||
using TFrame frame = browser.MainFrame;
|
||||
|
||||
if (frameAdapter.IsValid(frame)) {
|
||||
string? errorName = errorCodeAdapter.GetName(errorCode);
|
||||
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
|
||||
frameAdapter.LoadUrl(frame, "td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnFrameLoadStart(string url, TFrame frame) {
|
||||
if (frameAdapter.IsMain(frame)) {
|
||||
PageLoadStart?.Invoke(this, new PageLoadEventArgs(url));
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnFrameLoadEnd(string url, TFrame frame) {
|
||||
if (frameAdapter.IsMain(frame)) {
|
||||
PageLoadEnd?.Invoke(this, new PageLoadEventArgs(url));
|
||||
}
|
||||
}
|
||||
|
||||
public void RunScript(string identifier, string script) {
|
||||
using TFrame frame = browser.MainFrame;
|
||||
frameAdapter.ExecuteJavaScriptAsync(frame, script, identifier, 1);
|
||||
}
|
||||
|
||||
public void DownloadFile(string url, string path, Action? onSuccess, Action<Exception>? onError) {
|
||||
cefAdapter.RunOnUiThread(() => {
|
||||
var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
|
||||
try {
|
||||
var request = browser.CreateGetRequest();
|
||||
requestAdapter.SetUrl(request, url);
|
||||
requestAdapter.SetMethod(request, "GET");
|
||||
requestAdapter.SetReferrer(request, Url);
|
||||
requestAdapter.SetAllowStoredCredentials(request);
|
||||
|
||||
using TFrame frame = browser.MainFrame;
|
||||
browser.RequestDownload(frame, request, new DownloadCallbacks(fileStream, onSuccess, onError));
|
||||
} catch (Exception e) {
|
||||
fileStream.Dispose();
|
||||
onError?.Invoke(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
26
lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs
Normal file
26
lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Data {
|
||||
public sealed class ByteArrayResource {
|
||||
private const string DefaultMimeType = "text/html";
|
||||
private const HttpStatusCode DefaultStatusCode = HttpStatusCode.OK;
|
||||
private const string DefaultStatusText = "OK";
|
||||
|
||||
internal byte[] Contents { get; }
|
||||
internal int Length { get; }
|
||||
internal string MimeType { get; }
|
||||
internal HttpStatusCode StatusCode { get; }
|
||||
internal string StatusText { get; }
|
||||
|
||||
public ByteArrayResource(byte[] contents, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) {
|
||||
this.Contents = contents;
|
||||
this.Length = contents.Length;
|
||||
this.MimeType = mimeType;
|
||||
this.StatusCode = statusCode;
|
||||
this.StatusText = statusText;
|
||||
}
|
||||
|
||||
public ByteArrayResource(string contents, Encoding encoding, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) : this(encoding.GetBytes(contents), mimeType, statusCode, statusText) {}
|
||||
}
|
||||
}
|
29
lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs
Normal file
29
lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Data {
|
||||
abstract class ContextMenuActionRegistry<T> {
|
||||
private readonly Dictionary<T, Action> actions = new ();
|
||||
|
||||
protected abstract T NextId(int n);
|
||||
|
||||
public T AddAction(Action action) {
|
||||
T id = NextId(actions.Count);
|
||||
actions[id] = action;
|
||||
return id;
|
||||
}
|
||||
|
||||
public bool Execute(T id) {
|
||||
if (actions.TryGetValue(id, out var action)) {
|
||||
action();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
actions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
33
lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
Normal file
33
lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Data {
|
||||
public sealed class DownloadCallbacks {
|
||||
internal bool HasData { get; private set; }
|
||||
|
||||
private readonly FileStream fileStream;
|
||||
private readonly Action? onSuccess;
|
||||
private readonly Action<Exception>? onError;
|
||||
|
||||
internal DownloadCallbacks(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) {
|
||||
this.fileStream = fileStream;
|
||||
this.onSuccess = onSuccess;
|
||||
this.onError = onError;
|
||||
}
|
||||
|
||||
internal void OnData(Stream data) {
|
||||
data.CopyTo(fileStream);
|
||||
HasData |= fileStream.Position > 0;
|
||||
}
|
||||
|
||||
internal void OnSuccess() {
|
||||
fileStream.Dispose();
|
||||
onSuccess?.Invoke();
|
||||
}
|
||||
|
||||
internal void OnError(Exception e) {
|
||||
fileStream.Dispose();
|
||||
onError?.Invoke(e);
|
||||
}
|
||||
}
|
||||
}
|
47
lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs
Normal file
47
lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Data {
|
||||
public sealed class ResourceHandlerRegistry<TResourceHandler> where TResourceHandler : class {
|
||||
private readonly IResourceHandlerFactory<TResourceHandler> factory;
|
||||
private readonly ConcurrentDictionary<string, Func<TResourceHandler>> resourceHandlers = new (StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public ResourceHandlerRegistry(IResourceHandlerFactory<TResourceHandler> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
internal bool HasHandler(string url) {
|
||||
return resourceHandlers.ContainsKey(url);
|
||||
}
|
||||
|
||||
internal TResourceHandler? GetHandler(string url) {
|
||||
return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null;
|
||||
}
|
||||
|
||||
private void Register(string url, Func<TResourceHandler> factory) {
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
|
||||
throw new ArgumentException("Resource handler URL must be absolute!");
|
||||
}
|
||||
|
||||
resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (_, _) => factory);
|
||||
}
|
||||
|
||||
public void RegisterStatic(string url, byte[] staticData, string mimeType = "text/html") {
|
||||
Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, mimeType)));
|
||||
}
|
||||
|
||||
public void RegisterStatic(string url, string staticData, string mimeType = "text/html") {
|
||||
Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, Encoding.UTF8, mimeType)));
|
||||
}
|
||||
|
||||
public void RegisterDynamic(string url, TResourceHandler handler) {
|
||||
Register(url, () => handler);
|
||||
}
|
||||
|
||||
public void Unregister(string url) {
|
||||
resourceHandlers.TryRemove(url, out _);
|
||||
}
|
||||
}
|
||||
}
|
7
lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs
Normal file
7
lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace TweetLib.Browser.CEF.Dialogs {
|
||||
public enum FileDialogType {
|
||||
Open,
|
||||
OpenMultiple,
|
||||
Other
|
||||
}
|
||||
}
|
8
lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs
Normal file
8
lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TweetLib.Browser.CEF.Dialogs {
|
||||
public enum JsDialogType {
|
||||
Alert,
|
||||
Confirm,
|
||||
Prompt,
|
||||
Unknown
|
||||
}
|
||||
}
|
9
lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs
Normal file
9
lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TweetLib.Browser.CEF.Dialogs {
|
||||
public enum MessageDialogType {
|
||||
None,
|
||||
Question,
|
||||
Information,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
13
lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
Normal file
13
lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IBrowserWrapper<TFrame, TRequest> where TFrame : IDisposable {
|
||||
string Url { get; }
|
||||
TFrame MainFrame { get; }
|
||||
|
||||
void AddWordToDictionary(string word);
|
||||
TRequest CreateGetRequest();
|
||||
void RequestDownload(TFrame frame, TRequest request, DownloadCallbacks callbacks);
|
||||
}
|
||||
}
|
7
lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
Normal file
7
lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface ICefAdapter {
|
||||
void RunOnUiThread(Action action);
|
||||
}
|
||||
}
|
9
lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs
Normal file
9
lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IDragDataAdapter<T> {
|
||||
bool IsLink(T data);
|
||||
string GetLink(T data);
|
||||
|
||||
bool IsFragment(T data);
|
||||
string GetFragmentAsText(T data);
|
||||
}
|
||||
}
|
6
lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs
Normal file
6
lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IErrorCodeAdapter<T> {
|
||||
bool IsAborted(T errorCode);
|
||||
string? GetName(T errorCode);
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IFileDialogCallbackAdapter<T> {
|
||||
void Continue(T callback, int selectedAcceptFilter, string[] filePaths);
|
||||
void Cancel(T callback);
|
||||
void Dispose(T callback);
|
||||
}
|
||||
}
|
9
lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs
Normal file
9
lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TweetLib.Utils.Dialogs;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IFileDialogOpener {
|
||||
void OpenFile(string title, bool multiple, List<FileDialogFilter> filters, Action<string[]> onAccepted, Action onCancelled);
|
||||
}
|
||||
}
|
8
lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs
Normal file
8
lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IFrameAdapter<T> {
|
||||
bool IsValid(T frame);
|
||||
bool IsMain(T frame);
|
||||
void LoadUrl(T frame, string url);
|
||||
void ExecuteJavaScriptAsync(T frame, string script, string identifier, int startLine = 1);
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IJsDialogCallbackAdapter<T> {
|
||||
void Continue(T callback, bool success, string? userInput = null);
|
||||
void Dispose(T callback);
|
||||
}
|
||||
}
|
10
lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs
Normal file
10
lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using TweetLib.Browser.CEF.Dialogs;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IJsDialogOpener {
|
||||
void Alert(MessageDialogType type, string title, string message, Action<bool> callback);
|
||||
void Confirm(MessageDialogType type, string title, string message, Action<bool> callback);
|
||||
void Prompt(MessageDialogType type, string title, string message, Action<bool, string> callback);
|
||||
}
|
||||
}
|
16
lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
Normal file
16
lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IMenuModelAdapter<T> {
|
||||
int GetItemCount(T model);
|
||||
|
||||
void AddCommand(T model, int command, string name);
|
||||
int GetCommandAt(T model, int index);
|
||||
|
||||
void AddCheckCommand(T model, int command, string name);
|
||||
void SetChecked(T model, int command, bool isChecked);
|
||||
|
||||
void AddSeparator(T model);
|
||||
bool IsSeparatorAt(T model, int index);
|
||||
|
||||
void RemoveAt(T model, int index);
|
||||
}
|
||||
}
|
6
lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs
Normal file
6
lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IPopupHandler {
|
||||
bool IsPopupAllowed(string url);
|
||||
void OpenExternalBrowser(string url);
|
||||
}
|
||||
}
|
25
lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
Normal file
25
lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using TweetLib.Browser.Request;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IRequestAdapter<T> {
|
||||
ulong GetIdentifier(T request);
|
||||
|
||||
string GetUrl(T request);
|
||||
|
||||
void SetUrl(T request, string url);
|
||||
|
||||
void SetMethod(T request, string method);
|
||||
|
||||
bool IsTransitionForwardBack(T request);
|
||||
|
||||
bool IsCspReport(T request);
|
||||
|
||||
ResourceType GetResourceType(T request);
|
||||
|
||||
void SetHeader(T request, string header, string value);
|
||||
|
||||
void SetReferrer(T request, string referrer);
|
||||
|
||||
void SetAllowStoredCredentials(T request);
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IResourceHandlerFactory<T> {
|
||||
T CreateResourceHandler(ByteArrayResource resource);
|
||||
string GetMimeTypeFromExtension(string extension);
|
||||
}
|
||||
}
|
9
lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs
Normal file
9
lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface IResponseAdapter<T> {
|
||||
void SetCharset(T response, string charset);
|
||||
void SetMimeType(T response, string mimeType);
|
||||
void SetStatus(T response, int statusCode, string statusText);
|
||||
void SetHeader(T response, string header, string value);
|
||||
string? GetHeader(T response, string header);
|
||||
}
|
||||
}
|
5
lib/TweetLib.Browser.CEF/Lib.cs
Normal file
5
lib/TweetLib.Browser.CEF/Lib.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyTitle("TweetDuck Browser CEF Library")]
|
||||
[assembly: AssemblyDescription("TweetDuck Browser CEF Library")]
|
||||
[assembly: AssemblyProduct("TweetLib.Browser.CEF")]
|
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public abstract class ByteArrayResourceHandlerLogic {
|
||||
public delegate void WriteToOut<T>(T dataOut, byte[] dataIn, int position, int length);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public sealed class ByteArrayResourceHandlerLogic<TResponse> : ByteArrayResourceHandlerLogic {
|
||||
private readonly ByteArrayResource resource;
|
||||
private readonly IResponseAdapter<TResponse> responseAdapter;
|
||||
|
||||
private int position;
|
||||
|
||||
public ByteArrayResourceHandlerLogic(ByteArrayResource resource, IResponseAdapter<TResponse> responseAdapter) {
|
||||
this.resource = resource;
|
||||
this.responseAdapter = responseAdapter;
|
||||
}
|
||||
|
||||
public bool Open(out bool handleRequest) {
|
||||
position = 0;
|
||||
handleRequest = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void GetResponseHeaders(TResponse response, out long responseLength, out string? redirectUrl) {
|
||||
responseLength = resource.Length;
|
||||
redirectUrl = null;
|
||||
|
||||
responseAdapter.SetMimeType(response, resource.MimeType);
|
||||
responseAdapter.SetStatus(response, (int) resource.StatusCode, resource.StatusText);
|
||||
responseAdapter.SetCharset(response, "utf-8");
|
||||
responseAdapter.SetHeader(response, "Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
|
||||
public bool Skip(long bytesToSkip, out long bytesSkipped, IDisposable callback) {
|
||||
callback.Dispose();
|
||||
|
||||
position = (int) (position + bytesToSkip);
|
||||
bytesSkipped = bytesToSkip;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Read<T>(WriteToOut<T> write, T dataOut, long maxBytesToRead, out int bytesRead, IDisposable callback) {
|
||||
callback.Dispose();
|
||||
|
||||
if (maxBytesToRead == 0) {
|
||||
bytesRead = 0;
|
||||
}
|
||||
else {
|
||||
int bytesToRead = (int) Math.Min(maxBytesToRead, resource.Length - position);
|
||||
|
||||
write(dataOut, resource.Contents, position, bytesToRead);
|
||||
position += bytesToRead;
|
||||
bytesRead = bytesToRead;
|
||||
}
|
||||
|
||||
return bytesRead > 0;
|
||||
}
|
||||
}
|
||||
}
|
125
lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
Normal file
125
lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Browser.Contexts;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public abstract class ContextMenuLogic {
|
||||
protected const int CommandCustomFirst = 220;
|
||||
protected const int CommandCustomLast = 250;
|
||||
|
||||
protected static readonly HashSet<int> AllowedCommands = new () {
|
||||
-1, // NotFound
|
||||
110, // Undo
|
||||
111, // Redo
|
||||
112, // Cut
|
||||
113, // Copy
|
||||
114, // Paste
|
||||
115, // Delete
|
||||
116, // SelectAll
|
||||
200, // SpellCheckSuggestion0
|
||||
201, // SpellCheckSuggestion1
|
||||
202, // SpellCheckSuggestion2
|
||||
203, // SpellCheckSuggestion3
|
||||
204, // SpellCheckSuggestion4
|
||||
205, // SpellCheckNoSuggestions
|
||||
206 // AddToDictionary
|
||||
};
|
||||
|
||||
private protected sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<int> {
|
||||
private const int CommandUserFirst = 26500;
|
||||
|
||||
protected override int NextId(int n) {
|
||||
return CommandUserFirst + 500 + n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ContextMenuLogic<TModel> : ContextMenuLogic {
|
||||
private readonly IContextMenuHandler? handler;
|
||||
private readonly IMenuModelAdapter<TModel> modelAdapter;
|
||||
private readonly ContextMenuActionRegistry actionRegistry;
|
||||
|
||||
public ContextMenuLogic(IContextMenuHandler? handler, IMenuModelAdapter<TModel> modelAdapter) {
|
||||
this.handler = handler;
|
||||
this.modelAdapter = modelAdapter;
|
||||
this.actionRegistry = new ContextMenuActionRegistry();
|
||||
}
|
||||
|
||||
private sealed class ContextMenuBuilder : IContextMenuBuilder {
|
||||
private readonly IMenuModelAdapter<TModel> modelAdapter;
|
||||
private readonly ContextMenuActionRegistry actionRegistry;
|
||||
private readonly TModel model;
|
||||
|
||||
public ContextMenuBuilder(IMenuModelAdapter<TModel> modelAdapter, ContextMenuActionRegistry actionRegistry, TModel model) {
|
||||
this.model = model;
|
||||
this.actionRegistry = actionRegistry;
|
||||
this.modelAdapter = modelAdapter;
|
||||
}
|
||||
|
||||
public void AddAction(string name, Action action) {
|
||||
var id = actionRegistry.AddAction(action);
|
||||
modelAdapter.AddCommand(model, id, name);
|
||||
}
|
||||
|
||||
public void AddActionWithCheck(string name, bool isChecked, Action action) {
|
||||
var id = actionRegistry.AddAction(action);
|
||||
modelAdapter.AddCheckCommand(model, id, name);
|
||||
modelAdapter.SetChecked(model, id, isChecked);
|
||||
}
|
||||
|
||||
public void AddSeparator() {
|
||||
int count = modelAdapter.GetItemCount(model);
|
||||
if (count > 0 && !modelAdapter.IsSeparatorAt(model, count - 1)) { // do not add separators if there is nothing to separate
|
||||
modelAdapter.AddSeparator(model);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSeparatorIfLast(TModel model) {
|
||||
int count = modelAdapter.GetItemCount(model);
|
||||
if (count > 0 && modelAdapter.IsSeparatorAt(model, count - 1)) {
|
||||
modelAdapter.RemoveAt(model, count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBeforeContextMenu(TModel model, Context context) {
|
||||
for (int i = modelAdapter.GetItemCount(model) - 1; i >= 0; i--) {
|
||||
int command = modelAdapter.GetCommandAt(model, i);
|
||||
|
||||
if (!(AllowedCommands.Contains(command) || command is >= CommandCustomFirst and <= CommandCustomLast)) {
|
||||
modelAdapter.RemoveAt(model, i);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = modelAdapter.GetItemCount(model) - 2; i >= 0; i--) {
|
||||
if (modelAdapter.IsSeparatorAt(model, i) && modelAdapter.IsSeparatorAt(model, i + 1)) {
|
||||
modelAdapter.RemoveAt(model, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (modelAdapter.GetItemCount(model) > 0 && modelAdapter.IsSeparatorAt(model, 0)) {
|
||||
modelAdapter.RemoveAt(model, 0);
|
||||
}
|
||||
|
||||
var builder = new ContextMenuBuilder(modelAdapter, actionRegistry, model);
|
||||
builder.AddSeparator();
|
||||
handler?.Show(builder, context);
|
||||
builder.RemoveSeparatorIfLast(model);
|
||||
}
|
||||
|
||||
public bool OnContextMenuCommand(int commandId) {
|
||||
return actionRegistry.Execute(commandId);
|
||||
}
|
||||
|
||||
public void OnContextMenuDismissed() {
|
||||
actionRegistry.Clear();
|
||||
}
|
||||
|
||||
public bool RunContextMenu() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
75
lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs
Normal file
75
lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TweetLib.Browser.CEF.Dialogs;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Utils.Dialogs;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class DialogHandlerLogic<TCallback> {
|
||||
private readonly IFileDialogOpener fileDialogOpener;
|
||||
private readonly IFileDialogCallbackAdapter<TCallback> callbackAdapter;
|
||||
|
||||
public DialogHandlerLogic(IFileDialogOpener fileDialogOpener, IFileDialogCallbackAdapter<TCallback> callbackAdapter) {
|
||||
this.fileDialogOpener = fileDialogOpener;
|
||||
this.callbackAdapter = callbackAdapter;
|
||||
}
|
||||
|
||||
public bool OnFileDialog(FileDialogType type, IEnumerable<string> acceptFilters, TCallback callback) {
|
||||
if (type is FileDialogType.Open or FileDialogType.OpenMultiple) {
|
||||
var multiple = type == FileDialogType.OpenMultiple;
|
||||
var supportedExtensions = acceptFilters.SelectMany(ParseFileType).Where(static filter => !string.IsNullOrEmpty(filter)).ToArray();
|
||||
|
||||
var filters = new List<FileDialogFilter> {
|
||||
new ("All Supported Formats", supportedExtensions),
|
||||
new ("All Files", ".*")
|
||||
};
|
||||
|
||||
fileDialogOpener.OpenFile("Open Files", multiple, filters, files => {
|
||||
string ext = Path.GetExtension(files[0])!.ToLower();
|
||||
callbackAdapter.Continue(callback, Array.FindIndex(supportedExtensions, filter => ParseFileType(filter).Contains(ext)), files);
|
||||
callbackAdapter.Dispose(callback);
|
||||
}, () => {
|
||||
callbackAdapter.Cancel(callback);
|
||||
callbackAdapter.Dispose(callback);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
callbackAdapter.Dispose(callback);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ParseFileType(string type) {
|
||||
if (string.IsNullOrEmpty(type)) {
|
||||
return StringUtils.EmptyArray;
|
||||
}
|
||||
|
||||
if (type[0] == '.') {
|
||||
return new string[] { type };
|
||||
}
|
||||
|
||||
string[] extensions = type switch {
|
||||
"image/jpeg" => new string[] { ".jpg", ".jpeg" },
|
||||
"image/png" => new string[] { ".png" },
|
||||
"image/gif" => new string[] { ".gif" },
|
||||
"image/webp" => new string[] { ".webp" },
|
||||
"video/mp4" => new string[] { ".mp4" },
|
||||
"video/quicktime" => new string[] { ".mov", ".qt" },
|
||||
_ => StringUtils.EmptyArray
|
||||
};
|
||||
|
||||
if (extensions.Length == 0) {
|
||||
Debug.WriteLine("Unknown file type: " + type);
|
||||
Debugger.Break();
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
}
|
||||
}
|
64
lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
Normal file
64
lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class DownloadRequestClientLogic {
|
||||
public enum RequestStatus {
|
||||
Unknown,
|
||||
Success,
|
||||
Failed
|
||||
}
|
||||
|
||||
private readonly DownloadCallbacks callbacks;
|
||||
private bool hasFailed;
|
||||
|
||||
public DownloadRequestClientLogic(DownloadCallbacks callbacks) {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
public bool GetAuthCredentials(IDisposable callback) {
|
||||
callback.Dispose();
|
||||
|
||||
hasFailed = true;
|
||||
callbacks.OnError(new Exception("This URL requires authentication."));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnDownloadData(Stream data) {
|
||||
if (hasFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callbacks.OnData(data);
|
||||
} catch (Exception e) {
|
||||
callbacks.OnError(e);
|
||||
hasFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "SwitchStatementMissingSomeEnumCasesNoDefault")]
|
||||
public void OnRequestComplete(RequestStatus status) {
|
||||
if (hasFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case RequestStatus.Success when callbacks.HasData:
|
||||
callbacks.OnSuccess();
|
||||
break;
|
||||
|
||||
case RequestStatus.Success:
|
||||
callbacks.OnError(new Exception("File is empty."));
|
||||
break;
|
||||
|
||||
default:
|
||||
callbacks.OnError(new Exception("Unknown error."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs
Normal file
37
lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class DragHandlerLogic<TDragData, TRequest> {
|
||||
private readonly IScriptExecutor executor;
|
||||
private readonly RequestHandlerLogic<TRequest> requestHandlerLogic;
|
||||
private readonly IDragDataAdapter<TDragData> dragDataAdapter;
|
||||
|
||||
public DragHandlerLogic(IScriptExecutor executor, RequestHandlerLogic<TRequest> requestHandlerLogic, IDragDataAdapter<TDragData> dragDataAdapter) {
|
||||
this.executor = executor;
|
||||
this.requestHandlerLogic = requestHandlerLogic;
|
||||
this.dragDataAdapter = dragDataAdapter;
|
||||
}
|
||||
|
||||
private void TriggerDragStart(string type, string? data = null) {
|
||||
executor.RunFunction("window.TDGF_onGlobalDragStart && window.TDGF_onGlobalDragStart", type, data);
|
||||
}
|
||||
|
||||
public bool OnDragEnter(TDragData dragData) {
|
||||
var link = dragDataAdapter.GetLink(dragData);
|
||||
requestHandlerLogic.BlockNextUserNavUrl = link;
|
||||
|
||||
if (dragDataAdapter.IsLink(dragData)) {
|
||||
TriggerDragStart("link", link);
|
||||
}
|
||||
else if (dragDataAdapter.IsFragment(dragData)) {
|
||||
TriggerDragStart("text", dragDataAdapter.GetFragmentAsText(dragData).Trim());
|
||||
}
|
||||
else {
|
||||
TriggerDragStart("unknown");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
71
lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs
Normal file
71
lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using TweetLib.Browser.CEF.Dialogs;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class JsDialogHandlerLogic<TCallback> {
|
||||
private static (MessageDialogType, string) GetMessageDialogProperties(string text) {
|
||||
MessageDialogType type = MessageDialogType.None;
|
||||
|
||||
int pipe = text.IndexOf('|');
|
||||
if (pipe != -1) {
|
||||
type = text.Substring(0, pipe) switch {
|
||||
"error" => MessageDialogType.Error,
|
||||
"warning" => MessageDialogType.Warning,
|
||||
"info" => MessageDialogType.Information,
|
||||
"question" => MessageDialogType.Question,
|
||||
_ => MessageDialogType.None
|
||||
};
|
||||
|
||||
if (type != MessageDialogType.None) {
|
||||
text = text.Substring(pipe + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return (type, text);
|
||||
}
|
||||
|
||||
private readonly IJsDialogOpener jsDialogOpener;
|
||||
private readonly IJsDialogCallbackAdapter<TCallback> callbackAdapter;
|
||||
|
||||
public JsDialogHandlerLogic(IJsDialogOpener jsDialogOpener, IJsDialogCallbackAdapter<TCallback> callbackAdapter) {
|
||||
this.jsDialogOpener = jsDialogOpener;
|
||||
this.callbackAdapter = callbackAdapter;
|
||||
}
|
||||
|
||||
public bool OnJSDialog(JsDialogType dialogType, string messageText, TCallback callback, out bool suppressMessage) {
|
||||
suppressMessage = false;
|
||||
|
||||
var (type, text) = GetMessageDialogProperties(messageText);
|
||||
|
||||
if (dialogType == JsDialogType.Alert) {
|
||||
jsDialogOpener.Alert(type, "Browser Message", text, success => {
|
||||
callbackAdapter.Continue(callback, success);
|
||||
callbackAdapter.Dispose(callback);
|
||||
});
|
||||
}
|
||||
else if (dialogType == JsDialogType.Confirm) {
|
||||
jsDialogOpener.Confirm(type, "Browser Confirmation", text, success => {
|
||||
callbackAdapter.Continue(callback, success);
|
||||
callbackAdapter.Dispose(callback);
|
||||
});
|
||||
}
|
||||
else if (dialogType == JsDialogType.Prompt) {
|
||||
jsDialogOpener.Prompt(type, "Browser Prompt", text, (success, input) => {
|
||||
callbackAdapter.Continue(callback, success, input);
|
||||
callbackAdapter.Dispose(callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OnBeforeUnloadDialog(IDisposable callback) {
|
||||
callback.Dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
37
lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs
Normal file
37
lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class LifeSpanHandlerLogic {
|
||||
public enum TargetDisposition {
|
||||
NewBackgroundTab,
|
||||
NewForegroundTab,
|
||||
NewPopup,
|
||||
NewWindow,
|
||||
Other
|
||||
}
|
||||
|
||||
private readonly IPopupHandler popupHandler;
|
||||
|
||||
public LifeSpanHandlerLogic(IPopupHandler popupHandler) {
|
||||
this.popupHandler = popupHandler;
|
||||
}
|
||||
|
||||
public bool OnBeforePopup(string targetUrl, TargetDisposition targetDisposition) {
|
||||
switch (targetDisposition) {
|
||||
case TargetDisposition.NewBackgroundTab:
|
||||
case TargetDisposition.NewForegroundTab:
|
||||
case TargetDisposition.NewPopup when !popupHandler.IsPopupAllowed(targetUrl):
|
||||
case TargetDisposition.NewWindow:
|
||||
popupHandler.OpenExternalBrowser(targetUrl);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DoClose() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
29
lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs
Normal file
29
lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class RequestHandlerLogic<TRequest> {
|
||||
internal string BlockNextUserNavUrl { get; set; } = string.Empty;
|
||||
|
||||
private readonly IRequestAdapter<TRequest> requestAdapter;
|
||||
private readonly LifeSpanHandlerLogic lifeSpanHandlerLogic;
|
||||
|
||||
public RequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, LifeSpanHandlerLogic lifeSpanHandlerLogic) {
|
||||
this.requestAdapter = requestAdapter;
|
||||
this.lifeSpanHandlerLogic = lifeSpanHandlerLogic;
|
||||
}
|
||||
|
||||
private bool ShouldBlockNav(string url) {
|
||||
bool block = url == BlockNextUserNavUrl;
|
||||
BlockNextUserNavUrl = string.Empty;
|
||||
return block;
|
||||
}
|
||||
|
||||
public bool OnBeforeBrowse(TRequest request, bool userGesture) {
|
||||
return requestAdapter.IsTransitionForwardBack(request) || (userGesture && ShouldBlockNav(requestAdapter.GetUrl(request)));
|
||||
}
|
||||
|
||||
public bool OnOpenUrlFromTab(string targetUrl, bool userGesture, LifeSpanHandlerLogic.TargetDisposition targetDisposition) {
|
||||
return (userGesture && ShouldBlockNav(targetUrl)) || lifeSpanHandlerLogic.OnBeforePopup(targetUrl, targetDisposition);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class ResourceRequestHandlerFactoryLogic<TResourceRequestHandler, TResourceHandler, TRequest> where TResourceHandler : class {
|
||||
private readonly IRequestAdapter<TRequest> requestAdapter;
|
||||
private readonly TResourceRequestHandler handler;
|
||||
private readonly ResourceHandlerRegistry<TResourceHandler> registry;
|
||||
|
||||
public ResourceRequestHandlerFactoryLogic(IRequestAdapter<TRequest> requestAdapter, TResourceRequestHandler handler, ResourceHandlerRegistry<TResourceHandler> registry) {
|
||||
this.handler = handler;
|
||||
this.registry = registry;
|
||||
this.requestAdapter = requestAdapter;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "RedundantAssignment")]
|
||||
public TResourceRequestHandler GetResourceRequestHandler(TRequest request, ref bool disableDefaultHandling) {
|
||||
disableDefaultHandling = registry.HasHandler(requestAdapter.GetUrl(request));
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Browser.Request;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class ResourceRequestHandlerLogic<TRequest, TResponse, TResourceHandler> where TResourceHandler : class {
|
||||
private readonly IRequestAdapter<TRequest> requestAdapter;
|
||||
private readonly IResponseAdapter<TResponse> responseAdapter;
|
||||
private readonly ResourceHandlerRegistry<TResourceHandler> resourceHandlerRegistry;
|
||||
private readonly IResourceRequestHandler? resourceRequestHandler;
|
||||
|
||||
private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new ();
|
||||
|
||||
public ResourceRequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, IResponseAdapter<TResponse> responseAdapter, ResourceHandlerRegistry<TResourceHandler> resourceHandlerRegistry, IResourceRequestHandler? resourceRequestHandler) {
|
||||
this.requestAdapter = requestAdapter;
|
||||
this.responseAdapter = responseAdapter;
|
||||
this.resourceHandlerRegistry = resourceHandlerRegistry;
|
||||
this.resourceRequestHandler = resourceRequestHandler;
|
||||
}
|
||||
|
||||
public bool OnBeforeResourceLoad(TRequest request, IDisposable callback) {
|
||||
if (requestAdapter.IsCspReport(request)) {
|
||||
callback.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resourceRequestHandler != null) {
|
||||
var result = resourceRequestHandler.Handle(requestAdapter.GetUrl(request), requestAdapter.GetResourceType(request));
|
||||
|
||||
if (result is RequestHandleResult.Redirect redirect) {
|
||||
requestAdapter.SetUrl(request, redirect.Url);
|
||||
}
|
||||
else if (result is RequestHandleResult.Process process) {
|
||||
requestAdapter.SetHeader(request, "Accept-Encoding", "identity");
|
||||
responseProcessors[requestAdapter.GetIdentifier(request)] = process.Processor;
|
||||
}
|
||||
else if (result == RequestHandleResult.Cancel) {
|
||||
callback.Dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public TResourceHandler? GetResourceHandler(TRequest request) {
|
||||
return resourceHandlerRegistry.GetHandler(requestAdapter.GetUrl(request));
|
||||
}
|
||||
|
||||
public ResponseFilterLogic? GetResourceResponseFilter(TRequest request, TResponse response) {
|
||||
if (responseProcessors.TryGetValue(requestAdapter.GetIdentifier(request), out var processor) && int.TryParse(responseAdapter.GetHeader(response, "Content-Length"), out int totalBytes)) {
|
||||
return new ResponseFilterLogic(processor, totalBytes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void OnResourceLoadComplete(TRequest request) {
|
||||
responseProcessors.Remove(requestAdapter.GetIdentifier(request));
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,42 +1,45 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class ResponseFilterLogic {
|
||||
public enum FilterStatus {
|
||||
NeedMoreData,
|
||||
Done
|
||||
}
|
||||
|
||||
namespace TweetDuck.Browser.Handling.Filters {
|
||||
abstract class ResponseFilterBase : IResponseFilter {
|
||||
private enum State {
|
||||
Reading,
|
||||
Writing,
|
||||
Done
|
||||
}
|
||||
|
||||
private readonly Encoding encoding;
|
||||
private readonly IResponseProcessor processor;
|
||||
private byte[] responseData;
|
||||
|
||||
private State state;
|
||||
private int offset;
|
||||
|
||||
protected ResponseFilterBase(int totalBytes, Encoding encoding) {
|
||||
internal ResponseFilterLogic(IResponseProcessor processor, int totalBytes) {
|
||||
this.processor = processor;
|
||||
this.responseData = new byte[totalBytes];
|
||||
this.encoding = encoding;
|
||||
this.state = State.Reading;
|
||||
}
|
||||
|
||||
FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) {
|
||||
public FilterStatus Filter(Stream? dataIn, out long dataInRead, Stream dataOut, long dataOutLength, out long dataOutWritten) {
|
||||
int responseLength = responseData.Length;
|
||||
|
||||
if (state == State.Reading) {
|
||||
int bytesToRead = Math.Min(responseLength - offset, (int) Math.Min(dataIn?.Length ?? 0, int.MaxValue));
|
||||
int bytesRead = dataIn?.Read(responseData, offset, bytesToRead) ?? 0;
|
||||
|
||||
dataIn?.Read(responseData, offset, bytesToRead);
|
||||
offset += bytesToRead;
|
||||
|
||||
dataInRead = bytesToRead;
|
||||
offset += bytesRead;
|
||||
dataInRead = bytesRead;
|
||||
dataOutWritten = 0;
|
||||
|
||||
if (offset >= responseLength) {
|
||||
responseData = encoding.GetBytes(ProcessResponse(encoding.GetString(responseData)));
|
||||
responseData = processor.Process(responseData);
|
||||
state = State.Writing;
|
||||
offset = 0;
|
||||
}
|
||||
@@ -44,7 +47,7 @@ namespace TweetDuck.Browser.Handling.Filters {
|
||||
return FilterStatus.NeedMoreData;
|
||||
}
|
||||
else if (state == State.Writing) {
|
||||
int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOut.Length, int.MaxValue));
|
||||
int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOutLength, int.MaxValue));
|
||||
|
||||
if (bytesToWrite > 0) {
|
||||
dataOut.Write(responseData, offset, bytesToWrite);
|
||||
@@ -63,12 +66,8 @@ namespace TweetDuck.Browser.Handling.Filters {
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new InvalidOperationException("This resource filter cannot be reused.");
|
||||
throw new InvalidOperationException("This resource filter cannot be reused!");
|
||||
}
|
||||
}
|
||||
|
||||
public abstract bool InitFilter();
|
||||
protected abstract string ProcessResponse(string text);
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
21
lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs
Normal file
21
lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
public sealed class SchemeHandlerFactoryLogic<TRequest, TResourceHandler> where TResourceHandler : class {
|
||||
private readonly ICustomSchemeHandler handler;
|
||||
private readonly IRequestAdapter<TRequest> requestAdapter;
|
||||
private readonly ISchemeResourceVisitor<TResourceHandler> resourceVisitor;
|
||||
|
||||
public SchemeHandlerFactoryLogic(ICustomSchemeHandler handler, IRequestAdapter<TRequest> requestAdapter, IResourceHandlerFactory<TResourceHandler> resourceHandlerFactory) {
|
||||
this.handler = handler;
|
||||
this.requestAdapter = requestAdapter;
|
||||
this.resourceVisitor = new SchemeResourceVisitor<TResourceHandler>(resourceHandlerFactory);
|
||||
}
|
||||
|
||||
public TResourceHandler? Create(TRequest request) {
|
||||
return Uri.TryCreate(requestAdapter.GetUrl(request), UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(resourceVisitor) : null;
|
||||
}
|
||||
}
|
||||
}
|
29
lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs
Normal file
29
lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using TweetLib.Browser.CEF.Data;
|
||||
using TweetLib.Browser.CEF.Interfaces;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Browser.Request;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Logic {
|
||||
abstract class SchemeResourceVisitor {
|
||||
protected static readonly SchemeResource.Status FileIsEmpty = new (HttpStatusCode.NoContent, "File is empty.");
|
||||
}
|
||||
|
||||
sealed class SchemeResourceVisitor<TResourceHandler> : SchemeResourceVisitor, ISchemeResourceVisitor<TResourceHandler> {
|
||||
private readonly IResourceHandlerFactory<TResourceHandler> factory;
|
||||
|
||||
public SchemeResourceVisitor(IResourceHandlerFactory<TResourceHandler> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public TResourceHandler Status(SchemeResource.Status status) {
|
||||
return factory.CreateResourceHandler(new ByteArrayResource(Array.Empty<byte>(), statusCode: status.Code, statusText: status.Message));
|
||||
}
|
||||
|
||||
public TResourceHandler File(SchemeResource.File file) {
|
||||
byte[] contents = file.Contents;
|
||||
return contents.Length == 0 ? Status(FileIsEmpty) : factory.CreateResourceHandler(new ByteArrayResource(contents, factory.GetMimeTypeFromExtension(file.Extension)));
|
||||
}
|
||||
}
|
||||
}
|
21
lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj
Normal file
21
lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Platforms>x86;x64</Platforms>
|
||||
<LangVersion>9</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Version.cs" Link="Version.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TweetLib.Browser\TweetLib.Browser.csproj" />
|
||||
<ProjectReference Include="..\TweetLib.Utils\TweetLib.Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
41
lib/TweetLib.Browser.CEF/Utils/CefUtils.cs
Normal file
41
lib/TweetLib.Browser.CEF/Utils/CefUtils.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using TweetLib.Utils.Collections;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Utils {
|
||||
public static class CefUtils {
|
||||
public static string GetCacheFolder(string storagePath) {
|
||||
return Path.Combine(storagePath, "Cache");
|
||||
}
|
||||
|
||||
public static CommandLineArgs ParseCommandLineArguments(string argumentString) {
|
||||
CommandLineArgs args = new CommandLineArgs();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(argumentString)) {
|
||||
return args;
|
||||
}
|
||||
|
||||
foreach (Match match in Regex.Matches(argumentString, @"([^=\s]+(?:=(?:\S*""[^""]*?""\S*|\S*))?)")) {
|
||||
string matchValue = match.Value;
|
||||
|
||||
int indexEquals = matchValue.IndexOf('=');
|
||||
string key, value;
|
||||
|
||||
if (indexEquals == -1) {
|
||||
key = matchValue.TrimStart('-');
|
||||
value = "1";
|
||||
}
|
||||
else {
|
||||
key = matchValue.Substring(0, indexEquals).TrimStart('-');
|
||||
value = matchValue.Substring(indexEquals + 1).Trim('"');
|
||||
}
|
||||
|
||||
if (key.Length != 0) {
|
||||
args.SetValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
22
lib/TweetLib.Browser.CEF/Utils/SpellCheck.cs
Normal file
22
lib/TweetLib.Browser.CEF/Utils/SpellCheck.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TweetLib.Utils.Globalization;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Utils {
|
||||
public static class SpellCheck {
|
||||
// https://cs.chromium.org/chromium/src/third_party/hunspell_dictionaries/
|
||||
public static IEnumerable<Language> SupportedLanguages { get; } = new List<string> {
|
||||
"af-ZA", "bg-BG", "ca-ES", "cs-CZ", "da-DK", "de-DE",
|
||||
"el-GR", "en-AU", "en-CA", "en-GB", "en-US", "es-ES",
|
||||
"et-EE", "fa-IR", "fo-FO", "fr-FR", "he-IL", "hi-IN",
|
||||
"hr-HR", "hu-HU", "id-ID", "it-IT", "ko" , "lt-LT",
|
||||
"lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT",
|
||||
"ro-RO", "ru-RU", "sk-SK", "sl-SI", "sq" , "sr",
|
||||
"sv-SE", "ta-IN", "tg-TG", "tr" , "uk-UA", "vi-VN"
|
||||
}.Select(static code => {
|
||||
string lang = StringUtils.ExtractBefore(code, '-', 2);
|
||||
return lang is "en" or "pt" ? new Language(code) : new Language(code, lang);
|
||||
}).OrderBy(static code => code).ToList();
|
||||
}
|
||||
}
|
15
lib/TweetLib.Browser/Base/BaseBrowser.cs
Normal file
15
lib/TweetLib.Browser/Base/BaseBrowser.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.Base {
|
||||
public class BaseBrowser<T> : IDisposable where T : BaseBrowser<T> {
|
||||
protected readonly IBrowserComponent browserComponent;
|
||||
|
||||
protected BaseBrowser(IBrowserComponent browserComponent, Func<T, BrowserSetup> setup) {
|
||||
this.browserComponent = browserComponent;
|
||||
this.browserComponent.Setup(setup((T) this));
|
||||
}
|
||||
|
||||
public virtual void Dispose() {}
|
||||
}
|
||||
}
|
8
lib/TweetLib.Browser/Base/BrowserSetup.cs
Normal file
8
lib/TweetLib.Browser/Base/BrowserSetup.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.Base {
|
||||
public sealed class BrowserSetup {
|
||||
public IContextMenuHandler? ContextMenuHandler { get; set; }
|
||||
public IResourceRequestHandler? ResourceRequestHandler { get; set; }
|
||||
}
|
||||
}
|
9
lib/TweetLib.Browser/Contexts/Context.cs
Normal file
9
lib/TweetLib.Browser/Contexts/Context.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TweetLib.Browser.Contexts {
|
||||
public sealed class Context {
|
||||
public Tweet? Tweet { get; set; }
|
||||
public Link? Link { get; set; }
|
||||
public Media? Media { get; set; }
|
||||
public Selection? Selection { get; set; }
|
||||
public Notification? Notification { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user