mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-04-22 18:15:47 +02:00
Major refactor to abstract app logic into libraries
This commit is contained in:
parent
68582f6973
commit
bf9a0226be
Application
Browser
Adapters
CefBrowserComponent.csCefContextMenuActionRegistry.csCefContextMenuModel.csCefResourceHandlerFactory.csCefResourceHandlerRegistry.csCefResourceProvider.csCefResourceRequestHandler.csCefSchemeHandlerFactory.csCefScriptExecutor.cs
Bridge
Data
FormBrowser.Designer.csFormBrowser.csHandling
BrowserProcessHandler.csContextMenuBase.csContextMenuBrowser.csContextMenuGuide.csContextMenuNotification.csCustomKeyboardHandler.csCustomLifeSpanHandler.csDragHandlerBrowser.csFileDialogHandler.cs
Filters
JavaScriptDialogHandler.csKeyboardHandlerBrowser.csKeyboardHandlerNotification.csRequestHandlerBase.csResourceRequestHandler.csResourceRequestHandlerBase.csResourceRequestHandlerBrowser.csResponseFilter.csNotification
FormNotificationBase.csFormNotificationExample.csFormNotificationMain.csFormNotificationTweet.cs
TweetDeckBrowser.csTweetDeckInterfaceImpl.csScreenshot
SoundNotification.csConfiguration
Controls
Dialogs
FormAbout.csFormGuide.csFormPlugins.csFormSettings.cs
Settings
DialogSettingsCefArgs.csDialogSettingsManage.csDialogSettingsRestart.csTabSettingsAdvanced.csTabSettingsFeedback.csTabSettingsGeneral.csTabSettingsNotifications.cs
WindowState.csManagement
Plugins
Program.csProperties
Reporter.csResources
Content
images
notification/example
tweetdeck
Guide
ResourceHotSwap.csResourceProvider.csResourceUtils.csResourcesSchemeFactory.csUpdates
Utils
lib/TweetLib.Browser
Base
Contexts
Events
52
Application/DialogHandler.cs
Normal file
52
Application/DialogHandler.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Core.Application;
|
||||
using TweetLib.Core.Systems.Dialogs;
|
||||
|
||||
namespace TweetDuck.Application {
|
||||
sealed class DialogHandler : IAppDialogHandler {
|
||||
public void Information(string caption, string text, string buttonAccept, string buttonCancel = null) {
|
||||
FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept, buttonCancel));
|
||||
}
|
||||
|
||||
public void Error(string caption, string text, string buttonAccept, string buttonCancel = null) {
|
||||
FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept, buttonCancel));
|
||||
}
|
||||
|
||||
public void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted) {
|
||||
static string FormatFilter(FileDialogFilter filter) {
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(filter.Name);
|
||||
|
||||
var extensions = string.Join(";", filter.Extensions.Select(ext => "*" + ext));
|
||||
if (extensions.Length > 0) {
|
||||
builder.Append(" (");
|
||||
builder.Append(extensions);
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
builder.Append('|');
|
||||
builder.Append(extensions.Length == 0 ? "*.*" : extensions);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
FormManager.RunOnUIThreadAsync(() => {
|
||||
using SaveFileDialog dialog = new SaveFileDialog {
|
||||
AutoUpgradeEnabled = true,
|
||||
OverwritePrompt = settings.OverwritePrompt,
|
||||
Title = settings.DialogTitle,
|
||||
FileName = settings.FileName,
|
||||
Filter = settings.Filters == null ? null : string.Join("|", settings.Filters.Select(FormatFilter))
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == DialogResult.OK) {
|
||||
onAccepted(dialog.FileName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
68
Application/Logger.cs
Normal file
68
Application/Logger.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Application;
|
||||
|
||||
namespace TweetDuck.Application {
|
||||
sealed class Logger : IAppLogger {
|
||||
private string LogFilePath => Path.Combine(App.StoragePath, logFileName);
|
||||
|
||||
private readonly string logFileName;
|
||||
|
||||
public Logger(string logFileName) {
|
||||
this.logFileName = logFileName;
|
||||
}
|
||||
|
||||
bool IAppLogger.Debug(string message) {
|
||||
#if DEBUG
|
||||
return Log("DEBUG", message);
|
||||
#else
|
||||
return Arguments.HasFlag(Arguments.ArgLogging) && Log(message);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IAppLogger.Info(string message) {
|
||||
return Log("INFO", message);
|
||||
}
|
||||
|
||||
bool IAppLogger.Error(string message) {
|
||||
return Log("ERROR", message);
|
||||
}
|
||||
|
||||
bool IAppLogger.OpenLogFile() {
|
||||
try {
|
||||
using (Process.Start(LogFilePath)) {}
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool Log(string level, string message) {
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[" + level + "] " + message);
|
||||
#endif
|
||||
|
||||
string logFilePath = LogFilePath;
|
||||
|
||||
StringBuilder build = new StringBuilder();
|
||||
|
||||
if (!File.Exists(logFilePath)) {
|
||||
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(message).Append("\r\n\r\n");
|
||||
|
||||
try {
|
||||
File.AppendAllText(logFilePath, build.ToString(), Encoding.UTF8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Application;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Application {
|
||||
class SystemHandler : IAppSystemHandler {
|
||||
void IAppSystemHandler.OpenAssociatedProgram(string path) {
|
||||
sealed class SystemHandler : IAppSystemHandler {
|
||||
public void OpenAssociatedProgram(string path) {
|
||||
try {
|
||||
using (Process.Start(new ProcessStartInfo {
|
||||
FileName = path,
|
||||
@ -17,7 +24,66 @@ void IAppSystemHandler.OpenAssociatedProgram(string path) {
|
||||
}
|
||||
}
|
||||
|
||||
void IAppSystemHandler.OpenFileExplorer(string path) {
|
||||
public void OpenBrowser(string url) {
|
||||
if (string.IsNullOrWhiteSpace(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FormManager.RunOnUIThreadAsync(() => {
|
||||
var config = Program.Config.User;
|
||||
|
||||
switch (TwitterUrls.Check(url)) {
|
||||
case TwitterUrls.UrlType.Fine:
|
||||
string browserPath = config.BrowserPath;
|
||||
|
||||
if (browserPath == null || !File.Exists(browserPath)) {
|
||||
OpenAssociatedProgram(url);
|
||||
}
|
||||
else {
|
||||
string quotedUrl = '"' + url + '"';
|
||||
string browserArgs = config.BrowserPathArgs == null ? quotedUrl : config.BrowserPathArgs + ' ' + quotedUrl;
|
||||
|
||||
try {
|
||||
using (Process.Start(browserPath, browserArgs)) {}
|
||||
} catch (Exception e) {
|
||||
App.ErrorHandler.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 void OpenFileExplorer(string path) {
|
||||
if (File.Exists(path)) {
|
||||
using (Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")) {}
|
||||
}
|
||||
@ -25,5 +91,62 @@ void IAppSystemHandler.OpenFileExplorer(string path) {
|
||||
using (Process.Start("explorer.exe", '"' + path.Replace('/', '\\') + '"')) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyImageFromFile(string path) {
|
||||
FormManager.RunOnUIThreadAsync(() => {
|
||||
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 void CopyText(string text) {
|
||||
FormManager.RunOnUIThreadAsync(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
|
||||
}
|
||||
|
||||
public void SearchText(string text) {
|
||||
if (string.IsNullOrWhiteSpace(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FormManager.RunOnUIThreadAsync(() => {
|
||||
var config = Program.Config.User;
|
||||
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) {
|
||||
SearchText(text);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
OpenBrowser(searchUrl + Uri.EscapeUriString(text));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
108
Browser/Adapters/CefBrowserComponent.cs
Normal file
108
Browser/Adapters/CefBrowserComponent.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Browser.Base;
|
||||
using TweetLib.Browser.Events;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Utils.Static;
|
||||
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
||||
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal abstract class CefBrowserComponent : IBrowserComponent {
|
||||
public bool Ready { get; private set; }
|
||||
|
||||
public string Url => browser.Address;
|
||||
|
||||
public IFileDownloader FileDownloader => TwitterFileDownloader.Instance;
|
||||
|
||||
public event EventHandler<BrowserLoadedEventArgs> BrowserLoaded;
|
||||
public event EventHandler<PageLoadEventArgs> PageLoadStart;
|
||||
public event EventHandler<PageLoadEventArgs> PageLoadEnd;
|
||||
|
||||
private readonly ChromiumWebBrowser browser;
|
||||
|
||||
protected CefBrowserComponent(ChromiumWebBrowser browser) {
|
||||
this.browser = browser;
|
||||
this.browser.JsDialogHandler = new JavaScriptDialogHandler();
|
||||
this.browser.LifeSpanHandler = new CustomLifeSpanHandler();
|
||||
this.browser.LoadingStateChanged += OnLoadingStateChanged;
|
||||
this.browser.LoadError += OnLoadError;
|
||||
this.browser.FrameLoadStart += OnFrameLoadStart;
|
||||
this.browser.FrameLoadEnd += OnFrameLoadEnd;
|
||||
this.browser.SetupZoomEvents();
|
||||
}
|
||||
|
||||
void IBrowserComponent.Setup(BrowserSetup setup) {
|
||||
browser.MenuHandler = SetupContextMenu(setup.ContextMenuHandler);
|
||||
browser.ResourceRequestHandlerFactory = SetupResourceHandlerFactory(setup.ResourceRequestHandler);
|
||||
}
|
||||
|
||||
protected abstract ContextMenuBase SetupContextMenu(IContextMenuHandler handler);
|
||||
|
||||
protected abstract CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler);
|
||||
|
||||
private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
|
||||
if (!e.IsLoading) {
|
||||
Ready = true;
|
||||
browser.LoadingStateChanged -= OnLoadingStateChanged;
|
||||
BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser));
|
||||
BrowserLoaded = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs {
|
||||
private readonly IWebBrowser browser;
|
||||
|
||||
public BrowserLoadedEventArgsImpl(IWebBrowser browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
public override void AddDictionaryWords(params string[] words) {
|
||||
foreach (string word in words) {
|
||||
browser.AddWordToDictionary(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoadError(object sender, LoadErrorEventArgs e) {
|
||||
if (e.ErrorCode == CefErrorCode.Aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.FailedUrl.StartsWithOrdinal("td://resources/error/")) {
|
||||
string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
|
||||
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
|
||||
browser.Load("td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) {
|
||||
if (e.Frame.IsMain) {
|
||||
PageLoadStart?.Invoke(this, new PageLoadEventArgs(e.Url));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
|
||||
if (e.Frame.IsMain) {
|
||||
PageLoadEnd?.Invoke(this, new PageLoadEventArgs(e.Url));
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachBridgeObject(string name, object bridge) {
|
||||
browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
|
||||
browser.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder);
|
||||
}
|
||||
|
||||
public void RunFunction(string name, params object[] args) {
|
||||
browser.BrowserCore.ExecuteScriptAsync(name, args);
|
||||
}
|
||||
|
||||
public void RunScript(string identifier, string script) {
|
||||
using IFrame frame = browser.GetMainFrame();
|
||||
frame.ExecuteJavaScriptAsync(script, identifier, 1);
|
||||
}
|
||||
}
|
||||
}
|
28
Browser/Adapters/CefContextMenuActionRegistry.cs
Normal file
28
Browser/Adapters/CefContextMenuActionRegistry.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CefSharp;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal sealed class CefContextMenuActionRegistry {
|
||||
private readonly Dictionary<CefMenuCommand, Action> actions = new Dictionary<CefMenuCommand, Action>();
|
||||
|
||||
public CefMenuCommand AddAction(Action action) {
|
||||
CefMenuCommand id = CefMenuCommand.UserFirst + 500 + actions.Count;
|
||||
actions[id] = action;
|
||||
return id;
|
||||
}
|
||||
|
||||
public bool Execute(CefMenuCommand id) {
|
||||
if (actions.TryGetValue(id, out var action)) {
|
||||
action();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
actions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
80
Browser/Adapters/CefContextMenuModel.cs
Normal file
80
Browser/Adapters/CefContextMenuModel.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
using TweetLib.Browser.Contexts;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal sealed class CefContextMenuModel : IContextMenuBuilder {
|
||||
private readonly IMenuModel model;
|
||||
private readonly CefContextMenuActionRegistry actionRegistry;
|
||||
|
||||
public CefContextMenuModel(IMenuModel model, CefContextMenuActionRegistry actionRegistry) {
|
||||
this.model = model;
|
||||
this.actionRegistry = actionRegistry;
|
||||
}
|
||||
|
||||
public void AddAction(string name, Action action) {
|
||||
var id = actionRegistry.AddAction(action);
|
||||
model.AddItem(id, name);
|
||||
}
|
||||
|
||||
public void AddActionWithCheck(string name, bool isChecked, Action action) {
|
||||
var id = actionRegistry.AddAction(action);
|
||||
model.AddCheckItem(id, name);
|
||||
model.SetChecked(id, isChecked);
|
||||
}
|
||||
|
||||
public void AddSeparator() {
|
||||
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate
|
||||
model.AddSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
public static Context CreateContext(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) {
|
||||
var context = new Context();
|
||||
var flags = parameters.TypeFlags;
|
||||
|
||||
var tweet = extraContext?.Tweet;
|
||||
if (tweet != null && !flags.HasFlag(ContextMenuType.Editable)) {
|
||||
context.Tweet = tweet;
|
||||
}
|
||||
|
||||
context.Link = GetLink(parameters, extraContext);
|
||||
context.Media = GetMedia(parameters, extraContext, imageQuality);
|
||||
|
||||
if (flags.HasFlag(ContextMenuType.Selection)) {
|
||||
context.Selection = new Selection(parameters.SelectionText, flags.HasFlag(ContextMenuType.Editable));
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static Link? GetLink(IContextMenuParams parameters, TweetDeckExtraContext extraContext) {
|
||||
var link = extraContext?.Link;
|
||||
if (link != null) {
|
||||
return link;
|
||||
}
|
||||
|
||||
if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && extraContext?.Media == null) {
|
||||
return new Link(parameters.LinkUrl, parameters.UnfilteredLinkUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Media? GetMedia(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) {
|
||||
var media = extraContext?.Media;
|
||||
if (media != null) {
|
||||
return media;
|
||||
}
|
||||
|
||||
if (parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) {
|
||||
return new Media(Media.Type.Image, TwitterUrls.GetMediaLink(parameters.SourceUrl, imageQuality));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
23
Browser/Adapters/CefResourceHandlerFactory.cs
Normal file
23
Browser/Adapters/CefResourceHandlerFactory.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CefSharp;
|
||||
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
sealed class CefResourceHandlerFactory : IResourceRequestHandlerFactory {
|
||||
bool IResourceRequestHandlerFactory.HasHandlers => registry != null;
|
||||
|
||||
private readonly CefResourceRequestHandler resourceRequestHandler;
|
||||
private readonly CefResourceHandlerRegistry registry;
|
||||
|
||||
public CefResourceHandlerFactory(IResourceRequestHandler resourceRequestHandler, CefResourceHandlerRegistry registry) {
|
||||
this.resourceRequestHandler = new CefResourceRequestHandler(registry, resourceRequestHandler);
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "RedundantAssignment")]
|
||||
CefSharp.IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) {
|
||||
disableDefaultHandling = registry != null && registry.HasHandler(request.Url);
|
||||
return resourceRequestHandler;
|
||||
}
|
||||
}
|
||||
}
|
42
Browser/Adapters/CefResourceHandlerRegistry.cs
Normal file
42
Browser/Adapters/CefResourceHandlerRegistry.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal sealed class CefResourceHandlerRegistry {
|
||||
private readonly ConcurrentDictionary<string, Func<IResourceHandler>> resourceHandlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public bool HasHandler(string url) {
|
||||
return resourceHandlers.ContainsKey(url);
|
||||
}
|
||||
|
||||
public IResourceHandler GetHandler(string url) {
|
||||
return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null;
|
||||
}
|
||||
|
||||
private void Register(string url, Func<IResourceHandler> factory) {
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
|
||||
throw new ArgumentException("Resource handler URL must be absolute!");
|
||||
}
|
||||
|
||||
resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory);
|
||||
}
|
||||
|
||||
public void RegisterStatic(string url, byte[] staticData, string mimeType = ResourceHandler.DefaultMimeType) {
|
||||
Register(url, () => ResourceHandler.FromByteArray(staticData, mimeType));
|
||||
}
|
||||
|
||||
public void RegisterStatic(string url, string staticData, string mimeType = ResourceHandler.DefaultMimeType) {
|
||||
Register(url, () => ResourceHandler.FromString(staticData, Encoding.UTF8, mimeType: mimeType));
|
||||
}
|
||||
|
||||
public void RegisterDynamic(string url, IResourceHandler handler) {
|
||||
Register(url, () => handler);
|
||||
}
|
||||
|
||||
public void Unregister(string url) {
|
||||
resourceHandlers.TryRemove(url, out _);
|
||||
}
|
||||
}
|
||||
}
|
31
Browser/Adapters/CefResourceProvider.cs
Normal file
31
Browser/Adapters/CefResourceProvider.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal sealed class ResourceProvider : IResourceProvider<IResourceHandler> {
|
||||
public IResourceHandler Status(HttpStatusCode code, string message) {
|
||||
var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
|
||||
handler.StatusCode = (int) code;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public IResourceHandler File(byte[] contents, string extension) {
|
||||
if (contents.Length == 0) {
|
||||
return Status(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
|
||||
}
|
||||
|
||||
var handler = CreateHandler(contents);
|
||||
handler.MimeType = Cef.GetMimeType(extension);
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static ResourceHandler CreateHandler(byte[] bytes) {
|
||||
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
|
||||
handler.Headers.Set("Access-Control-Allow-Origin", "*");
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
77
Browser/Adapters/CefResourceRequestHandler.cs
Normal file
77
Browser/Adapters/CefResourceRequestHandler.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using CefSharp;
|
||||
using CefSharp.Handler;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Browser.Request;
|
||||
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
|
||||
using ResourceType = TweetLib.Browser.Request.ResourceType;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
sealed class CefResourceRequestHandler : ResourceRequestHandler {
|
||||
private readonly CefResourceHandlerRegistry resourceHandlerRegistry;
|
||||
private readonly IResourceRequestHandler resourceRequestHandler;
|
||||
private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new Dictionary<ulong, IResponseProcessor>();
|
||||
|
||||
public CefResourceRequestHandler(CefResourceHandlerRegistry resourceHandlerRegistry, IResourceRequestHandler resourceRequestHandler) {
|
||||
this.resourceHandlerRegistry = resourceHandlerRegistry;
|
||||
this.resourceRequestHandler = resourceRequestHandler;
|
||||
}
|
||||
|
||||
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
|
||||
if (request.ResourceType == CefSharp.ResourceType.CspReport) {
|
||||
callback.Dispose();
|
||||
return CefReturnValue.Cancel;
|
||||
}
|
||||
|
||||
if (resourceRequestHandler != null) {
|
||||
var result = resourceRequestHandler.Handle(request.Url, TranslateResourceType(request.ResourceType));
|
||||
|
||||
switch (result) {
|
||||
case RequestHandleResult.Redirect redirect:
|
||||
request.Url = redirect.Url;
|
||||
break;
|
||||
|
||||
case RequestHandleResult.Process process:
|
||||
request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true);
|
||||
responseProcessors[request.Identifier] = process.Processor;
|
||||
break;
|
||||
|
||||
case RequestHandleResult.Cancel _:
|
||||
callback.Dispose();
|
||||
return CefReturnValue.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnBeforeResourceLoad(chromiumWebBrowser, browser, frame, request, callback);
|
||||
}
|
||||
|
||||
protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) {
|
||||
return resourceHandlerRegistry?.GetHandler(request.Url);
|
||||
}
|
||||
|
||||
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
|
||||
if (responseProcessors.TryGetValue(request.Identifier, out var processor) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)) {
|
||||
return new ResponseFilter(processor, totalBytes);
|
||||
}
|
||||
|
||||
return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
|
||||
}
|
||||
|
||||
protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) {
|
||||
responseProcessors.Remove(request.Identifier);
|
||||
base.OnResourceLoadComplete(chromiumWebBrowser, browser, frame, request, response, status, receivedContentLength);
|
||||
}
|
||||
|
||||
private static ResourceType TranslateResourceType(CefSharp.ResourceType resourceType) {
|
||||
return resourceType switch {
|
||||
CefSharp.ResourceType.MainFrame => ResourceType.MainFrame,
|
||||
CefSharp.ResourceType.Script => ResourceType.Script,
|
||||
CefSharp.ResourceType.Stylesheet => ResourceType.Stylesheet,
|
||||
CefSharp.ResourceType.Xhr => ResourceType.Xhr,
|
||||
CefSharp.ResourceType.Image => ResourceType.Image,
|
||||
_ => ResourceType.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
29
Browser/Adapters/CefSchemeHandlerFactory.cs
Normal file
29
Browser/Adapters/CefSchemeHandlerFactory.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory {
|
||||
public static void Register(CefSettings settings, ICustomSchemeHandler<IResourceHandler> handler) {
|
||||
settings.RegisterScheme(new CefCustomScheme {
|
||||
SchemeName = handler.Protocol,
|
||||
IsStandard = false,
|
||||
IsSecure = true,
|
||||
IsCorsEnabled = true,
|
||||
IsCSPBypassing = true,
|
||||
SchemeHandlerFactory = new CefSchemeHandlerFactory(handler)
|
||||
});
|
||||
}
|
||||
|
||||
private readonly ICustomSchemeHandler<IResourceHandler> handler;
|
||||
|
||||
private CefSchemeHandlerFactory(ICustomSchemeHandler<IResourceHandler> handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
|
||||
return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri) : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CefSharp;
|
||||
using TweetDuck.Resources;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Browser;
|
||||
|
||||
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 = ResourceUtils.ReadFileOrNull("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.Utils.Static;
|
||||
|
||||
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.Utils.Static;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
2
Browser/FormBrowser.Designer.cs
generated
2
Browser/FormBrowser.Designer.cs
generated
@ -27,7 +27,7 @@ private void InitializeComponent() {
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = TweetDuck.Utils.TwitterUtils.BackgroundColor;
|
||||
this.BackColor = TweetDuck.Browser.TweetDeckBrowser.BackgroundColor;
|
||||
this.ClientSize = new System.Drawing.Size(1008, 730);
|
||||
this.Icon = Properties.Resources.icon;
|
||||
this.Location = TweetDuck.Controls.ControlExtensions.InvisibleLocation;
|
||||
|
@ -5,9 +5,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Bridge;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Browser.Notification.Screenshot;
|
||||
using TweetDuck.Configuration;
|
||||
@ -15,17 +13,18 @@
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Dialogs.Settings;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Resources;
|
||||
using TweetDuck.Updates;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
using TweetLib.Core.Resources;
|
||||
using TweetLib.Core.Systems.Updates;
|
||||
|
||||
namespace TweetDuck.Browser {
|
||||
sealed partial class FormBrowser : Form {
|
||||
sealed partial class FormBrowser : Form, CustomKeyboardHandler.IBrowserKeyHandler {
|
||||
private static UserConfig Config => Program.Config.User;
|
||||
|
||||
public bool IsWaiting {
|
||||
@ -46,18 +45,17 @@ public bool IsWaiting {
|
||||
}
|
||||
|
||||
public UpdateInstaller UpdateInstaller { get; private set; }
|
||||
private bool ignoreUpdateCheckError;
|
||||
|
||||
#pragma warning disable IDE0069 // Disposable fields should be disposed
|
||||
private readonly TweetDeckBrowser browser;
|
||||
private readonly FormNotificationTweet notification;
|
||||
#pragma warning restore IDE0069 // Disposable fields should be disposed
|
||||
|
||||
private readonly ResourceProvider resourceProvider;
|
||||
private readonly CachingResourceProvider<IResourceHandler> resourceProvider;
|
||||
private readonly ITweetDeckInterface tweetDeckInterface;
|
||||
private readonly PluginManager plugins;
|
||||
private readonly UpdateHandler updates;
|
||||
private readonly UpdateChecker updates;
|
||||
private readonly ContextMenu contextMenu;
|
||||
private readonly UpdateBridge updateBridge;
|
||||
|
||||
private bool isLoaded;
|
||||
private FormWindowState prevState;
|
||||
@ -65,30 +63,25 @@ public bool IsWaiting {
|
||||
private TweetScreenshotManager notificationScreenshotManager;
|
||||
private VideoPlayer videoPlayer;
|
||||
|
||||
public FormBrowser(ResourceProvider resourceProvider, PluginSchemeFactory pluginScheme) {
|
||||
public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, PluginManager pluginManager, IUpdateCheckClient updateCheckClient) {
|
||||
InitializeComponent();
|
||||
|
||||
Text = Program.BrandName;
|
||||
|
||||
this.resourceProvider = resourceProvider;
|
||||
|
||||
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
|
||||
this.plugins.Reloaded += plugins_Reloaded;
|
||||
this.plugins.Executed += plugins_Executed;
|
||||
this.plugins.Reload();
|
||||
pluginScheme.Setup(plugins);
|
||||
this.plugins = pluginManager;
|
||||
|
||||
this.notification = new FormNotificationTweet(this, plugins);
|
||||
this.tweetDeckInterface = new TweetDeckInterfaceImpl(this);
|
||||
|
||||
this.notification = new FormNotificationTweet(this, tweetDeckInterface, plugins);
|
||||
this.notification.Show();
|
||||
|
||||
this.updates = new UpdateHandler(new UpdateCheckClient(Program.InstallerPath), TaskScheduler.FromCurrentSynchronizationContext());
|
||||
this.updates.CheckFinished += updates_CheckFinished;
|
||||
this.updates = new UpdateChecker(updateCheckClient, TaskScheduler.FromCurrentSynchronizationContext());
|
||||
this.updates.InteractionManager.UpdateAccepted += updateInteractionManager_UpdateAccepted;
|
||||
this.updates.InteractionManager.UpdateDismissed += updateInteractionManager_UpdateDismissed;
|
||||
|
||||
this.updateBridge = new UpdateBridge(updates, this);
|
||||
this.updateBridge.UpdateAccepted += updateBridge_UpdateAccepted;
|
||||
this.updateBridge.UpdateDismissed += updateBridge_UpdateDismissed;
|
||||
|
||||
this.browser = new TweetDeckBrowser(this, plugins, new TweetDeckBridge.Browser(this, notification), updateBridge);
|
||||
this.browser = new TweetDeckBrowser(this, plugins, tweetDeckInterface, updates);
|
||||
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
|
||||
|
||||
Controls.Add(new MenuStrip { Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
|
||||
@ -233,7 +226,8 @@ private void FormBrowser_FormClosing(object sender, FormClosingEventArgs e) {
|
||||
|
||||
private void FormBrowser_FormClosed(object sender, FormClosedEventArgs e) {
|
||||
if (isLoaded && UpdateInstaller == null) {
|
||||
updateBridge.Cleanup();
|
||||
updates.InteractionManager.ClearUpdate();
|
||||
updates.InteractionManager.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,94 +250,61 @@ private void trayIcon_ClickClose(object sender, EventArgs e) {
|
||||
ForceClose();
|
||||
}
|
||||
|
||||
private void plugins_Reloaded(object sender, PluginErrorEventArgs e) {
|
||||
if (e.HasErrors) {
|
||||
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
|
||||
}
|
||||
private void updateInteractionManager_UpdateAccepted(object sender, UpdateInfo update) {
|
||||
this.InvokeAsyncSafe(() => {
|
||||
FormManager.CloseAllDialogs();
|
||||
|
||||
if (isLoaded) {
|
||||
browser.ReloadToTweetDeck();
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(Config.DismissedUpdate)) {
|
||||
Config.DismissedUpdate = null;
|
||||
Config.Save();
|
||||
}
|
||||
|
||||
private void plugins_Executed(object sender, PluginErrorEventArgs e) {
|
||||
if (e.HasErrors) {
|
||||
this.InvokeAsyncSafe(() => { FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK); });
|
||||
}
|
||||
}
|
||||
void OnFinished() {
|
||||
UpdateDownloadStatus status = update.DownloadStatus;
|
||||
|
||||
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e) {
|
||||
e.Result.Handle(update => {
|
||||
string tag = update.VersionTag;
|
||||
if (status == UpdateDownloadStatus.Done) {
|
||||
UpdateInstaller = new UpdateInstaller(update.InstallerPath);
|
||||
ForceClose();
|
||||
}
|
||||
else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: " + (update.DownloadError?.Message ?? "unknown error") + "\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)) {
|
||||
App.SystemHandler.OpenBrowser(Program.Website);
|
||||
ForceClose();
|
||||
}
|
||||
else {
|
||||
Show();
|
||||
}
|
||||
}
|
||||
|
||||
if (tag != Program.VersionTag && tag != Config.DismissedUpdate) {
|
||||
update.BeginSilentDownload();
|
||||
browser.ShowUpdateNotification(tag, update.ReleaseNotes);
|
||||
if (update.DownloadStatus.IsFinished(true)) {
|
||||
OnFinished();
|
||||
}
|
||||
else {
|
||||
updates.StartTimer();
|
||||
}
|
||||
}, ex => {
|
||||
if (!ignoreUpdateCheckError) {
|
||||
App.ErrorHandler.HandleException("Update Check Error", "An error occurred while checking for updates.", true, ex);
|
||||
updates.StartTimer();
|
||||
FormUpdateDownload downloadForm = new FormUpdateDownload(update);
|
||||
|
||||
downloadForm.VisibleChanged += (sender2, args2) => {
|
||||
downloadForm.MoveToCenter(this);
|
||||
Hide();
|
||||
};
|
||||
|
||||
downloadForm.FormClosed += (sender2, args2) => {
|
||||
if (downloadForm.DialogResult != DialogResult.OK) {
|
||||
update.CancelDownload();
|
||||
}
|
||||
|
||||
downloadForm.Dispose();
|
||||
OnFinished();
|
||||
};
|
||||
|
||||
downloadForm.Show();
|
||||
}
|
||||
});
|
||||
|
||||
ignoreUpdateCheckError = true;
|
||||
}
|
||||
|
||||
private void updateBridge_UpdateAccepted(object sender, UpdateInfo update) {
|
||||
FormManager.CloseAllDialogs();
|
||||
|
||||
if (!string.IsNullOrEmpty(Config.DismissedUpdate)) {
|
||||
Config.DismissedUpdate = null;
|
||||
private void updateInteractionManager_UpdateDismissed(object sender, UpdateInfo update) {
|
||||
this.InvokeAsyncSafe(() => {
|
||||
Config.DismissedUpdate = update.VersionTag;
|
||||
Config.Save();
|
||||
}
|
||||
|
||||
void OnFinished() {
|
||||
UpdateDownloadStatus status = update.DownloadStatus;
|
||||
|
||||
if (status == UpdateDownloadStatus.Done) {
|
||||
UpdateInstaller = new UpdateInstaller(update.InstallerPath);
|
||||
ForceClose();
|
||||
}
|
||||
else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: " + (update.DownloadError?.Message ?? "unknown error") + "\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)) {
|
||||
BrowserUtils.OpenExternalBrowser(Program.Website);
|
||||
ForceClose();
|
||||
}
|
||||
else {
|
||||
Show();
|
||||
}
|
||||
}
|
||||
|
||||
if (update.DownloadStatus.IsFinished(true)) {
|
||||
OnFinished();
|
||||
}
|
||||
else {
|
||||
FormUpdateDownload downloadForm = new FormUpdateDownload(update);
|
||||
|
||||
downloadForm.VisibleChanged += (sender2, args2) => {
|
||||
downloadForm.MoveToCenter(this);
|
||||
Hide();
|
||||
};
|
||||
|
||||
downloadForm.FormClosed += (sender2, args2) => {
|
||||
if (downloadForm.DialogResult != DialogResult.OK) {
|
||||
update.CancelDownload();
|
||||
}
|
||||
|
||||
downloadForm.Dispose();
|
||||
OnFinished();
|
||||
};
|
||||
|
||||
downloadForm.Show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBridge_UpdateDismissed(object sender, UpdateInfo update) {
|
||||
Config.DismissedUpdate = update.VersionTag;
|
||||
Config.Save();
|
||||
});
|
||||
}
|
||||
|
||||
protected override void WndProc(ref Message m) {
|
||||
@ -358,11 +319,11 @@ protected override void WndProc(ref Message m) {
|
||||
}
|
||||
|
||||
if (browser.Ready && m.Msg == NativeMethods.WM_PARENTNOTIFY && (m.WParam.ToInt32() & 0xFFFF) == NativeMethods.WM_XBUTTONDOWN) {
|
||||
if (videoPlayer != null && videoPlayer.Running) {
|
||||
if (videoPlayer is { Running: true }) {
|
||||
videoPlayer.Close();
|
||||
}
|
||||
else {
|
||||
browser.OnMouseClickExtra(m.WParam);
|
||||
browser.Functions.OnMouseClickExtra((m.WParam.ToInt32() >> 16) & 0xFFFF);
|
||||
}
|
||||
|
||||
return;
|
||||
@ -373,10 +334,6 @@ protected override void WndProc(ref Message m) {
|
||||
|
||||
// bridge methods
|
||||
|
||||
public void OnModulesLoaded(string moduleNamespace) {
|
||||
browser.OnModulesLoaded(moduleNamespace);
|
||||
}
|
||||
|
||||
public void PauseNotification() {
|
||||
notification.PauseNotification();
|
||||
}
|
||||
@ -385,10 +342,6 @@ public void ResumeNotification() {
|
||||
notification.ResumeNotification();
|
||||
}
|
||||
|
||||
public void ReinjectCustomCSS(string css) {
|
||||
browser.ReinjectCustomCSS(css);
|
||||
}
|
||||
|
||||
public void ReloadToTweetDeck() {
|
||||
#if DEBUG
|
||||
ResourceHotSwap.Run();
|
||||
@ -399,30 +352,9 @@ public void ReloadToTweetDeck() {
|
||||
}
|
||||
#endif
|
||||
|
||||
ignoreUpdateCheckError = false;
|
||||
browser.ReloadToTweetDeck();
|
||||
}
|
||||
|
||||
public void AddSearchColumn(string query) {
|
||||
browser.AddSearchColumn(query);
|
||||
}
|
||||
|
||||
public void TriggerTweetScreenshot(string columnId, string chirpId) {
|
||||
browser.TriggerTweetScreenshot(columnId, chirpId);
|
||||
}
|
||||
|
||||
public void ReloadColumns() {
|
||||
browser.ReloadColumns();
|
||||
}
|
||||
|
||||
public void PlaySoundNotification() {
|
||||
browser.PlaySoundNotification();
|
||||
}
|
||||
|
||||
public void ApplyROT13() {
|
||||
browser.ApplyROT13();
|
||||
}
|
||||
|
||||
public void OpenDevTools() {
|
||||
browser.OpenDevTools();
|
||||
}
|
||||
@ -452,7 +384,7 @@ public void OpenSettings(Type startTab) {
|
||||
if (!FormManager.TryBringToFront<FormSettings>()) {
|
||||
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
|
||||
|
||||
FormSettings form = new FormSettings(this, plugins, updates, startTab);
|
||||
FormSettings form = new FormSettings(this, plugins, updates, browser.Functions, startTab);
|
||||
|
||||
form.FormClosed += (sender, args) => {
|
||||
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck) {
|
||||
@ -473,7 +405,7 @@ public void OpenSettings(Type startTab) {
|
||||
plugins.Reload(); // also reloads the browser
|
||||
}
|
||||
else {
|
||||
browser.UpdateProperties();
|
||||
Program.Config.User.TriggerOptionsDialogClosed();
|
||||
}
|
||||
|
||||
notification.RequiresResize = true;
|
||||
@ -508,13 +440,19 @@ public void OpenProfileImport() {
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowDesktopNotification(DesktopNotification notification) {
|
||||
this.notification.ShowNotification(notification);
|
||||
}
|
||||
|
||||
public void OnTweetNotification() { // may be called multiple times, once for each type of notification
|
||||
if (Config.EnableTrayHighlight && !ContainsFocus) {
|
||||
trayIcon.HasNotifications = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnTweetSound() {}
|
||||
public void SaveVideo(string url, string username) {
|
||||
browser.SaveVideo(url, username);
|
||||
}
|
||||
|
||||
public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
|
||||
string playerPath = Config.VideoPlayerPath;
|
||||
@ -548,25 +486,16 @@ public void StopVideo() {
|
||||
videoPlayer?.Close();
|
||||
}
|
||||
|
||||
public bool ProcessBrowserKey(Keys key) {
|
||||
if (videoPlayer != null && videoPlayer.Running) {
|
||||
videoPlayer.SendKeyEvent(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl) {
|
||||
public bool ShowTweetDetail(string columnId, string chirpId, string fallbackUrl) {
|
||||
Activate();
|
||||
|
||||
if (!browser.IsTweetDeckWebsite) {
|
||||
FormMessage.Error("View Tweet Detail", "TweetDeck is not currently loaded.", FormMessage.OK);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
notification.FinishCurrentNotification();
|
||||
browser.ShowTweetDetail(columnId, chirpId, fallbackUrl);
|
||||
browser.Functions.ShowTweetDetail(columnId, chirpId, fallbackUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnTweetScreenshotReady(string html, int width) {
|
||||
@ -584,5 +513,18 @@ public void DisplayTooltip(string text) {
|
||||
toolTip.Show(text, this, position);
|
||||
}
|
||||
}
|
||||
|
||||
public FormNotificationExample CreateExampleNotification() {
|
||||
return new FormNotificationExample(this, tweetDeckInterface, plugins);
|
||||
}
|
||||
|
||||
bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) {
|
||||
if (videoPlayer is { Running: true }) {
|
||||
videoPlayer.SendKeyEvent(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
using CefSharp;
|
||||
using TweetDuck.Configuration;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class BrowserProcessHandler : IBrowserProcessHandler {
|
||||
public static Task UpdatePrefs() {
|
||||
return Cef.UIThreadTaskFactory.StartNew(UpdatePrefsInternal);
|
@ -1,213 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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.Utils.Static;
|
||||
using TweetLib.Browser.Contexts;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
abstract class ContextMenuBase : IContextMenuHandler {
|
||||
public static ContextInfo CurrentInfo { get; } = new ContextInfo();
|
||||
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26500;
|
||||
|
||||
private static readonly HashSet<CefMenuCommand> AllowedCefCommands = new HashSet<CefMenuCommand> {
|
||||
CefMenuCommand.NotFound,
|
||||
CefMenuCommand.Undo,
|
||||
CefMenuCommand.Redo,
|
||||
CefMenuCommand.Cut,
|
||||
CefMenuCommand.Copy,
|
||||
CefMenuCommand.Paste,
|
||||
CefMenuCommand.Delete,
|
||||
CefMenuCommand.SelectAll,
|
||||
CefMenuCommand.SpellCheckSuggestion0,
|
||||
CefMenuCommand.SpellCheckSuggestion1,
|
||||
CefMenuCommand.SpellCheckSuggestion2,
|
||||
CefMenuCommand.SpellCheckSuggestion3,
|
||||
CefMenuCommand.SpellCheckSuggestion4,
|
||||
CefMenuCommand.SpellCheckNoSuggestions,
|
||||
CefMenuCommand.AddToDictionary
|
||||
};
|
||||
|
||||
protected static UserConfig Config => Program.Config.User;
|
||||
private 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;
|
||||
private readonly TweetLib.Browser.Interfaces.IContextMenuHandler handler;
|
||||
private readonly CefContextMenuActionRegistry actionRegistry;
|
||||
|
||||
protected ContextInfo.ContextData Context { get; private set; }
|
||||
protected ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) {
|
||||
this.handler = handler;
|
||||
this.actionRegistry = new CefContextMenuActionRegistry();
|
||||
}
|
||||
|
||||
protected virtual Context CreateContext(IContextMenuParams parameters) {
|
||||
return CefContextMenuModel.CreateContext(parameters, null, Config.TwitterImageQuality);
|
||||
}
|
||||
|
||||
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
|
||||
if (!TwitterUrls.IsTweetDeck(frame.Url) || browser.IsLoading) {
|
||||
Context = CurrentInfo.Reset();
|
||||
}
|
||||
else {
|
||||
Context = CurrentInfo.Create(parameters);
|
||||
}
|
||||
for (int i = model.Count - 1; i >= 0; i--) {
|
||||
CefMenuCommand command = model.GetCommandIdAt(i);
|
||||
|
||||
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");
|
||||
if (!AllowedCefCommands.Contains(command) && !(command >= CefMenuCommand.CustomFirst && command <= CefMenuCommand.CustomLast)) {
|
||||
model.RemoveAt(i);
|
||||
}
|
||||
else {
|
||||
model.AddItem(MenuOpenLinkUrl, TextOpen("link"));
|
||||
model.AddItem(MenuCopyLinkUrl, TextCopy("link"));
|
||||
}
|
||||
|
||||
for (int i = model.Count - 2; i >= 0; i--) {
|
||||
if (model.GetTypeAt(i) == MenuItemType.Separator && model.GetTypeAt(i + 1) == MenuItemType.Separator) {
|
||||
model.RemoveAt(i);
|
||||
}
|
||||
|
||||
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();
|
||||
if (model.Count > 0 && model.GetTypeAt(0) == MenuItemType.Separator) {
|
||||
model.RemoveAt(0);
|
||||
}
|
||||
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();
|
||||
}
|
||||
AddSeparator(model);
|
||||
handler.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters));
|
||||
RemoveSeparatorIfLast(model);
|
||||
}
|
||||
|
||||
public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
|
||||
Control control = browserControl.AsControl();
|
||||
if (actionRegistry.Execute(commandId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
if (commandId == MenuOpenDevTools) {
|
||||
browserControl.OpenDevToolsCustom(new Point(parameters.XCoord, parameters.YCoord));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
|
||||
Context = CurrentInfo.Reset();
|
||||
actionRegistry.Clear();
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -1,8 +1,12 @@
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Browser.Contexts;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class ContextMenuBrowser : ContextMenuBase {
|
||||
@ -12,14 +16,6 @@ sealed class ContextMenuBrowser : ContextMenuBase {
|
||||
private const CefMenuCommand MenuPlugins = (CefMenuCommand) 26003;
|
||||
private const CefMenuCommand MenuAbout = (CefMenuCommand) 26604;
|
||||
|
||||
private const CefMenuCommand MenuOpenTweetUrl = (CefMenuCommand) 26610;
|
||||
private const CefMenuCommand MenuCopyTweetUrl = (CefMenuCommand) 26611;
|
||||
private const CefMenuCommand MenuOpenQuotedTweetUrl = (CefMenuCommand) 26612;
|
||||
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand) 26613;
|
||||
private const CefMenuCommand MenuScreenshotTweet = (CefMenuCommand) 26614;
|
||||
private const CefMenuCommand MenuWriteApplyROT13 = (CefMenuCommand) 26615;
|
||||
private const CefMenuCommand MenuSearchInColumn = (CefMenuCommand) 26616;
|
||||
|
||||
private const string TitleReloadBrowser = "Reload browser";
|
||||
private const string TitleMuteNotifications = "Mute notifications";
|
||||
private const string TitleSettings = "Options";
|
||||
@ -27,49 +23,26 @@ sealed class ContextMenuBrowser : ContextMenuBase {
|
||||
private const string TitleAboutProgram = "About " + Program.BrandName;
|
||||
|
||||
private readonly FormBrowser form;
|
||||
private readonly TweetDeckExtraContext extraContext;
|
||||
|
||||
public ContextMenuBrowser(FormBrowser form) {
|
||||
public ContextMenuBrowser(FormBrowser form, IContextMenuHandler handler, TweetDeckExtraContext extraContext) : base(handler) {
|
||||
this.form = form;
|
||||
this.extraContext = extraContext;
|
||||
}
|
||||
|
||||
protected override Context CreateContext(IContextMenuParams parameters) {
|
||||
return CefContextMenuModel.CreateContext(parameters, extraContext, Config.TwitterImageQuality);
|
||||
}
|
||||
|
||||
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
|
||||
bool isSelecting = parameters.TypeFlags.HasFlag(ContextMenuType.Selection);
|
||||
bool isEditing = parameters.TypeFlags.HasFlag(ContextMenuType.Editable);
|
||||
|
||||
model.Remove(CefMenuCommand.Back);
|
||||
model.Remove(CefMenuCommand.Forward);
|
||||
model.Remove(CefMenuCommand.Print);
|
||||
model.Remove(CefMenuCommand.ViewSource);
|
||||
RemoveSeparatorIfLast(model);
|
||||
|
||||
if (isSelecting) {
|
||||
if (isEditing) {
|
||||
model.AddSeparator();
|
||||
model.AddItem(MenuWriteApplyROT13, "Apply ROT13");
|
||||
}
|
||||
|
||||
model.AddSeparator();
|
||||
if (!TwitterUrls.IsTweetDeck(frame.Url) || browser.IsLoading) {
|
||||
extraContext.Reset();
|
||||
}
|
||||
|
||||
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
|
||||
|
||||
if (isSelecting && !isEditing && TwitterUrls.IsTweetDeck(frame.Url)) {
|
||||
InsertSelectionSearchItem(model, MenuSearchInColumn, "Search in a column");
|
||||
}
|
||||
|
||||
if (Context.Types.HasFlag(ContextInfo.ContextType.Chirp) && !isSelecting && !isEditing) {
|
||||
model.AddItem(MenuOpenTweetUrl, "Open tweet in browser");
|
||||
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
|
||||
model.AddItem(MenuScreenshotTweet, "Screenshot tweet to clipboard");
|
||||
|
||||
if (!string.IsNullOrEmpty(Context.Chirp.QuoteUrl)) {
|
||||
model.AddSeparator();
|
||||
model.AddItem(MenuOpenQuotedTweetUrl, "Open quoted tweet in browser");
|
||||
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
|
||||
}
|
||||
|
||||
model.AddSeparator();
|
||||
}
|
||||
bool isSelecting = parameters.TypeFlags.HasFlag(ContextMenuType.Selection);
|
||||
bool isEditing = parameters.TypeFlags.HasFlag(ContextMenuType.Editable);
|
||||
|
||||
if (!isSelecting && !isEditing) {
|
||||
AddSeparator(model);
|
||||
@ -87,8 +60,6 @@ public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser br
|
||||
|
||||
AddDebugMenuItems(globalMenu);
|
||||
}
|
||||
|
||||
RemoveSeparatorIfLast(model);
|
||||
}
|
||||
|
||||
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
|
||||
@ -117,41 +88,14 @@ public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser b
|
||||
form.InvokeAsyncSafe(ToggleMuteNotifications);
|
||||
return true;
|
||||
|
||||
case MenuOpenTweetUrl:
|
||||
OpenBrowser(form, Context.Chirp.TweetUrl);
|
||||
return true;
|
||||
|
||||
case MenuCopyTweetUrl:
|
||||
SetClipboardText(form, Context.Chirp.TweetUrl);
|
||||
return true;
|
||||
|
||||
case MenuScreenshotTweet:
|
||||
var chirp = Context.Chirp;
|
||||
|
||||
form.InvokeAsyncSafe(() => form.TriggerTweetScreenshot(chirp.ColumnId, chirp.ChirpId));
|
||||
|
||||
return true;
|
||||
|
||||
case MenuOpenQuotedTweetUrl:
|
||||
OpenBrowser(form, Context.Chirp.QuoteUrl);
|
||||
return true;
|
||||
|
||||
case MenuCopyQuotedTweetUrl:
|
||||
SetClipboardText(form, Context.Chirp.QuoteUrl);
|
||||
return true;
|
||||
|
||||
case MenuWriteApplyROT13:
|
||||
form.InvokeAsyncSafe(form.ApplyROT13);
|
||||
return true;
|
||||
|
||||
case MenuSearchInColumn:
|
||||
string query = parameters.SelectionText;
|
||||
form.InvokeAsyncSafe(() => form.AddSearchColumn(query));
|
||||
DeselectAll(frame);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
|
||||
base.OnContextMenuDismissed(browserControl, browser, frame);
|
||||
extraContext.Reset();
|
||||
}
|
||||
|
||||
public static ContextMenu CreateMenu(FormBrowser form) {
|
||||
|
@ -1,9 +1,11 @@
|
||||
using CefSharp;
|
||||
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class ContextMenuGuide : ContextMenuBase {
|
||||
public ContextMenuGuide(IContextMenuHandler handler) : base(handler) {}
|
||||
|
||||
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
|
||||
model.Clear();
|
||||
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
|
||||
AddDebugMenuItems(model);
|
||||
}
|
||||
|
@ -1,88 +1,27 @@
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Browser.Contexts;
|
||||
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
||||
|
||||
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) {
|
||||
public ContextMenuNotification(FormNotificationBase form, IContextMenuHandler handler) : base(handler) {
|
||||
this.form = form;
|
||||
this.enableCustomMenu = enableCustomMenu;
|
||||
}
|
||||
|
||||
protected override Context CreateContext(IContextMenuParams parameters) {
|
||||
Context context = base.CreateContext(parameters);
|
||||
context.Notification = new TweetLib.Browser.Contexts.Notification(form.CurrentTweetUrl, form.CurrentQuoteUrl);
|
||||
return context;
|
||||
}
|
||||
|
||||
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;
|
||||
form.InvokeAsyncSafe(() => form.ContextMenuOpen = true);
|
||||
}
|
||||
|
||||
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
|
||||
|
@ -1,32 +1,43 @@
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
class KeyboardHandlerBase : IKeyboardHandler {
|
||||
protected virtual bool HandleRawKey(IWebBrowser browserControl, Keys key, CefEventFlags modifiers) {
|
||||
sealed class CustomKeyboardHandler : IKeyboardHandler {
|
||||
private readonly IBrowserKeyHandler handler;
|
||||
|
||||
public CustomKeyboardHandler(IBrowserKeyHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut) {
|
||||
if (type != KeyType.RawKeyDown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
using (var frame = browser.FocusedFrame) {
|
||||
if (frame.Url.StartsWithOrdinal("devtools://")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Keys key = (Keys) windowsKeyCode;
|
||||
|
||||
if (modifiers == (CefEventFlags.ControlDown | CefEventFlags.ShiftDown) && key == Keys.I) {
|
||||
browserControl.OpenDevToolsCustom();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut) {
|
||||
if (type == KeyType.RawKeyDown) {
|
||||
using var frame = browser.FocusedFrame;
|
||||
|
||||
if (!frame.Url.StartsWith("devtools://")) {
|
||||
return HandleRawKey(browserControl, (Keys) windowsKeyCode, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return handler != null && handler.HandleBrowserKey(key);
|
||||
}
|
||||
|
||||
bool IKeyboardHandler.OnKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface IBrowserKeyHandler {
|
||||
bool HandleBrowserKey(Keys key);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,23 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
using CefSharp;
|
||||
using CefSharp.Handler;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class CustomLifeSpanHandler : LifeSpanHandler {
|
||||
private static bool IsPopupAllowed(string url) {
|
||||
return url.StartsWith("https://twitter.com/teams/authorize?", StringComparison.Ordinal) ||
|
||||
url.StartsWith("https://accounts.google.com/", StringComparison.Ordinal) ||
|
||||
url.StartsWith("https://appleid.apple.com/", StringComparison.Ordinal);
|
||||
return url.StartsWithOrdinal("https://twitter.com/teams/authorize?") ||
|
||||
url.StartsWithOrdinal("https://accounts.google.com/") ||
|
||||
url.StartsWithOrdinal("https://appleid.apple.com/");
|
||||
}
|
||||
|
||||
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl) {
|
||||
public static bool HandleLinkClick(WindowOpenDisposition targetDisposition, string targetUrl) {
|
||||
switch (targetDisposition) {
|
||||
case WindowOpenDisposition.NewBackgroundTab:
|
||||
case WindowOpenDisposition.NewForegroundTab:
|
||||
case WindowOpenDisposition.NewPopup when !IsPopupAllowed(targetUrl):
|
||||
case WindowOpenDisposition.NewWindow:
|
||||
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
|
||||
App.SystemHandler.OpenBrowser(targetUrl);
|
||||
return true;
|
||||
|
||||
default:
|
||||
@ -28,7 +27,7 @@ public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposi
|
||||
|
||||
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);
|
||||
return HandleLinkClick(targetDisposition, targetUrl);
|
||||
}
|
||||
|
||||
protected override bool DoClose(IWebBrowser browserControl, IBrowser browser) {
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using CefSharp;
|
||||
using CefSharp.Enums;
|
||||
using TweetDuck.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class DragHandlerBrowser : IDragHandler {
|
||||
@ -13,7 +12,7 @@ public DragHandlerBrowser(RequestHandlerBrowser 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);
|
||||
browserControl.BrowserCore.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", type, data);
|
||||
}
|
||||
|
||||
requestHandler.BlockNextUserNavUrl = dragData.LinkUrl; // empty if not a link
|
||||
|
@ -1,11 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class FileDialogHandler : IDialogHandler {
|
||||
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback) {
|
||||
if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple) {
|
||||
@ -54,7 +55,7 @@ private static IEnumerable<string> ParseFileType(string type) {
|
||||
case "video/quicktime": return new string[] { ".mov", ".qt" };
|
||||
}
|
||||
|
||||
System.Diagnostics.Debugger.Break();
|
||||
Debugger.Break();
|
||||
return StringUtils.EmptyArray;
|
||||
}
|
||||
}
|
@ -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,11 +1,12 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Utils;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.General {
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class JavaScriptDialogHandler : IJsDialogHandler {
|
||||
private static FormMessage CreateMessageForm(string caption, string text) {
|
||||
MessageBoxIcon icon = MessageBoxIcon.None;
|
||||
@ -29,7 +30,9 @@ private static FormMessage CreateMessageForm(string caption, string text) {
|
||||
}
|
||||
|
||||
bool IJsDialogHandler.OnJSDialog(IWebBrowser browserControl, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage) {
|
||||
browserControl.AsControl().InvokeSafe(() => {
|
||||
var control = (ChromiumWebBrowser) browserControl;
|
||||
|
||||
control.InvokeSafe(() => {
|
||||
FormMessage form;
|
||||
TextBox input = null;
|
||||
|
@ -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,6 +1,5 @@
|
||||
using CefSharp;
|
||||
using CefSharp.Handler;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
class RequestHandlerBase : RequestHandler {
|
||||
@ -11,7 +10,7 @@ public RequestHandlerBase(bool autoReload) {
|
||||
}
|
||||
|
||||
protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) {
|
||||
return CustomLifeSpanHandler.HandleLinkClick(browserControl, targetDisposition, targetUrl);
|
||||
return CustomLifeSpanHandler.HandleLinkClick(targetDisposition, targetUrl);
|
||||
}
|
||||
|
||||
protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) {
|
||||
|
@ -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.Utils.Static;
|
||||
|
||||
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,28 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetDuck.Browser.Handling.Filters {
|
||||
abstract class ResponseFilterBase : IResponseFilter {
|
||||
namespace TweetDuck.Browser.Handling {
|
||||
sealed class ResponseFilter : 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) {
|
||||
public ResponseFilter(IResponseProcessor processor, int totalBytes) {
|
||||
this.processor = processor;
|
||||
this.responseData = new byte[totalBytes];
|
||||
this.encoding = encoding;
|
||||
this.state = State.Reading;
|
||||
}
|
||||
|
||||
public bool InitFilter() {
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) {
|
||||
int responseLength = responseData.Length;
|
||||
|
||||
@ -36,7 +40,7 @@ FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream d
|
||||
dataOutWritten = 0;
|
||||
|
||||
if (offset >= responseLength) {
|
||||
responseData = encoding.GetBytes(ProcessResponse(encoding.GetString(responseData)));
|
||||
responseData = processor.Process(responseData);
|
||||
state = State.Writing;
|
||||
offset = 0;
|
||||
}
|
||||
@ -67,8 +71,6 @@ FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream d
|
||||
}
|
||||
}
|
||||
|
||||
public abstract bool InitFilter();
|
||||
protected abstract string ProcessResponse(string text);
|
||||
public abstract void Dispose();
|
||||
public void Dispose() {}
|
||||
}
|
||||
}
|
@ -1,35 +1,20 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Browser.Notification {
|
||||
abstract partial class FormNotificationBase : Form {
|
||||
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandlers.ForBytes(Properties.Resources.avatar, "image/png"));
|
||||
|
||||
protected const string BlankURL = TwitterUrls.TweetDeck + "/?blank";
|
||||
|
||||
public static string FontSize = null;
|
||||
public static string HeadLayout = null;
|
||||
|
||||
protected static UserConfig Config => Program.Config.User;
|
||||
|
||||
protected static int FontSizeLevel {
|
||||
get => FontSize switch {
|
||||
"largest" => 4,
|
||||
"large" => 3,
|
||||
"small" => 1,
|
||||
"smallest" => 0,
|
||||
_ => 2
|
||||
};
|
||||
}
|
||||
protected delegate NotificationBrowser CreateBrowserImplFunc(FormNotificationBase form, IBrowserComponent browserComponent);
|
||||
|
||||
protected virtual Point PrimaryLocation {
|
||||
get {
|
||||
@ -100,6 +85,9 @@ protected virtual FormBorderStyle NotificationBorderStyle {
|
||||
|
||||
private readonly FormBrowser owner;
|
||||
|
||||
protected readonly IBrowserComponent browserComponent;
|
||||
private readonly NotificationBrowser browserImpl;
|
||||
|
||||
#pragma warning disable IDE0069 // Disposable fields should be disposed
|
||||
protected readonly ChromiumWebBrowser browser;
|
||||
#pragma warning restore IDE0069 // Disposable fields should be disposed
|
||||
@ -112,43 +100,33 @@ protected virtual FormBorderStyle NotificationBorderStyle {
|
||||
public string CurrentTweetUrl => currentNotification?.TweetUrl;
|
||||
public string CurrentQuoteUrl => currentNotification?.QuoteUrl;
|
||||
|
||||
public bool CanViewDetail => currentNotification != null && !string.IsNullOrEmpty(currentNotification.ColumnId) && !string.IsNullOrEmpty(currentNotification.ChirpId);
|
||||
|
||||
protected bool IsPaused => pauseCounter > 0;
|
||||
protected bool IsCursorOverBrowser => browser.Bounds.Contains(PointToClient(Cursor.Position));
|
||||
protected internal bool IsCursorOverBrowser => browser.Bounds.Contains(PointToClient(Cursor.Position));
|
||||
|
||||
public bool FreezeTimer { get; set; }
|
||||
public bool ContextMenuOpen { get; set; }
|
||||
|
||||
protected FormNotificationBase(FormBrowser owner, bool enableContextMenu) {
|
||||
protected FormNotificationBase(FormBrowser owner, CreateBrowserImplFunc createBrowserImpl) {
|
||||
InitializeComponent();
|
||||
|
||||
this.owner = owner;
|
||||
this.owner.FormClosed += owner_FormClosed;
|
||||
|
||||
var resourceRequestHandler = new ResourceRequestHandlerBase();
|
||||
var resourceHandlers = resourceRequestHandler.ResourceHandlers;
|
||||
|
||||
resourceHandlers.Register(BlankURL, ResourceHandlers.ForString(string.Empty));
|
||||
resourceHandlers.Register(TwitterUrls.TweetDeck, () => this.resourceHandler);
|
||||
resourceHandlers.Register(AppLogo);
|
||||
|
||||
this.browser = new ChromiumWebBrowser(BlankURL) {
|
||||
MenuHandler = new ContextMenuNotification(this, enableContextMenu),
|
||||
JsDialogHandler = new JavaScriptDialogHandler(),
|
||||
LifeSpanHandler = new CustomLifeSpanHandler(),
|
||||
RequestHandler = new RequestHandlerBase(false),
|
||||
ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
|
||||
this.browser = new ChromiumWebBrowser(NotificationBrowser.BlankURL) {
|
||||
RequestHandler = new RequestHandlerBase(false)
|
||||
};
|
||||
|
||||
this.browserComponent = new ComponentImpl(browser, this);
|
||||
this.browserImpl = createBrowserImpl(this, browserComponent);
|
||||
|
||||
this.browser.Dock = DockStyle.None;
|
||||
this.browser.ClientSize = ClientSize;
|
||||
this.browser.SetupZoomEvents();
|
||||
|
||||
Controls.Add(browser);
|
||||
|
||||
Disposed += (sender, args) => {
|
||||
this.owner.FormClosed -= owner_FormClosed;
|
||||
this.browserImpl.Dispose();
|
||||
this.browser.Dispose();
|
||||
};
|
||||
|
||||
@ -158,6 +136,25 @@ protected FormNotificationBase(FormBrowser owner, bool enableContextMenu) {
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
protected sealed class ComponentImpl : CefBrowserComponent {
|
||||
private readonly FormNotificationBase owner;
|
||||
|
||||
public ComponentImpl(ChromiumWebBrowser browser, FormNotificationBase owner) : base(browser) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) {
|
||||
return new ContextMenuNotification(owner, handler);
|
||||
}
|
||||
|
||||
protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) {
|
||||
var registry = new CefResourceHandlerRegistry();
|
||||
registry.RegisterStatic(NotificationBrowser.BlankURL, string.Empty);
|
||||
registry.RegisterDynamic(TwitterUrls.TweetDeck, owner.resourceHandler);
|
||||
return new CefResourceHandlerFactory(handler, registry);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
if (disposing) {
|
||||
components?.Dispose();
|
||||
@ -184,7 +181,7 @@ private void owner_FormClosed(object sender, FormClosedEventArgs e) {
|
||||
// notification methods
|
||||
|
||||
public virtual void HideNotification() {
|
||||
browser.Load(BlankURL);
|
||||
browser.Load(NotificationBrowser.BlankURL);
|
||||
DisplayTooltip(null);
|
||||
|
||||
Location = ControlExtensions.InvisibleLocation;
|
||||
@ -205,11 +202,9 @@ public virtual void ResumeNotification() {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract string GetTweetHTML(DesktopNotification tweet);
|
||||
|
||||
protected virtual void LoadTweet(DesktopNotification tweet) {
|
||||
currentNotification = tweet;
|
||||
resourceHandler.SetHTML(GetTweetHTML(tweet));
|
||||
resourceHandler.SetHTML(browserImpl.GetTweetHTML(tweet));
|
||||
|
||||
browser.Load(TwitterUrls.TweetDeck);
|
||||
DisplayTooltip(null);
|
||||
@ -225,8 +220,8 @@ protected virtual void UpdateTitle() {
|
||||
}
|
||||
|
||||
public void ShowTweetDetail() {
|
||||
if (currentNotification != null) {
|
||||
owner.ShowTweetDetail(currentNotification.ColumnId, currentNotification.ChirpId, currentNotification.TweetUrl);
|
||||
if (currentNotification != null && owner.ShowTweetDetail(currentNotification.ColumnId, currentNotification.ChirpId, currentNotification.TweetUrl)) {
|
||||
FinishCurrentNotification();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,18 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Resources;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
using TweetLib.Core.Resources;
|
||||
|
||||
namespace TweetDuck.Browser.Notification.Example {
|
||||
namespace TweetDuck.Browser.Notification {
|
||||
sealed class FormNotificationExample : FormNotificationMain {
|
||||
private static NotificationBrowser CreateBrowserImpl(IBrowserComponent browserComponent, INotificationInterface notificationInterface, ITweetDeckInterface tweetDeckInterface, PluginManager pluginManager) {
|
||||
return new NotificationBrowser.Example(browserComponent, notificationInterface, tweetDeckInterface, pluginManager);
|
||||
}
|
||||
|
||||
public override bool RequiresResize => true;
|
||||
protected override bool CanDragWindow => Config.NotificationPosition == DesktopNotification.Position.Custom;
|
||||
|
||||
@ -24,16 +29,16 @@ protected override FormBorderStyle 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;
|
||||
public FormNotificationExample(FormBrowser owner, ITweetDeckInterface tweetDeckInterface, PluginManager pluginManager) : base(owner, (form, browserComponent) => CreateBrowserImpl(browserComponent, new NotificationInterfaceImpl(form), tweetDeckInterface, pluginManager)) {
|
||||
browserComponent.BrowserLoaded += (sender, args) => {
|
||||
Ready?.Invoke(this, EventArgs.Empty);
|
||||
};
|
||||
|
||||
string exampleTweetHTML = ResourceUtils.ReadFileOrNull("notification/example/example.html")?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
|
||||
string exampleTweetHTML = ResourceUtils.ReadFileOrNull("notification/example/example.html") ?? string.Empty;
|
||||
|
||||
#if DEBUG
|
||||
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
|
||||
@ -42,13 +47,6 @@ public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) :
|
||||
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;
|
||||
}
|
@ -2,20 +2,50 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Browser.Bridge;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Plugins;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
|
||||
namespace TweetDuck.Browser.Notification {
|
||||
abstract partial class FormNotificationMain : FormNotificationBase {
|
||||
private readonly PluginManager plugins;
|
||||
abstract partial class FormNotificationMain : FormNotificationBase, CustomKeyboardHandler.IBrowserKeyHandler {
|
||||
protected sealed class NotificationInterfaceImpl : INotificationInterface {
|
||||
public bool FreezeTimer {
|
||||
get => notification.FreezeTimer;
|
||||
set => notification.FreezeTimer = value;
|
||||
}
|
||||
|
||||
public bool IsHovered => notification.IsCursorOverBrowser;
|
||||
|
||||
private readonly FormNotificationBase notification;
|
||||
|
||||
public NotificationInterfaceImpl(FormNotificationBase notification) {
|
||||
this.notification = notification;
|
||||
}
|
||||
|
||||
public void DisplayTooltip(string text) {
|
||||
notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
|
||||
}
|
||||
|
||||
public void FinishCurrentNotification() {
|
||||
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
|
||||
}
|
||||
|
||||
public void ShowTweetDetail() {
|
||||
notification.InvokeAsyncSafe(notification.ShowTweetDetail);
|
||||
}
|
||||
}
|
||||
|
||||
private static int FontSizeLevel {
|
||||
get => NotificationBrowser.FontSize switch {
|
||||
"largest" => 4,
|
||||
"large" => 3,
|
||||
"small" => 1,
|
||||
"smallest" => 0,
|
||||
_ => 2
|
||||
};
|
||||
}
|
||||
|
||||
private readonly int timerBarHeight;
|
||||
|
||||
protected int timeLeft, totalTime;
|
||||
@ -59,29 +89,21 @@ private int BaseClientHeight {
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual string BodyClasses => IsCursorOverBrowser ? "td-notification td-hover" : "td-notification";
|
||||
|
||||
public Size BrowserSize => Config.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height - timerBarHeight) : ClientSize;
|
||||
|
||||
protected FormNotificationMain(FormBrowser owner, PluginManager pluginManager, bool enableContextMenu) : base(owner, enableContextMenu) {
|
||||
protected FormNotificationMain(FormBrowser owner, CreateBrowserImplFunc createBrowserImpl) : base(owner, createBrowserImpl) {
|
||||
InitializeComponent();
|
||||
|
||||
this.plugins = pluginManager;
|
||||
this.timerBarHeight = BrowserUtils.Scale(4, DpiScale);
|
||||
|
||||
browser.KeyboardHandler = new KeyboardHandlerNotification(this);
|
||||
browser.RegisterJsBridge("$TD", new TweetDeckBridge.Notification(owner, this));
|
||||
browser.KeyboardHandler = new CustomKeyboardHandler(this);
|
||||
|
||||
browser.LoadingStateChanged += Browser_LoadingStateChanged;
|
||||
|
||||
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser, url => TwitterUrls.IsTweetDeck(url) && url != BlankURL));
|
||||
|
||||
mouseHookDelegate = MouseHookProc;
|
||||
Disposed += (sender, args) => StopMouseHook(true);
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private void SetOpacity(int opacity) {
|
||||
if (currentOpacity != opacity) {
|
||||
currentOpacity = opacity;
|
||||
@ -113,7 +135,7 @@ private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam) {
|
||||
int delta = BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed * 0.01);
|
||||
|
||||
if (Config.EnableSmoothScrolling) {
|
||||
browser.ExecuteJsAsync("window.TDGF_scrollSmoothly", (int) Math.Round(-delta / 0.6));
|
||||
browser.BrowserCore.ExecuteScriptAsync("window.TDGF_scrollSmoothly", (int) Math.Round(-delta / 0.6));
|
||||
}
|
||||
else {
|
||||
browser.SendMouseWheelEvent(0, 0, 0, delta, CefEventFlags.None);
|
||||
@ -158,7 +180,7 @@ private void FormNotification_FormClosing(object sender, FormClosingEventArgs e)
|
||||
}
|
||||
|
||||
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
|
||||
if (!e.IsLoading && browser.Address != BlankURL) {
|
||||
if (!e.IsLoading && browser.Address != NotificationBrowser.BlankURL) {
|
||||
this.InvokeSafe(() => {
|
||||
Visible = true; // ensures repaint before moving the window to a visible location
|
||||
timerDisplayDelay.Start();
|
||||
@ -236,13 +258,6 @@ public override void ResumeNotification() {
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetTweetHTML(DesktopNotification tweet) {
|
||||
return tweet.GenerateHtml(BodyClasses, HeadLayout, Config.CustomNotificationCSS, plugins.NotificationInjections, new string[] {
|
||||
PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification),
|
||||
CefScriptExecutor.GetBootstrapScript("notification", includeStylesheets: false)
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadTweet(DesktopNotification tweet) {
|
||||
timerProgress.Stop();
|
||||
totalTime = timeLeft = tweet.GetDisplayDuration(Config.NotificationDurationValue);
|
||||
@ -278,5 +293,24 @@ protected virtual void OnNotificationReady() {
|
||||
PrepareAndDisplayWindow();
|
||||
timerProgress.Start();
|
||||
}
|
||||
|
||||
bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) {
|
||||
switch (key) {
|
||||
case Keys.Enter:
|
||||
this.InvokeAsyncSafe(FinishCurrentNotification);
|
||||
return true;
|
||||
|
||||
case Keys.Escape:
|
||||
this.InvokeAsyncSafe(HideNotification);
|
||||
return true;
|
||||
|
||||
case Keys.Space:
|
||||
this.InvokeAsyncSafe(() => FreezeTimer = !FreezeTimer);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,17 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
|
||||
namespace TweetDuck.Browser.Notification {
|
||||
sealed partial class FormNotificationTweet : FormNotificationMain {
|
||||
private static NotificationBrowser CreateBrowserImpl(IBrowserComponent browserComponent, INotificationInterface notificationInterface, ITweetDeckInterface tweetDeckInterface, PluginManager pluginManager) {
|
||||
return new NotificationBrowser.Tweet(browserComponent, notificationInterface, tweetDeckInterface, pluginManager);
|
||||
}
|
||||
|
||||
private const int NonIntrusiveIdleLimit = 30;
|
||||
private const int TrimMinimum = 32;
|
||||
|
||||
@ -30,7 +36,7 @@ protected override bool CanDragWindow {
|
||||
private bool needsTrim;
|
||||
private bool hasTemporarilyMoved;
|
||||
|
||||
public FormNotificationTweet(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, true) {
|
||||
public FormNotificationTweet(FormBrowser owner, ITweetDeckInterface tweetDeckInterface, PluginManager pluginManager) : base(owner, (form, browserComponent) => CreateBrowserImpl(browserComponent, new NotificationInterfaceImpl(form), tweetDeckInterface, pluginManager)) {
|
||||
InitializeComponent();
|
||||
|
||||
Config.MuteToggled += Config_MuteToggled;
|
||||
|
@ -4,33 +4,30 @@
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
using CefSharp.DevTools.Page;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Resources;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Resources;
|
||||
|
||||
namespace TweetDuck.Browser.Notification.Screenshot {
|
||||
sealed class FormNotificationScreenshotable : FormNotificationBase {
|
||||
private static NotificationBrowser CreateBrowserImpl( IBrowserComponent browserComponent, PluginManager pluginManager) {
|
||||
return new NotificationBrowser.Screenshot(browserComponent, pluginManager.NotificationInjections);
|
||||
}
|
||||
|
||||
protected override bool CanDragWindow => false;
|
||||
|
||||
private readonly PluginManager plugins;
|
||||
private int height;
|
||||
|
||||
public FormNotificationScreenshotable(Action callback, FormBrowser owner, PluginManager pluginManager, string html, int width) : base(owner, false) {
|
||||
this.plugins = pluginManager;
|
||||
|
||||
public FormNotificationScreenshotable(Action callback, FormBrowser owner, PluginManager pluginManager, string html, int width) : base(owner, (_, browserComponent) => CreateBrowserImpl(browserComponent, pluginManager)) {
|
||||
int realWidth = BrowserUtils.Scale(width, DpiScale);
|
||||
|
||||
browser.RegisterJsBridge("$TD_NotificationScreenshot", new ScreenshotBridge(this, SetScreenshotHeight, callback));
|
||||
|
||||
browser.LoadingStateChanged += (sender, args) => {
|
||||
if (args.IsLoading) {
|
||||
return;
|
||||
}
|
||||
browserComponent.AttachBridgeObject("$TD_NotificationScreenshot", new ScreenshotBridge(this, SetScreenshotHeight, callback));
|
||||
|
||||
browserComponent.BrowserLoaded += (sender, args) => {
|
||||
string script = ResourceUtils.ReadFileOrNull("notification/screenshot/screenshot.js");
|
||||
|
||||
if (script == null) {
|
||||
@ -38,18 +35,14 @@ public FormNotificationScreenshotable(Action callback, FormBrowser owner, Plugin
|
||||
return;
|
||||
}
|
||||
|
||||
using IFrame frame = args.Browser.MainFrame;
|
||||
CefScriptExecutor.RunScript(frame, script.Replace("{width}", realWidth.ToString()).Replace("1/*FRAMES*/", TweetScreenshotManager.WaitFrames.ToString()), "gen:screenshot");
|
||||
string substituted = script.Replace("{width}", realWidth.ToString()).Replace("1/*FRAMES*/", TweetScreenshotManager.WaitFrames.ToString());
|
||||
browserComponent.RunScript("gen:screenshot", substituted);
|
||||
};
|
||||
|
||||
SetNotificationSize(realWidth, 1024);
|
||||
LoadTweet(new DesktopNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty));
|
||||
}
|
||||
|
||||
protected override string GetTweetHTML(DesktopNotification tweet) {
|
||||
return tweet.GenerateHtml("td-screenshot", HeadLayout, Config.CustomNotificationCSS, plugins.NotificationInjections, Array.Empty<string>());
|
||||
}
|
||||
|
||||
private void SetScreenshotHeight(int browserHeight) {
|
||||
this.height = BrowserUtils.Scale(browserHeight, SizeScale);
|
||||
}
|
||||
|
@ -1,19 +1,36 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Dialogs.Settings;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
|
||||
namespace TweetDuck.Browser.Notification {
|
||||
static class SoundNotification {
|
||||
sealed class SoundNotification : ISoundNotificationHandler {
|
||||
public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
|
||||
|
||||
public static Func<IResourceHandler> CreateFileHandler(string path) {
|
||||
private readonly CefResourceHandlerRegistry registry;
|
||||
|
||||
public SoundNotification(CefResourceHandlerRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
public void Unregister(string url) {
|
||||
registry.Unregister(url);
|
||||
}
|
||||
|
||||
public void Register(string url, string path) {
|
||||
var fileHandler = CreateFileHandler(path);
|
||||
if (fileHandler.HasValue) {
|
||||
var (data, mimeType) = fileHandler.Value;
|
||||
registry.RegisterStatic(url, data, mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
private static (byte[] data, string mimeType)? CreateFileHandler(string path) {
|
||||
string mimeType = Path.GetExtension(path) switch {
|
||||
".weba" => "audio/webm",
|
||||
".webm" => "audio/webm",
|
||||
@ -26,7 +43,7 @@ public static Func<IResourceHandler> CreateFileHandler(string path) {
|
||||
};
|
||||
|
||||
try {
|
||||
return ResourceHandlers.ForBytes(File.ReadAllBytes(path), mimeType);
|
||||
return (File.ReadAllBytes(path), mimeType);
|
||||
} catch {
|
||||
FormBrowser browser = FormManager.TryFind<FormBrowser>();
|
||||
|
||||
|
@ -1,31 +1,25 @@
|
||||
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.TweetDeck;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using TweetLib.Utils.Static;
|
||||
using TweetLib.Core.Systems.Updates;
|
||||
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
||||
using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler;
|
||||
using TweetDeckBrowserImpl = TweetLib.Core.Features.TweetDeck.TweetDeckBrowser;
|
||||
|
||||
namespace TweetDuck.Browser {
|
||||
sealed class TweetDeckBrowser : IDisposable {
|
||||
private static UserConfig Config => Program.Config.User;
|
||||
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153);
|
||||
|
||||
private const string NamespaceTweetDeck = "tweetdeck";
|
||||
|
||||
public bool Ready { get; private set; }
|
||||
public bool Ready => browserComponent.Ready;
|
||||
|
||||
public bool Enabled {
|
||||
get => browser.Enabled;
|
||||
@ -43,53 +37,62 @@ public bool IsTweetDeckWebsite {
|
||||
}
|
||||
}
|
||||
|
||||
public TweetDeckFunctions Functions => browserImpl.Functions;
|
||||
|
||||
private readonly CefBrowserComponent browserComponent;
|
||||
private readonly TweetDeckBrowserImpl browserImpl;
|
||||
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);
|
||||
|
||||
public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDeckInterface tweetDeckInterface, UpdateChecker updateChecker) {
|
||||
RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
|
||||
|
||||
this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck) {
|
||||
DialogHandler = new FileDialogHandler(),
|
||||
DragHandler = new DragHandlerBrowser(requestHandler),
|
||||
MenuHandler = new ContextMenuBrowser(owner),
|
||||
JsDialogHandler = new JavaScriptDialogHandler(),
|
||||
KeyboardHandler = new KeyboardHandlerBrowser(owner),
|
||||
LifeSpanHandler = new CustomLifeSpanHandler(),
|
||||
RequestHandler = requestHandler,
|
||||
ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
|
||||
KeyboardHandler = new CustomKeyboardHandler(owner),
|
||||
RequestHandler = requestHandler
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable
|
||||
this.browser.BrowserSettings.BackgroundColor = (uint) TwitterUtils.BackgroundColor.ToArgb();
|
||||
this.browser.Dock = DockStyle.None;
|
||||
this.browser.Location = ControlExtensions.InvisibleLocation;
|
||||
this.browser.SetupZoomEvents();
|
||||
this.browser.BrowserSettings.BackgroundColor = (uint) BackgroundColor.ToArgb();
|
||||
|
||||
var extraContext = new TweetDeckExtraContext();
|
||||
var resourceHandlerRegistry = new CefResourceHandlerRegistry();
|
||||
var soundNotificationHandler = new SoundNotification(resourceHandlerRegistry);
|
||||
|
||||
this.browserComponent = new ComponentImpl(browser, owner, extraContext, resourceHandlerRegistry);
|
||||
this.browserImpl = new TweetDeckBrowserImpl(browserComponent, tweetDeckInterface, extraContext, soundNotificationHandler, pluginManager, updateChecker);
|
||||
|
||||
if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) {
|
||||
browserComponent.PageLoadEnd += (sender, args) => {
|
||||
if (TwitterUrls.IsTweetDeck(args.Url)) {
|
||||
browserImpl.ScriptExecutor.RunScript("gen:gdpr", "TD.storage.Account.prototype.requiresConsent = function() { return false; }");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
owner.Controls.Add(browser);
|
||||
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser, TwitterUrls.IsTweetDeck));
|
||||
|
||||
Config.MuteToggled += Config_MuteToggled;
|
||||
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
|
||||
}
|
||||
|
||||
// setup and management
|
||||
private sealed class ComponentImpl : CefBrowserComponent {
|
||||
private readonly FormBrowser owner;
|
||||
private readonly TweetDeckExtraContext extraContext;
|
||||
private readonly CefResourceHandlerRegistry registry;
|
||||
|
||||
public ComponentImpl(ChromiumWebBrowser browser, FormBrowser owner, TweetDeckExtraContext extraContext, CefResourceHandlerRegistry registry) : base(browser) {
|
||||
this.owner = owner;
|
||||
this.extraContext = extraContext;
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) {
|
||||
return new ContextMenuBrowser(owner, handler, extraContext);
|
||||
}
|
||||
|
||||
protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) {
|
||||
return new CefResourceHandlerFactory(handler, registry);
|
||||
}
|
||||
}
|
||||
|
||||
public void PrepareSize(Size size) {
|
||||
if (!Ready) {
|
||||
@ -97,178 +100,33 @@ public void PrepareSize(Size size) {
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowserReady() {
|
||||
if (!Ready) {
|
||||
browser.Location = Point.Empty;
|
||||
browser.Dock = DockStyle.Fill;
|
||||
Ready = true;
|
||||
}
|
||||
public void Dispose() {
|
||||
browserImpl.Dispose();
|
||||
browser.Dispose();
|
||||
}
|
||||
|
||||
public void Focus() {
|
||||
browser.Focus();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Config.MuteToggled -= Config_MuteToggled;
|
||||
Config.SoundNotificationChanged -= Config_SoundNotificationInfoChanged;
|
||||
|
||||
browser.Dispose();
|
||||
public void OpenDevTools() {
|
||||
browser.OpenDevToolsCustom();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
public void ReloadToTweetDeck() {
|
||||
browserImpl.ReloadToTweetDeck();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
public void SaveVideo(string url, string username) {
|
||||
browserImpl.FileDownloadManager.SaveVideo(url, username);
|
||||
}
|
||||
|
||||
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://resources/error/", 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();
|
||||
browserImpl.ScriptExecutor.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
Browser/TweetDeckInterfaceImpl.cs
Normal file
81
Browser/TweetDeckInterfaceImpl.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
|
||||
namespace TweetDuck.Browser {
|
||||
sealed class TweetDeckInterfaceImpl : ITweetDeckInterface {
|
||||
private readonly FormBrowser form;
|
||||
|
||||
public TweetDeckInterfaceImpl(FormBrowser form) {
|
||||
this.form = form;
|
||||
}
|
||||
|
||||
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 DisplayTooltip(string text) {
|
||||
form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
|
||||
}
|
||||
|
||||
public void FixClipboard() {
|
||||
form.InvokeAsyncSafe(ClipboardManager.StripHtmlStyles);
|
||||
}
|
||||
|
||||
public int GetIdleSeconds() {
|
||||
return NativeMethods.GetIdleSeconds();
|
||||
}
|
||||
|
||||
public void OnIntroductionClosed(bool showGuide) {
|
||||
form.InvokeAsyncSafe(() => form.OnIntroductionClosed(showGuide));
|
||||
}
|
||||
|
||||
public void OnSoundNotification() {
|
||||
form.InvokeAsyncSafe(form.OnTweetNotification);
|
||||
}
|
||||
|
||||
public void OpenContextMenu() {
|
||||
form.InvokeAsyncSafe(form.OpenContextMenu);
|
||||
}
|
||||
|
||||
public void OpenProfileImport() {
|
||||
form.InvokeAsyncSafe(form.OpenProfileImport);
|
||||
}
|
||||
|
||||
public void PlayVideo(string videoUrl, string tweetUrl, string username, object callShowOverlay) {
|
||||
form.InvokeAsyncSafe(() => form.PlayVideo(videoUrl, tweetUrl, username, (IJavascriptCallback) callShowOverlay));
|
||||
}
|
||||
|
||||
public void ScreenshotTweet(string html, int width) {
|
||||
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width));
|
||||
}
|
||||
|
||||
public void ShowDesktopNotification(DesktopNotification notification) {
|
||||
form.InvokeAsyncSafe(() => {
|
||||
form.OnTweetNotification();
|
||||
form.ShowDesktopNotification(notification);
|
||||
});
|
||||
}
|
||||
|
||||
public void StopVideo() {
|
||||
form.InvokeAsyncSafe(form.StopVideo);
|
||||
}
|
||||
|
||||
public Task ExecuteCallback(object callback, params object[] parameters) {
|
||||
return ((IJavascriptCallback) callback).ExecuteAsync(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
using TweetLib.Utils.Serialization.Converters;
|
||||
@ -8,6 +8,14 @@
|
||||
|
||||
namespace TweetDuck.Configuration {
|
||||
sealed class ConfigManager : IConfigManager {
|
||||
internal sealed class Paths {
|
||||
public string UserConfig { get; set; }
|
||||
public string SystemConfig { get; set; }
|
||||
public string PluginConfig { get; set; }
|
||||
}
|
||||
|
||||
public Paths FilePaths { get; }
|
||||
|
||||
public UserConfig User { get; }
|
||||
public SystemConfig System { get; }
|
||||
public PluginConfig Plugins { get; }
|
||||
@ -20,15 +28,17 @@ sealed class ConfigManager : IConfigManager {
|
||||
|
||||
private readonly IConfigInstance<BaseConfig>[] infoList;
|
||||
|
||||
public ConfigManager() {
|
||||
User = new UserConfig(this);
|
||||
System = new SystemConfig(this);
|
||||
Plugins = new PluginConfig(this);
|
||||
public ConfigManager(UserConfig userConfig, Paths paths) {
|
||||
FilePaths = paths;
|
||||
|
||||
User = userConfig;
|
||||
System = new SystemConfig();
|
||||
Plugins = new PluginConfig();
|
||||
|
||||
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)
|
||||
infoUser = new FileConfigInstance<UserConfig>(paths.UserConfig, User, "program options"),
|
||||
infoSystem = new FileConfigInstance<SystemConfig>(paths.SystemConfig, System, "system options"),
|
||||
infoPlugins = new PluginConfigInstance<PluginConfig>(paths.PluginConfig, Plugins)
|
||||
};
|
||||
|
||||
// TODO refactor further
|
||||
@ -70,7 +80,15 @@ public void ReloadAll() {
|
||||
infoPlugins.Reload();
|
||||
}
|
||||
|
||||
void IConfigManager.TriggerProgramRestartRequested() {
|
||||
public void Save(BaseConfig instance) {
|
||||
((IConfigManager) this).GetInstanceInfo(instance).Save();
|
||||
}
|
||||
|
||||
public void Reset(BaseConfig instance) {
|
||||
((IConfigManager) this).GetInstanceInfo(instance).Reset();
|
||||
}
|
||||
|
||||
public void TriggerProgramRestartRequested() {
|
||||
ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@ -79,4 +97,14 @@ IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance)
|
||||
return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null
|
||||
}
|
||||
}
|
||||
|
||||
static class ConfigManagerExtensions {
|
||||
public static void Save(this BaseConfig instance) {
|
||||
Program.Config.Save(instance);
|
||||
}
|
||||
|
||||
public static void Reset(this BaseConfig instance) {
|
||||
Program.Config.Reset(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,8 @@ sealed class PluginConfig : BaseConfig, IPluginConfig {
|
||||
|
||||
// END OF CONFIG
|
||||
|
||||
public PluginConfig(IConfigManager configManager) : base(configManager) {}
|
||||
|
||||
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
|
||||
return new PluginConfig(configManager);
|
||||
protected override BaseConfig ConstructWithDefaults() {
|
||||
return new PluginConfig();
|
||||
}
|
||||
|
||||
// INTERFACE IMPLEMENTATION
|
||||
@ -40,7 +38,7 @@ void IPluginConfig.Reset(IEnumerable<string> 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();
|
||||
this.Save();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace TweetDuck.Configuration {
|
||||
sealed class SystemConfig : BaseConfig {
|
||||
// CONFIGURATION DATA
|
||||
|
||||
private bool _hardwareAcceleration = true;
|
||||
|
||||
public bool ClearCacheAutomatically { get; set; } = true;
|
||||
@ -13,15 +11,13 @@ sealed class SystemConfig : BaseConfig {
|
||||
|
||||
public bool HardwareAcceleration {
|
||||
get => _hardwareAcceleration;
|
||||
set => UpdatePropertyWithRestartRequest(ref _hardwareAcceleration, value);
|
||||
set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
// END OF CONFIG
|
||||
|
||||
public SystemConfig(IConfigManager configManager) : base(configManager) {}
|
||||
|
||||
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
|
||||
return new SystemConfig(configManager);
|
||||
protected override BaseConfig ConstructWithDefaults() {
|
||||
return new SystemConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetLib.Core.Application;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
using TweetLib.Core.Features.Twitter;
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
|
||||
namespace TweetDuck.Configuration {
|
||||
sealed class UserConfig : BaseConfig {
|
||||
// CONFIGURATION DATA
|
||||
|
||||
public bool FirstRun { get; set; } = true;
|
||||
sealed class UserConfig : BaseConfig, IAppUserConfiguration {
|
||||
public bool FirstRun { get; set; } = true;
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
public bool AllowDataCollection { get; set; } = false;
|
||||
@ -122,32 +121,32 @@ public TrayIcon.Behavior TrayBehavior {
|
||||
|
||||
public bool EnableSmoothScrolling {
|
||||
get => _enableSmoothScrolling;
|
||||
set => UpdatePropertyWithRestartRequest(ref _enableSmoothScrolling, value);
|
||||
set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
public bool EnableTouchAdjustment {
|
||||
get => _enableTouchAdjustment;
|
||||
set => UpdatePropertyWithRestartRequest(ref _enableTouchAdjustment, value);
|
||||
set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
public bool EnableColorProfileDetection {
|
||||
get => _enableColorProfileDetection;
|
||||
set => UpdatePropertyWithRestartRequest(ref _enableColorProfileDetection, value);
|
||||
set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
public bool UseSystemProxyForAllConnections {
|
||||
get => _useSystemProxyForAllConnections;
|
||||
set => UpdatePropertyWithRestartRequest(ref _useSystemProxyForAllConnections, value);
|
||||
set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
public string CustomCefArgs {
|
||||
get => _customCefArgs;
|
||||
set => UpdatePropertyWithRestartRequest(ref _customCefArgs, value);
|
||||
set => UpdatePropertyWithCallback(ref _customCefArgs, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
public string SpellCheckLanguage {
|
||||
get => _spellCheckLanguage;
|
||||
set => UpdatePropertyWithRestartRequest(ref _spellCheckLanguage, value);
|
||||
set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, Program.Config.TriggerProgramRestartRequested);
|
||||
}
|
||||
|
||||
// EVENTS
|
||||
@ -156,13 +155,16 @@ public string SpellCheckLanguage {
|
||||
public event EventHandler ZoomLevelChanged;
|
||||
public event EventHandler TrayBehaviorChanged;
|
||||
public event EventHandler SoundNotificationChanged;
|
||||
public event EventHandler OptionsDialogClosed;
|
||||
|
||||
public void TriggerOptionsDialogClosed() {
|
||||
OptionsDialogClosed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// END OF CONFIG
|
||||
|
||||
public UserConfig(IConfigManager configManager) : base(configManager) {}
|
||||
|
||||
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
|
||||
return new UserConfig(configManager);
|
||||
protected override BaseConfig ConstructWithDefaults() {
|
||||
return new UserConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,12 @@ public static void InvokeSafe(this Control control, Action func) {
|
||||
}
|
||||
|
||||
public static void InvokeAsyncSafe(this Control control, Action func) {
|
||||
control.BeginInvoke(func);
|
||||
if (control.InvokeRequired) {
|
||||
control.BeginInvoke(func);
|
||||
}
|
||||
else {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
public static float GetDPIScale(this Control control) {
|
||||
|
@ -1,9 +1,10 @@
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
|
||||
namespace TweetDuck.Dialogs {
|
||||
sealed partial class FormAbout : Form, FormManager.IAppDialog {
|
||||
@ -21,13 +22,15 @@ public FormAbout() {
|
||||
labelTips.Links.Add(new LinkLabel.Link(0, labelTips.Text.Length, TipsLink));
|
||||
labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, IssuesLink));
|
||||
|
||||
MemoryStream logoStream = new MemoryStream(Properties.Resources.avatar);
|
||||
pictureLogo.Image = Image.FromStream(logoStream);
|
||||
Disposed += (sender, args) => logoStream.Dispose();
|
||||
try {
|
||||
pictureLogo.Image = Image.FromFile(Path.Combine(App.ResourcesPath, "images/logo.png"));
|
||||
} catch (Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
|
||||
BrowserUtils.OpenExternalBrowser(e.Link.LinkData as string);
|
||||
App.SystemHandler.OpenBrowser(e.Link.LinkData as string);
|
||||
}
|
||||
|
||||
private void FormAbout_HelpRequested(object sender, HelpEventArgs hlpevent) {
|
||||
|
@ -1,20 +1,18 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Browser.Data;
|
||||
using TweetDuck.Browser.Adapters;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Core.Features;
|
||||
|
||||
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));
|
||||
private const string GuideUrl = "td://guide/index.html";
|
||||
|
||||
public static void Show(string hash = null) {
|
||||
string url = GuideUrl + (string.IsNullOrEmpty(hash) ? string.Empty : "#" + hash);
|
||||
@ -37,36 +35,43 @@ public static void Show(string hash = null) {
|
||||
private readonly ChromiumWebBrowser browser;
|
||||
#pragma warning restore IDE0069 // Disposable fields should be disposed
|
||||
|
||||
private string nextUrl;
|
||||
|
||||
private FormGuide(string url, FormBrowser owner) {
|
||||
private FormGuide(string url, Form 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 = new ChromiumWebBrowser(url) {
|
||||
KeyboardHandler = new CustomKeyboardHandler(null),
|
||||
RequestHandler = new RequestHandlerBase(true)
|
||||
};
|
||||
|
||||
browser.LoadingStateChanged += browser_LoadingStateChanged;
|
||||
|
||||
browser.BrowserSettings.BackgroundColor = (uint) BackColor.ToArgb();
|
||||
browser.Dock = DockStyle.None;
|
||||
browser.Location = ControlExtensions.InvisibleLocation;
|
||||
browser.SetupZoomEvents();
|
||||
|
||||
var browserComponent = new ComponentImpl(browser);
|
||||
var browserImpl = new BaseBrowser(browserComponent);
|
||||
|
||||
BrowserUtils.SetupDockOnLoad(browserComponent, browser);
|
||||
|
||||
Controls.Add(browser);
|
||||
Disposed += (sender, args) => browser.Dispose();
|
||||
|
||||
Disposed += (sender, args) => {
|
||||
browserImpl.Dispose();
|
||||
browser.Dispose();
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class ComponentImpl : CefBrowserComponent {
|
||||
public ComponentImpl(ChromiumWebBrowser browser) : base(browser) {}
|
||||
|
||||
protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) {
|
||||
return new ContextMenuGuide(handler);
|
||||
}
|
||||
|
||||
protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) {
|
||||
return new CefResourceHandlerFactory(handler, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
@ -78,27 +83,7 @@ protected override void Dispose(bool 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;
|
||||
}
|
||||
}
|
||||
browser.Load(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ private void flowLayoutPlugins_Resize(object sender, EventArgs e) {
|
||||
}
|
||||
|
||||
private void btnOpenFolder_Click(object sender, EventArgs e) {
|
||||
App.SystemHandler.OpenFileExplorer(pluginManager.PathCustomPlugins);
|
||||
App.SystemHandler.OpenFileExplorer(pluginManager.CustomPluginFolder);
|
||||
}
|
||||
|
||||
private void btnReload_Click(object sender, EventArgs e) {
|
||||
|
@ -3,14 +3,14 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Browser.Notification.Example;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs.Settings;
|
||||
using TweetDuck.Management;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
using TweetLib.Core.Systems.Updates;
|
||||
|
||||
namespace TweetDuck.Dialogs {
|
||||
@ -25,7 +25,7 @@ sealed partial class FormSettings : Form, FormManager.IAppDialog {
|
||||
private readonly Dictionary<Type, SettingsTab> tabs = new Dictionary<Type, SettingsTab>(8);
|
||||
private SettingsTab currentTab;
|
||||
|
||||
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler updates, Type startTab) {
|
||||
public FormSettings(FormBrowser browser, PluginManager plugins, UpdateChecker updates, TweetDeckFunctions tweetDeckFunctions, Type startTab) {
|
||||
InitializeComponent();
|
||||
|
||||
Text = Program.BrandName + " Options";
|
||||
@ -39,12 +39,12 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler up
|
||||
|
||||
PrepareLoad();
|
||||
|
||||
AddButton("General", () => new TabSettingsGeneral(this.browser.ReloadColumns, updates));
|
||||
AddButton("Notifications", () => new TabSettingsNotifications(new FormNotificationExample(this.browser, this.plugins)));
|
||||
AddButton("Sounds", () => new TabSettingsSounds(this.browser.PlaySoundNotification));
|
||||
AddButton("General", () => new TabSettingsGeneral(tweetDeckFunctions.ReloadColumns, updates));
|
||||
AddButton("Notifications", () => new TabSettingsNotifications(this.browser.CreateExampleNotification()));
|
||||
AddButton("Sounds", () => new TabSettingsSounds(tweetDeckFunctions.PlaySoundNotification));
|
||||
AddButton("Tray", () => new TabSettingsTray());
|
||||
AddButton("Feedback", () => new TabSettingsFeedback());
|
||||
AddButton("Advanced", () => new TabSettingsAdvanced(this.browser.ReinjectCustomCSS, this.browser.OpenDevTools));
|
||||
AddButton("Advanced", () => new TabSettingsAdvanced(tweetDeckFunctions.ReinjectCustomCSS, this.browser.OpenDevTools));
|
||||
|
||||
SelectTab(tabs[startTab ?? typeof(TabSettingsGeneral)]);
|
||||
}
|
||||
@ -181,7 +181,7 @@ private void SelectTab(SettingsTab tab) {
|
||||
}
|
||||
|
||||
private void control_MouseLeave(object sender, EventArgs e) {
|
||||
if (sender is ComboBox cb && cb.DroppedDown) {
|
||||
if (sender is ComboBox { DroppedDown: true } ) {
|
||||
return; // prevents comboboxes from closing when MouseLeave event triggers during opening animation
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Chromium;
|
||||
|
||||
namespace TweetDuck.Dialogs.Settings {
|
||||
@ -21,7 +21,7 @@ public DialogSettingsCefArgs(string args) {
|
||||
}
|
||||
|
||||
private void btnHelp_Click(object sender, EventArgs e) {
|
||||
BrowserUtils.OpenExternalBrowser("http://peter.sh/experiments/chromium-command-line-switches/");
|
||||
App.SystemHandler.OpenBrowser("http://peter.sh/experiments/chromium-command-line-switches/");
|
||||
}
|
||||
|
||||
private void btnApply_Click(object sender, EventArgs e) {
|
||||
|
@ -149,7 +149,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
|
||||
Program.Config.Plugins.Reset();
|
||||
|
||||
try {
|
||||
Directory.Delete(Program.PluginDataPath, true);
|
||||
Directory.Delete(plugins.PluginDataFolder, true);
|
||||
} catch (Exception ex) {
|
||||
App.ErrorHandler.HandleException("Profile Reset", "Could not delete plugin data.", true, ex);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Configuration;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Utils.Collections;
|
||||
|
||||
namespace TweetDuck.Dialogs.Settings {
|
||||
@ -13,7 +14,7 @@ public DialogSettingsRestart(CommandLineArgs currentArgs) {
|
||||
cbLogging.Checked = currentArgs.HasFlag(Arguments.ArgLogging);
|
||||
cbLogging.CheckedChanged += control_Change;
|
||||
|
||||
if (Program.IsPortable) {
|
||||
if (App.IsPortable) {
|
||||
tbDataFolder.Text = "Not available in portable version";
|
||||
tbDataFolder.Enabled = false;
|
||||
}
|
||||
|
@ -85,11 +85,11 @@ public override void OnClosing() {
|
||||
#region Application
|
||||
|
||||
private void btnOpenAppFolder_Click(object sender, EventArgs e) {
|
||||
App.SystemHandler.OpenFileExplorer(Program.ProgramPath);
|
||||
App.SystemHandler.OpenFileExplorer(App.ProgramPath);
|
||||
}
|
||||
|
||||
private void btnOpenDataFolder_Click(object sender, EventArgs e) {
|
||||
App.SystemHandler.OpenFileExplorer(Program.StoragePath);
|
||||
App.SystemHandler.OpenFileExplorer(App.StoragePath);
|
||||
}
|
||||
|
||||
private void btnRestart_Click(object sender, EventArgs e) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
|
||||
namespace TweetDuck.Dialogs.Settings {
|
||||
sealed partial class TabSettingsFeedback : FormSettings.BaseTab {
|
||||
@ -14,7 +14,7 @@ public override void OnReady() {
|
||||
#region Feedback
|
||||
|
||||
private void btnSendFeedback_Click(object sender, EventArgs e) {
|
||||
BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/issues/new");
|
||||
App.SystemHandler.OpenBrowser("https://github.com/chylex/TweetDuck/issues/new");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Browser.Handling.General;
|
||||
using TweetDuck.Browser.Handling;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
@ -15,7 +15,7 @@ namespace TweetDuck.Dialogs.Settings {
|
||||
sealed partial class TabSettingsGeneral : FormSettings.BaseTab {
|
||||
private readonly Action reloadColumns;
|
||||
|
||||
private readonly UpdateHandler updates;
|
||||
private readonly UpdateChecker updates;
|
||||
private int updateCheckEventId = -1;
|
||||
|
||||
private readonly int browserListIndexDefault;
|
||||
@ -27,7 +27,7 @@ sealed partial class TabSettingsGeneral : FormSettings.BaseTab {
|
||||
private readonly int searchEngineIndexDefault;
|
||||
private readonly int searchEngineIndexCustom;
|
||||
|
||||
public TabSettingsGeneral(Action reloadColumns, UpdateHandler updates) {
|
||||
public TabSettingsGeneral(Action reloadColumns, UpdateChecker updates) {
|
||||
InitializeComponent();
|
||||
|
||||
this.reloadColumns = reloadColumns;
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Browser.Notification.Example;
|
||||
using TweetDuck.Browser.Notification;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Core.Features.Notifications;
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
using TweetLib.Utils.Serialization.Converters;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetDuck.Browser.Data {
|
||||
namespace TweetDuck.Dialogs {
|
||||
sealed class WindowState {
|
||||
private Rectangle rect;
|
||||
private bool isMaximized;
|
@ -3,10 +3,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TweetLib.Core;
|
||||
|
||||
namespace TweetDuck.Management {
|
||||
static class BrowserCache {
|
||||
public static string CacheFolder => Path.Combine(Program.StoragePath, "Cache");
|
||||
public static string CacheFolder => Path.Combine(App.StoragePath, "Cache");
|
||||
|
||||
private static bool clearOnExit;
|
||||
private static Timer autoClearTimer;
|
||||
@ -22,7 +23,7 @@ private static long CalculateCacheSize() {
|
||||
}
|
||||
|
||||
public static void GetCacheSize(Action<Task<long>> callbackBytes) {
|
||||
Task<long> task = new Task<long>(CalculateCacheSize);
|
||||
var task = new Task<long>(CalculateCacheSize);
|
||||
task.ContinueWith(callbackBytes);
|
||||
task.Start();
|
||||
}
|
||||
|
@ -1,10 +1,21 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Browser;
|
||||
using TweetDuck.Controls;
|
||||
|
||||
namespace TweetDuck.Management {
|
||||
static class FormManager {
|
||||
private static FormCollection OpenForms => System.Windows.Forms.Application.OpenForms;
|
||||
|
||||
public static void RunOnUIThread(Action action) {
|
||||
TryFind<FormBrowser>()?.InvokeSafe(action);
|
||||
}
|
||||
|
||||
public static void RunOnUIThreadAsync(Action action) {
|
||||
TryFind<FormBrowser>()?.InvokeAsyncSafe(action);
|
||||
}
|
||||
|
||||
public static T TryFind<T>() where T : Form {
|
||||
return OpenForms.OfType<T>().FirstOrDefault();
|
||||
}
|
||||
|
@ -10,13 +10,13 @@
|
||||
|
||||
namespace TweetDuck.Management {
|
||||
sealed class ProfileManager {
|
||||
private static readonly string CookiesPath = Path.Combine(Program.StoragePath, "Cookies");
|
||||
private static readonly string LocalPrefsPath = Path.Combine(Program.StoragePath, "LocalPrefs.json");
|
||||
private static readonly string CookiesPath = Path.Combine(App.StoragePath, "Cookies");
|
||||
private static readonly string LocalPrefsPath = Path.Combine(App.StoragePath, "LocalPrefs.json");
|
||||
|
||||
private static readonly string TempCookiesPath = Path.Combine(Program.StoragePath, "CookiesTmp");
|
||||
private static readonly string TempLocalPrefsPath = Path.Combine(Program.StoragePath, "LocalPrefsTmp.json");
|
||||
private static readonly string TempCookiesPath = Path.Combine(App.StoragePath, "CookiesTmp");
|
||||
private static readonly string TempLocalPrefsPath = Path.Combine(App.StoragePath, "LocalPrefsTmp.json");
|
||||
|
||||
private static readonly int SessionFileCount = 2;
|
||||
private const int SessionFileCount = 2;
|
||||
|
||||
[Flags]
|
||||
public enum Items {
|
||||
@ -41,15 +41,15 @@ public bool Export(Items items) {
|
||||
using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None));
|
||||
|
||||
if (items.HasFlag(Items.UserConfig)) {
|
||||
stream.WriteFile("config", Program.UserConfigFilePath);
|
||||
stream.WriteFile("config", Program.Config.FilePaths.UserConfig);
|
||||
}
|
||||
|
||||
if (items.HasFlag(Items.SystemConfig)) {
|
||||
stream.WriteFile("system", Program.SystemConfigFilePath);
|
||||
stream.WriteFile("system", Program.Config.FilePaths.SystemConfig);
|
||||
}
|
||||
|
||||
if (items.HasFlag(Items.PluginData)) {
|
||||
stream.WriteFile("plugin.config", Program.PluginConfigFilePath);
|
||||
stream.WriteFile("plugin.config", Program.Config.FilePaths.PluginConfig);
|
||||
|
||||
foreach (Plugin plugin in plugins.Plugins) {
|
||||
foreach (PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))) {
|
||||
@ -122,21 +122,21 @@ public bool Import(Items items) {
|
||||
switch (entry.KeyName) {
|
||||
case "config":
|
||||
if (items.HasFlag(Items.UserConfig)) {
|
||||
entry.WriteToFile(Program.UserConfigFilePath);
|
||||
entry.WriteToFile(Program.Config.FilePaths.UserConfig);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "system":
|
||||
if (items.HasFlag(Items.SystemConfig)) {
|
||||
entry.WriteToFile(Program.SystemConfigFilePath);
|
||||
entry.WriteToFile(Program.Config.FilePaths.SystemConfig);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "plugin.config":
|
||||
if (items.HasFlag(Items.PluginData)) {
|
||||
entry.WriteToFile(Program.PluginConfigFilePath);
|
||||
entry.WriteToFile(Program.Config.FilePaths.PluginConfig);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -145,7 +145,7 @@ public bool Import(Items items) {
|
||||
if (items.HasFlag(Items.PluginData)) {
|
||||
string[] value = entry.KeyValue;
|
||||
|
||||
entry.WriteToFile(Path.Combine(Program.PluginDataPath, value[0], value[1]), true);
|
||||
entry.WriteToFile(Path.Combine(plugins.PluginDataFolder, value[0], value[1]), true);
|
||||
|
||||
if (!plugins.Plugins.Any(plugin => plugin.Identifier.Equals(value[0]))) {
|
||||
missingPlugins.Add(value[0]);
|
||||
|
@ -6,7 +6,6 @@
|
||||
using TweetDuck.Configuration;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Communication.Pipe;
|
||||
using TweetLib.Core;
|
||||
|
||||
@ -14,7 +13,7 @@ namespace TweetDuck.Management {
|
||||
sealed class VideoPlayer : IDisposable {
|
||||
private static UserConfig Config => Program.Config.User;
|
||||
|
||||
public bool Running => currentInstance != null && currentInstance.Running;
|
||||
public bool Running => currentInstance is { Running: true };
|
||||
|
||||
public event EventHandler ProcessExited;
|
||||
|
||||
@ -39,7 +38,7 @@ public void Launch(string videoUrl, string tweetUrl, string username) {
|
||||
pipe.DataIn += pipe_DataIn;
|
||||
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo {
|
||||
FileName = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe"),
|
||||
FileName = Path.Combine(App.ProgramPath, "TweetDuck.Video.exe"),
|
||||
Arguments = $"{owner.Handle} {(int) Math.Floor(100F * owner.GetDPIScale())} {Config.VideoPlayerVolume} \"{videoUrl}\" \"{pipe.GenerateToken()}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true
|
||||
@ -83,7 +82,7 @@ private void pipe_DataIn(object sender, DuplexPipe.PipeReadEventArgs e) {
|
||||
|
||||
case "download":
|
||||
if (currentInstance != null) {
|
||||
TwitterUtils.DownloadVideo(currentInstance.VideoUrl, currentInstance.Username);
|
||||
owner.SaveVideo(currentInstance.VideoUrl, currentInstance.Username);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -137,7 +136,7 @@ private void owner_FormClosing(object sender, FormClosingEventArgs e) {
|
||||
|
||||
private void process_OutputDataReceived(object sender, DataReceivedEventArgs e) {
|
||||
if (!string.IsNullOrEmpty(e.Data)) {
|
||||
Program.Reporter.LogVerbose("[VideoPlayer] " + e.Data);
|
||||
App.Logger.Debug("[VideoPlayer] " + e.Data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,14 +154,14 @@ private void process_Exited(object sender, EventArgs e) {
|
||||
switch (exitCode) {
|
||||
case 3: // CODE_LAUNCH_FAIL
|
||||
if (FormMessage.Error("Video Playback Error", "Error launching video player, this may be caused by missing Windows Media Player. Do you want to open the video in your browser?", FormMessage.Yes, FormMessage.No)) {
|
||||
BrowserUtils.OpenExternalBrowser(tweetUrl);
|
||||
App.SystemHandler.OpenBrowser(tweetUrl);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 4: // CODE_MEDIA_ERROR
|
||||
if (FormMessage.Error("Video Playback Error", "The video could not be loaded, most likely due to unknown format. Do you want to open the video in your browser?", FormMessage.Yes, FormMessage.No)) {
|
||||
BrowserUtils.OpenExternalBrowser(tweetUrl);
|
||||
App.SystemHandler.OpenBrowser(tweetUrl);
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -3,6 +3,7 @@
|
||||
using System.Windows.Forms;
|
||||
using TweetDuck.Controls;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.Plugins.Enums;
|
||||
|
||||
@ -80,7 +81,7 @@ private void panelDescription_Resize(object sender, EventArgs e) {
|
||||
|
||||
private void labelWebsite_Click(object sender, EventArgs e) {
|
||||
if (labelWebsite.Text.Length > 0) {
|
||||
BrowserUtils.OpenExternalBrowser(labelWebsite.Text);
|
||||
App.SystemHandler.OpenBrowser(labelWebsite.Text);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.Resources;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
149
Program.cs
149
Program.cs
@ -1,21 +1,24 @@
|
||||
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.Adapters;
|
||||
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.Updates;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core;
|
||||
using TweetLib.Core.Application;
|
||||
using TweetLib.Core.Features;
|
||||
using TweetLib.Core.Features.Chromium;
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Features.TweetDeck;
|
||||
using TweetLib.Core.Resources;
|
||||
using TweetLib.Utils.Collections;
|
||||
using TweetLib.Utils.Static;
|
||||
using Win = System.Windows.Forms;
|
||||
@ -27,47 +30,22 @@ static class Program {
|
||||
|
||||
public const string Website = "https://tweetduck.chylex.com";
|
||||
|
||||
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
public static readonly string ExecutablePath = Win.Application.ExecutablePath;
|
||||
private const string PluginDataFolder = "TD_Plugins";
|
||||
private const string InstallerFolder = "TD_Updates";
|
||||
private const string CefDataFolder = "TD_Chromium";
|
||||
|
||||
public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
|
||||
private const string ProgramLogFile = "TD_Log.txt";
|
||||
private const string ConsoleLogFile = "TD_Console.txt";
|
||||
|
||||
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 string ExecutablePath => Win.Application.ExecutablePath;
|
||||
|
||||
public static uint WindowRestoreMessage;
|
||||
|
||||
private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
|
||||
private static LockManager lockManager;
|
||||
private static Reporter errorReporter;
|
||||
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(),
|
||||
});
|
||||
}
|
||||
public static ConfigManager Config { get; private set; }
|
||||
|
||||
internal static void SetupWinForms() {
|
||||
Win.Application.EnableVisualStyles();
|
||||
@ -76,17 +54,46 @@ internal static void SetupWinForms() {
|
||||
|
||||
[STAThread]
|
||||
private static void Main() {
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
|
||||
SetupWinForms();
|
||||
Cef.EnableHighDPISupport();
|
||||
|
||||
var startup = new AppStartup {
|
||||
CustomDataFolder = Arguments.GetValue(Arguments.ArgDataFolder)
|
||||
};
|
||||
|
||||
var reporter = new Reporter();
|
||||
var userConfig = new UserConfig();
|
||||
|
||||
Lib.Initialize(new AppBuilder {
|
||||
Startup = startup,
|
||||
Logger = new Logger(ProgramLogFile),
|
||||
ErrorHandler = reporter,
|
||||
SystemHandler = new SystemHandler(),
|
||||
DialogHandler = new DialogHandler(),
|
||||
UserConfiguration = userConfig
|
||||
});
|
||||
|
||||
LaunchApp(reporter, userConfig);
|
||||
}
|
||||
|
||||
private static void LaunchApp(Reporter reporter, UserConfig userConfig) {
|
||||
App.Launch();
|
||||
|
||||
errorReporter = reporter;
|
||||
string storagePath = App.StoragePath;
|
||||
|
||||
Config = new ConfigManager(userConfig, new ConfigManager.Paths {
|
||||
UserConfig = Path.Combine(storagePath, "TD_UserConfig.cfg"),
|
||||
SystemConfig = Path.Combine(storagePath, "TD_SystemConfig.cfg"),
|
||||
PluginConfig = Path.Combine(storagePath, "TD_PluginConfig.cfg")
|
||||
});
|
||||
|
||||
lockManager = new LockManager(Path.Combine(storagePath, ".lock"));
|
||||
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))) {
|
||||
if (!lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -99,19 +106,23 @@ private static void Main() {
|
||||
ProfileManager.DeleteCookies();
|
||||
}
|
||||
|
||||
var installerFolderPath = Path.Combine(storagePath, InstallerFolder);
|
||||
|
||||
if (Arguments.HasFlag(Arguments.ArgUpdated)) {
|
||||
WindowsUtils.TryDeleteFolderWhenAble(InstallerPath, 8000);
|
||||
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(StoragePath, "Service Worker"), 4000);
|
||||
WindowsUtils.TryDeleteFolderWhenAble(installerFolderPath, 8000);
|
||||
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(storagePath, "Service Worker"), 4000);
|
||||
BrowserCache.TryClearNow();
|
||||
}
|
||||
|
||||
try {
|
||||
ResourceRequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
|
||||
BaseResourceRequestHandler.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
|
||||
} catch (Exception e) {
|
||||
FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK);
|
||||
return;
|
||||
}
|
||||
|
||||
WebUtils.DefaultUserAgent = BrowserUtils.UserAgentVanilla;
|
||||
|
||||
if (Config.User.UseSystemProxyForAllConnections) {
|
||||
WebUtils.EnableSystemProxy();
|
||||
}
|
||||
@ -122,21 +133,20 @@ private static void Main() {
|
||||
|
||||
CefSettings settings = new CefSettings {
|
||||
UserAgent = BrowserUtils.UserAgentChrome,
|
||||
BrowserSubprocessPath = Path.Combine(ProgramPath, BrandName + ".Browser.exe"),
|
||||
CachePath = StoragePath,
|
||||
UserDataPath = CefDataPath,
|
||||
LogFile = ConsoleLogFilePath,
|
||||
BrowserSubprocessPath = Path.Combine(App.ProgramPath, BrandName + ".Browser.exe"),
|
||||
CachePath = storagePath,
|
||||
UserDataPath = Path.Combine(storagePath, CefDataFolder),
|
||||
LogFile = Path.Combine(storagePath, ConsoleLogFile),
|
||||
#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);
|
||||
var resourceProvider = new CachingResourceProvider<IResourceHandler>(new ResourceProvider());
|
||||
var pluginManager = new PluginManager(Config.Plugins, Path.Combine(storagePath, PluginDataFolder));
|
||||
|
||||
settings.SetupCustomScheme(ResourceSchemeFactory.Name, resourceScheme);
|
||||
settings.SetupCustomScheme(PluginSchemeFactory.Name, pluginScheme);
|
||||
CefSchemeHandlerFactory.Register(settings, new TweetDuckSchemeHandler<IResourceHandler>(resourceProvider));
|
||||
CefSchemeHandlerFactory.Register(settings, new PluginSchemeHandler<IResourceHandler>(resourceProvider, pluginManager));
|
||||
|
||||
CefUtils.ParseCommandLineArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
|
||||
BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
|
||||
@ -144,8 +154,7 @@ private static void Main() {
|
||||
Cef.Initialize(settings, false, new BrowserProcessHandler());
|
||||
|
||||
Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
|
||||
|
||||
FormBrowser mainForm = new FormBrowser(resourceProvider, pluginScheme);
|
||||
FormBrowser mainForm = new FormBrowser(resourceProvider, pluginManager, new UpdateCheckClient(installerFolderPath));
|
||||
Win.Application.Run(mainForm);
|
||||
|
||||
if (mainForm.UpdateInstaller != null) {
|
||||
@ -160,21 +169,19 @@ private static void Main() {
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDataStoragePath() {
|
||||
string custom = Arguments.GetValue(Arguments.ArgDataFolder);
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) {
|
||||
if (e.ExceptionObject is Exception ex) {
|
||||
AppException appEx = ex.GetBaseException() as AppException;
|
||||
string title = appEx?.Title ?? "TweetDuck Has Failed :(";
|
||||
string message = appEx?.Message ?? "An unhandled exception has occurred: " + ex.Message;
|
||||
|
||||
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);
|
||||
if (errorReporter == null) {
|
||||
Debug.WriteLine(ex);
|
||||
Reporter.HandleEarlyFailure(title, message);
|
||||
}
|
||||
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);
|
||||
else {
|
||||
errorReporter.HandleException(title, message, false, ex);
|
||||
}
|
||||
|
||||
return Environment.ExpandEnvironmentVariables(custom);
|
||||
}
|
||||
else {
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), custom ?? BrandName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,7 +218,7 @@ private static void ExitCleanup() {
|
||||
Cef.Shutdown();
|
||||
BrowserCache.Exit();
|
||||
|
||||
LockManager.Unlock();
|
||||
lockManager.Unlock();
|
||||
hasCleanedUp = true;
|
||||
}
|
||||
}
|
||||
|
20
Properties/Resources.Designer.cs
generated
20
Properties/Resources.Designer.cs
generated
@ -60,16 +60,6 @@ internal Resources() {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Byte[].
|
||||
/// </summary>
|
||||
internal static byte[] avatar {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("avatar", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
|
||||
/// </summary>
|
||||
@ -119,15 +109,5 @@ internal static System.Drawing.Icon icon_tray_new {
|
||||
return ((System.Drawing.Icon)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Byte[].
|
||||
/// </summary>
|
||||
internal static byte[] spinner {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("spinner", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,9 +118,6 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="avatar" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Images\avatar.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="icon" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Images\icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
@ -136,7 +133,4 @@
|
||||
<data name="icon_tray_new" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Images\icon-tray-new.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="spinner" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Images\spinner.apng;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
</root>
|
127
Reporter.cs
127
Reporter.cs
@ -1,109 +1,64 @@
|
||||
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 TweetDuck.Management;
|
||||
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;
|
||||
}
|
||||
|
||||
private static void Exit(string message, Exception ex = null) {
|
||||
try {
|
||||
Process.GetCurrentProcess().Kill();
|
||||
} catch {
|
||||
Environment.FailFast(message, e);
|
||||
Environment.FailFast(message, ex ?? new Exception(message));
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleEarlyFailure(string caption, string message) {
|
||||
Program.SetupWinForms();
|
||||
FormMessage.Error(caption, message, "Exit");
|
||||
Exit(message);
|
||||
}
|
||||
|
||||
try {
|
||||
Process.GetCurrentProcess().Kill();
|
||||
} catch {
|
||||
Environment.FailFast(message, new Exception(message));
|
||||
}
|
||||
public void HandleException(string caption, string message, bool canIgnore, Exception e) {
|
||||
bool loggedSuccessfully = App.Logger.Error(e.ToString());
|
||||
|
||||
FormManager.RunOnUIThread(() => {
|
||||
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) => {
|
||||
if (!App.Logger.OpenLogFile()) {
|
||||
FormMessage.Error("Error Log", "Cannot open error log.", FormMessage.OK);
|
||||
}
|
||||
};
|
||||
|
||||
form.AddActionControl(btnOpenLog);
|
||||
|
||||
if (form.ShowDialog() != DialogResult.Ignore) {
|
||||
Exit(message, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public sealed class ExpandedLogException : Exception {
|
||||
|
Before ![]() (image error) Size: 4.4 KiB After ![]() (image error) Size: 4.4 KiB ![]() ![]() |
Before (image error) Size: 12 KiB After (image error) Size: 12 KiB |
@ -7,7 +7,7 @@
|
||||
</time>
|
||||
<a target="_blank" rel="user" href="https://twitter.com/TryMyAwesomeApp" class="account-link link-complex block">
|
||||
<div class="obj-left item-img tweet-img">
|
||||
<img width="48" height="48" alt="TryMyAwesomeApp's avatar" src="{avatar}" class="tweet-avatar avatar pull-right">
|
||||
<img width="48" height="48" alt="TryMyAwesomeApp's avatar" src="td://resources/images/logo.png" class="tweet-avatar avatar pull-right">
|
||||
</div>
|
||||
<div class="nbfc">
|
||||
<span class="account-inline txt-ellipsis">
|
||||
|
@ -7,7 +7,7 @@ import { checkPropertyExists } from "../api/utils.js";
|
||||
*/
|
||||
export default function() {
|
||||
const realDisplayName = "TweetDuck";
|
||||
const realAvatar = "https://ton.twimg.com/tduck/avatar";
|
||||
const realAvatar = "td://resources/images/logo.png";
|
||||
const accountId = "957608948189880320";
|
||||
|
||||
if (checkPropertyExists(TD, "services", "TwitterUser", "prototype")) {
|
||||
|
@ -34,3 +34,7 @@ if (location.hash.length > 1) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", function() {
|
||||
location.reload();
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
#if DEBUG
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using TweetLib.Core;
|
||||
|
||||
namespace TweetDuck.Resources {
|
||||
static class ResourceHotSwap {
|
||||
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../")));
|
||||
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(App.ProgramPath, "../../../")));
|
||||
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
|
||||
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe");
|
||||
|
||||
@ -44,11 +45,11 @@ public static void Run() {
|
||||
sw.Stop();
|
||||
Debug.WriteLine($"Finished rebuild script in {sw.ElapsedMilliseconds} ms");
|
||||
|
||||
Directory.Delete(Program.ResourcesPath, true);
|
||||
Directory.Delete(Program.PluginPath, true);
|
||||
Directory.Delete(App.ResourcesPath, true);
|
||||
Directory.Delete(App.PluginPath, true);
|
||||
|
||||
Directory.Move(Path.Combine(HotSwapTargetDir, "resources"), Program.ResourcesPath);
|
||||
Directory.Move(Path.Combine(HotSwapTargetDir, "plugins"), Program.PluginPath);
|
||||
Directory.Move(Path.Combine(HotSwapTargetDir, "resources"), App.ResourcesPath);
|
||||
Directory.Move(Path.Combine(HotSwapTargetDir, "plugins"), App.PluginPath);
|
||||
|
||||
DeleteHotSwapFolder();
|
||||
}
|
||||
|
@ -1,100 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Browser;
|
||||
|
||||
namespace TweetDuck.Resources {
|
||||
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,20 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using TweetLib.Core;
|
||||
|
||||
namespace TweetDuck.Resources {
|
||||
public static class ResourceUtils {
|
||||
public static string ReadFileOrNull(string relativePath) {
|
||||
string path = Path.Combine(Program.ResourcesPath, relativePath);
|
||||
|
||||
try {
|
||||
return File.ReadAllText(path, Encoding.UTF8);
|
||||
} catch (Exception e) {
|
||||
App.ErrorHandler.Log("Error reading file: " + path);
|
||||
App.ErrorHandler.Log(e.ToString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using CefSharp;
|
||||
using TweetLib.Core.Browser;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -62,36 +62,37 @@
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Application\DialogHandler.cs" />
|
||||
<Compile Include="Application\Logger.cs" />
|
||||
<Compile Include="Browser\Adapters\CefBrowserComponent.cs" />
|
||||
<Compile Include="Browser\Adapters\CefContextMenuActionRegistry.cs" />
|
||||
<Compile Include="Browser\Adapters\CefContextMenuModel.cs" />
|
||||
<Compile Include="Browser\Adapters\CefResourceHandlerFactory.cs" />
|
||||
<Compile Include="Browser\Adapters\CefResourceHandlerRegistry.cs" />
|
||||
<Compile Include="Browser\Adapters\CefResourceProvider.cs" />
|
||||
<Compile Include="Browser\Adapters\CefResourceRequestHandler.cs" />
|
||||
<Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
|
||||
<Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
|
||||
<Compile Include="Browser\Handling\CustomLifeSpanHandler.cs" />
|
||||
<Compile Include="Browser\Handling\FileDialogHandler.cs" />
|
||||
<Compile Include="Browser\Handling\JavaScriptDialogHandler.cs" />
|
||||
<Compile Include="Browser\Handling\ResponseFilter.cs" />
|
||||
<Compile Include="Browser\Notification\FormNotificationExample.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Browser\TweetDeckInterfaceImpl.cs" />
|
||||
<Compile Include="Dialogs\WindowState.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\CustomKeyboardHandler.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" />
|
||||
@ -107,20 +108,15 @@
|
||||
<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="Resources\ResourceProvider.cs" />
|
||||
<Compile Include="Resources\ResourceUtils.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\TwitterFileDownloader.cs" />
|
||||
<Compile Include="Utils\WindowsUtils.cs" />
|
||||
<Compile Include="Version.cs" />
|
||||
</ItemGroup>
|
||||
@ -143,9 +139,6 @@
|
||||
<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>
|
||||
@ -323,19 +316,19 @@
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="Resources\Content\error\error.html" />
|
||||
<None Include="Resources\Content\images\logo.png" />
|
||||
<None Include="Resources\Content\images\spinner.apng" />
|
||||
<None Include="Resources\Content\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" />
|
||||
@ -364,6 +357,10 @@
|
||||
<Redist Include="$(ProjectDir)bld\Redist\*.*" Visible="false" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="lib\TweetLib.Browser\TweetLib.Browser.csproj">
|
||||
<Project>{eefb1f37-7cad-46bd-8042-66e7b502ab02}</Project>
|
||||
<Name>TweetLib.Browser</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj">
|
||||
<Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
|
||||
<Name>TweetLib.Core</Name>
|
||||
|
@ -10,7 +10,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "video\Tw
|
||||
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}") = "TweetDuck.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser", "lib\TweetLib.Browser\TweetLib.Browser.csproj", "{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Utils", "lib\TweetLib.Utils\TweetLib.Utils.csproj", "{476B1007-B12C-447F-B855-9886048201D6}"
|
||||
EndProject
|
||||
@ -44,6 +46,10 @@ Global
|
||||
{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
|
||||
{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
|
||||
|
@ -5,7 +5,6 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
using TweetDuck.Utils;
|
||||
using TweetLib.Core.Systems.Updates;
|
||||
using TweetLib.Utils.Static;
|
||||
using JsonObject = System.Collections.Generic.IDictionary<string, object>;
|
||||
@ -24,9 +23,9 @@ public UpdateCheckClient(string installerFolder) {
|
||||
bool IUpdateCheckClient.CanCheck => Program.Config.User.EnableUpdateCheck;
|
||||
|
||||
Task<UpdateInfo> IUpdateCheckClient.Check() {
|
||||
TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>();
|
||||
var result = new TaskCompletionSource<UpdateInfo>();
|
||||
|
||||
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
|
||||
WebClient client = WebUtils.NewClient();
|
||||
client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json";
|
||||
|
||||
client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {
|
||||
@ -57,7 +56,7 @@ static string AssetDownloadUrl(JsonObject obj) {
|
||||
return (string) obj["browser_download_url"];
|
||||
}
|
||||
|
||||
JsonObject root = (JsonObject) new JavaScriptSerializer().DeserializeObject(json);
|
||||
var root = (JsonObject) new JavaScriptSerializer().DeserializeObject(json);
|
||||
|
||||
string versionTag = (string) root["tag_name"];
|
||||
string releaseNotes = (string) root["body"];
|
||||
@ -67,7 +66,7 @@ static string AssetDownloadUrl(JsonObject obj) {
|
||||
}
|
||||
|
||||
private static Exception ExpandWebException(Exception e) {
|
||||
if (e is WebException we && we.Response is HttpWebResponse response) {
|
||||
if (e is WebException { Response: HttpWebResponse response } ) {
|
||||
try {
|
||||
using var stream = response.GetResponseStream();
|
||||
using var reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"));
|
||||
|
@ -15,8 +15,8 @@ public UpdateInstaller(string path) {
|
||||
|
||||
public bool Launch() {
|
||||
// ProgramPath has a trailing backslash
|
||||
string arguments = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\"" + Program.ProgramPath + "\" /RUNARGS=\"" + Arguments.GetCurrentForInstallerCmd() + "\"" + (Program.IsPortable ? " /PORTABLE=1" : "");
|
||||
bool runElevated = !Program.IsPortable || !FileUtils.CheckFolderWritePermission(Program.ProgramPath);
|
||||
string arguments = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\"" + App.ProgramPath + "\" /RUNARGS=\"" + Arguments.GetCurrentForInstallerCmd() + "\"" + (App.IsPortable ? " /PORTABLE=1" : "");
|
||||
bool runElevated = !App.IsPortable || !FileUtils.CheckFolderWritePermission(App.ProgramPath);
|
||||
|
||||
try {
|
||||
using (Process.Start(new ProcessStartInfo {
|
||||
|
@ -1,17 +1,12 @@
|
||||
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;
|
||||
using TweetDuck.Controls;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetDuck.Utils {
|
||||
static class BrowserUtils {
|
||||
@ -52,19 +47,16 @@ public static void SetupCefArgs(IDictionary<string, string> args) {
|
||||
}
|
||||
}
|
||||
|
||||
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 void SetupDockOnLoad(IBrowserComponent browserComponent, ChromiumWebBrowser browser) {
|
||||
browser.Dock = DockStyle.None;
|
||||
browser.Location = ControlExtensions.InvisibleLocation;
|
||||
|
||||
public static ChromiumWebBrowser AsControl(this IWebBrowser browserControl) {
|
||||
return (ChromiumWebBrowser) browserControl;
|
||||
browserComponent.BrowserLoaded += (sender, args) => {
|
||||
browser.InvokeAsyncSafe(() => {
|
||||
browser.Location = Point.Empty;
|
||||
browser.Dock = DockStyle.Fill;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public static void SetupZoomEvents(this ChromiumWebBrowser browser) {
|
||||
@ -86,20 +78,6 @@ void UpdateZoomLevel(object sender, EventArgs args) {
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
@ -112,96 +90,6 @@ public static void OpenDevToolsCustom(this IWebBrowser browser, Point? inspectPo
|
||||
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) {
|
||||
App.ErrorHandler.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);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
namespace TweetDuck.Utils {
|
||||
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
static class NativeMethods {
|
||||
public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
|
||||
public static readonly IntPtr HOOK_HANDLED = new IntPtr(-1);
|
||||
|
41
Utils/TwitterFileDownloader.cs
Normal file
41
Utils/TwitterFileDownloader.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Utils.Static;
|
||||
using Cookie = CefSharp.Cookie;
|
||||
|
||||
namespace TweetDuck.Utils {
|
||||
sealed class TwitterFileDownloader : IFileDownloader {
|
||||
public static TwitterFileDownloader Instance { get; } = new TwitterFileDownloader();
|
||||
|
||||
private TwitterFileDownloader() {}
|
||||
|
||||
public string CacheFolder => BrowserCache.CacheFolder;
|
||||
|
||||
public void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onFailure) {
|
||||
const string authCookieName = "auth_token";
|
||||
|
||||
using ICookieManager cookies = Cef.GetGlobalCookieManager();
|
||||
|
||||
cookies.VisitUrlCookiesAsync(url, true).ContinueWith(task => {
|
||||
string cookieStr = null;
|
||||
|
||||
if (task.Status == TaskStatus.RanToCompletion) {
|
||||
Cookie found = task.Result?.Find(cookie => cookie.Name == authCookieName); // the list may be null
|
||||
|
||||
if (found != null) {
|
||||
cookieStr = $"{found.Name}={found.Value}";
|
||||
}
|
||||
}
|
||||
|
||||
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentChrome);
|
||||
client.Headers[HttpRequestHeader.Cookie] = cookieStr;
|
||||
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(path, onSuccess, onFailure);
|
||||
client.DownloadFileAsync(new Uri(url), path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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.Utils.Static;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -72,7 +72,7 @@ static IEnumerable<Browser> ReadBrowsersFromKey(RegistryHive hive) {
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Browser> browsers = new HashSet<Browser>();
|
||||
var browsers = new HashSet<Browser>();
|
||||
|
||||
try {
|
||||
browsers.UnionWith(ReadBrowsersFromKey(RegistryHive.CurrentUser));
|
||||
|
19
lib/TweetLib.Browser/Base/BaseBrowser.cs
Normal file
19
lib/TweetLib.Browser/Base/BaseBrowser.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.Base {
|
||||
public class BaseBrowser<T> : IDisposable where T : BaseBrowser<T> {
|
||||
public IScriptExecutor ScriptExecutor { get; }
|
||||
|
||||
protected readonly IBrowserComponent browserComponent;
|
||||
|
||||
protected BaseBrowser(IBrowserComponent browserComponent, Func<T, BrowserSetup> setup) {
|
||||
this.browserComponent = browserComponent;
|
||||
this.browserComponent.Setup(setup((T) this));
|
||||
|
||||
this.ScriptExecutor = browserComponent;
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
11
lib/TweetLib.Browser/Contexts/Link.cs
Normal file
11
lib/TweetLib.Browser/Contexts/Link.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace TweetLib.Browser.Contexts {
|
||||
public readonly struct Link {
|
||||
public string Url { get; }
|
||||
public string CopyUrl { get; }
|
||||
|
||||
public Link(string url, string copyUrl) {
|
||||
Url = url;
|
||||
CopyUrl = copyUrl;
|
||||
}
|
||||
}
|
||||
}
|
16
lib/TweetLib.Browser/Contexts/Media.cs
Normal file
16
lib/TweetLib.Browser/Contexts/Media.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace TweetLib.Browser.Contexts {
|
||||
public readonly struct Media {
|
||||
public Type MediaType { get; }
|
||||
public string Url { get; }
|
||||
|
||||
public Media(Type mediaType, string url) {
|
||||
MediaType = mediaType;
|
||||
Url = url;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
Image,
|
||||
Video
|
||||
}
|
||||
}
|
||||
}
|
11
lib/TweetLib.Browser/Contexts/Notification.cs
Normal file
11
lib/TweetLib.Browser/Contexts/Notification.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace TweetLib.Browser.Contexts {
|
||||
public struct Notification {
|
||||
public string TweetUrl { get; }
|
||||
public string? QuoteUrl { get; }
|
||||
|
||||
public Notification(string tweetUrl, string? quoteUrl) {
|
||||
TweetUrl = tweetUrl;
|
||||
QuoteUrl = quoteUrl;
|
||||
}
|
||||
}
|
||||
}
|
11
lib/TweetLib.Browser/Contexts/Selection.cs
Normal file
11
lib/TweetLib.Browser/Contexts/Selection.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace TweetLib.Browser.Contexts {
|
||||
public readonly struct Selection {
|
||||
public string Text { get; }
|
||||
public bool Editable { get; }
|
||||
|
||||
public Selection(string text, bool editable) {
|
||||
Text = text;
|
||||
Editable = editable;
|
||||
}
|
||||
}
|
||||
}
|
24
lib/TweetLib.Browser/Contexts/Tweet.cs
Normal file
24
lib/TweetLib.Browser/Contexts/Tweet.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace TweetLib.Browser.Contexts {
|
||||
public readonly struct Tweet {
|
||||
public string Url { get; }
|
||||
public string? QuoteUrl { get; }
|
||||
public string? Author { get; }
|
||||
public string? QuoteAuthor { get; }
|
||||
public string[] ImageUrls { get; }
|
||||
|
||||
public string ColumnId { get; }
|
||||
public string ChirpId { get; }
|
||||
|
||||
public string? MediaAuthor => QuoteAuthor ?? Author;
|
||||
|
||||
public Tweet(string url, string? quoteUrl, string? author, string? quoteAuthor, string[] imageUrls, string columnId, string chirpId) {
|
||||
Url = url;
|
||||
QuoteUrl = quoteUrl;
|
||||
Author = author;
|
||||
QuoteAuthor = quoteAuthor;
|
||||
ImageUrls = imageUrls;
|
||||
ColumnId = columnId;
|
||||
ChirpId = chirpId;
|
||||
}
|
||||
}
|
||||
}
|
7
lib/TweetLib.Browser/Events/BrowserLoadedEventArgs.cs
Normal file
7
lib/TweetLib.Browser/Events/BrowserLoadedEventArgs.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace TweetLib.Browser.Events {
|
||||
public abstract class BrowserLoadedEventArgs : EventArgs {
|
||||
public abstract void AddDictionaryWords(params string[] word);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user