1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-21 06:15:47 +02:00

Work on abstracting app logic and making some implementation optional

This commit is contained in:
chylex 2022-01-10 07:54:20 +01:00
parent ec7827df24
commit fa534f9eb3
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
77 changed files with 1062 additions and 924 deletions
Application
Browser
Configuration
Controls
Dialogs
Management
Plugins
Program.csReporter.csTweetDuck.csproj
Utils
lib

View File

@ -2,21 +2,12 @@
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));
}
sealed class FileDialogs : IAppFileDialogs {
public void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted) {
static string FormatFilter(FileDialogFilter filter) {
var builder = new StringBuilder();

View File

@ -1,68 +0,0 @@
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 Configuration.Arguments.HasFlag(Configuration.Arguments.ArgLogging) && Log("DEBUG", 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;
}
}
}
}

View File

@ -0,0 +1,15 @@
using TweetDuck.Dialogs;
using TweetDuck.Management;
using TweetLib.Core.Application;
namespace TweetDuck.Application {
sealed class MessageDialogs : IAppMessageDialogs {
public void Information(string caption, string text, string buttonAccept) {
FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept));
}
public void Error(string caption, string text, string buttonAccept) {
FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept));
}
}
}

View File

@ -4,26 +4,15 @@
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;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Application {
sealed class SystemHandler : IAppSystemHandler {
public void OpenAssociatedProgram(string path) {
try {
using (Process.Start(new ProcessStartInfo {
FileName = path,
ErrorDialog = true
})) {}
} catch (Exception e) {
App.ErrorHandler.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
}
}
public void OpenBrowser(string url) {
if (string.IsNullOrWhiteSpace(url)) {
return;
@ -92,7 +81,19 @@ public void OpenFileExplorer(string path) {
}
}
public void CopyImageFromFile(string path) {
public IAppSystemHandler.OpenAssociatedProgramFunc OpenAssociatedProgram { get; } = path => {
try {
using (Process.Start(new ProcessStartInfo {
FileName = path,
ErrorDialog = true
})) {}
} catch (Exception e) {
App.ErrorHandler.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
}
};
public IAppSystemHandler.CopyImageFromFileFunc CopyImageFromFile { get; } = path => {
FormManager.RunOnUIThreadAsync(() => {
Image image;
@ -105,18 +106,18 @@ public void CopyImageFromFile(string path) {
ClipboardManager.SetImage(image);
});
}
};
public void CopyText(string text) {
public IAppSystemHandler.CopyTextFunc CopyText { get; } = text => {
FormManager.RunOnUIThreadAsync(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
}
};
public void SearchText(string text) {
public IAppSystemHandler.SearchTextFunc SearchText { get; } = text => {
if (string.IsNullOrWhiteSpace(text)) {
return;
}
FormManager.RunOnUIThreadAsync(() => {
void PerformSearch() {
var config = Program.Config.User;
string searchUrl = config.SearchEngineUrl;
@ -138,15 +139,17 @@ public void SearchText(string text) {
settings.FormClosed += (sender, args) => {
if (args.CloseReason == CloseReason.UserClosing && config.SearchEngineUrl != searchUrl) {
SearchText(text);
PerformSearch();
}
};
}
}
else {
OpenBrowser(searchUrl + Uri.EscapeUriString(text));
App.SystemHandler.OpenBrowser(searchUrl + Uri.EscapeUriString(text));
}
});
}
}
FormManager.RunOnUIThreadAsync(PerformSearch);
};
}
}

View File

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

View File

@ -5,7 +5,7 @@
namespace TweetDuck.Browser.Adapters {
internal sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory {
public static void Register(CefSettings settings, ICustomSchemeHandler<IResourceHandler> handler) {
public static void Register(CefSettings settings, ICustomSchemeHandler handler) {
settings.RegisterScheme(new CefCustomScheme {
SchemeName = handler.Protocol,
IsStandard = false,
@ -16,14 +16,14 @@ public static void Register(CefSettings settings, ICustomSchemeHandler<IResource
});
}
private readonly ICustomSchemeHandler<IResourceHandler> handler;
private readonly ICustomSchemeHandler handler;
private CefSchemeHandlerFactory(ICustomSchemeHandler<IResourceHandler> handler) {
private CefSchemeHandlerFactory(ICustomSchemeHandler handler) {
this.handler = handler;
}
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri) : null;
return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(CefSchemeResourceVisitor.Instance) : null;
}
}
}

View File

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

View File

@ -21,6 +21,7 @@
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.TweetDeck;
using TweetLib.Core.Resources;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Core.Systems.Updates;
namespace TweetDuck.Browser {
@ -51,11 +52,12 @@ public bool IsWaiting {
private readonly FormNotificationTweet notification;
#pragma warning restore IDE0069 // Disposable fields should be disposed
private readonly CachingResourceProvider<IResourceHandler> resourceProvider;
private readonly ResourceCache resourceCache;
private readonly ITweetDeckInterface tweetDeckInterface;
private readonly PluginManager plugins;
private readonly UpdateChecker updates;
private readonly ContextMenu contextMenu;
private readonly uint windowRestoreMessage;
private bool isLoaded;
private FormWindowState prevState;
@ -63,12 +65,12 @@ public bool IsWaiting {
private TweetScreenshotManager notificationScreenshotManager;
private VideoPlayer videoPlayer;
public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, PluginManager pluginManager, IUpdateCheckClient updateCheckClient) {
public FormBrowser(ResourceCache resourceCache, PluginManager pluginManager, IUpdateCheckClient updateCheckClient, uint windowRestoreMessage) {
InitializeComponent();
Text = Program.BrandName;
this.resourceProvider = resourceProvider;
this.resourceCache = resourceCache;
this.plugins = pluginManager;
@ -84,19 +86,21 @@ public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, P
this.browser = new TweetDeckBrowser(this, plugins, tweetDeckInterface, updates);
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
this.windowRestoreMessage = windowRestoreMessage;
Controls.Add(new MenuStrip { Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
Config.MuteToggled += Config_MuteToggled;
Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
Disposed += (sender, args) => {
Config.MuteToggled -= Config_MuteToggled;
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
browser.Dispose();
};
Config.MuteToggled += Config_MuteToggled;
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
this.trayIcon.ClickClose += trayIcon_ClickClose;
Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
UpdateTray();
@ -162,7 +166,7 @@ private void FormBrowser_Activated(object sender, EventArgs e) {
trayIcon.HasNotifications = false;
if (!browser.Enabled) { // when taking a screenshot, the window is unfocused and
if (!browser.Enabled) { // when taking a screenshot, the window is unfocused and
browser.Enabled = true; // the browser is disabled; if the user clicks back into
} // the window, enable the browser again
}
@ -308,7 +312,7 @@ private void updateInteractionManager_UpdateDismissed(object sender, UpdateInfo
}
protected override void WndProc(ref Message m) {
if (isLoaded && m.Msg == Program.WindowRestoreMessage) {
if (isLoaded && m.Msg == windowRestoreMessage) {
using Process me = Process.GetCurrentProcess();
if (me.Id == m.WParam.ToInt32()) {
@ -345,10 +349,10 @@ public void ResumeNotification() {
public void ReloadToTweetDeck() {
#if DEBUG
Resources.ResourceHotSwap.Run();
resourceProvider.ClearCache();
resourceCache.ClearCache();
#else
if (ModifierKeys.HasFlag(Keys.Shift)) {
resourceProvider.ClearCache();
resourceCache.ClearCache();
}
#endif
@ -361,7 +365,7 @@ public void OpenDevTools() {
// callback handlers
public void OnIntroductionClosed(bool showGuide) {
private void OnIntroductionClosed(bool showGuide) {
if (Config.FirstRun) {
Config.FirstRun = false;
Config.Save();
@ -372,7 +376,7 @@ public void OnIntroductionClosed(bool showGuide) {
}
}
public void OpenContextMenu() {
private void OpenContextMenu() {
contextMenu.Show(this, PointToClient(Cursor.Position));
}
@ -428,7 +432,7 @@ public void OpenPlugins() {
}
}
public void OpenProfileImport() {
private void OpenProfileImport() {
FormManager.TryFind<FormSettings>()?.Close();
using DialogSettingsManage dialog = new DialogSettingsManage(plugins, true);
@ -440,11 +444,11 @@ public void OpenProfileImport() {
}
}
public void ShowDesktopNotification(DesktopNotification notification) {
private void ShowDesktopNotification(DesktopNotification notification) {
this.notification.ShowNotification(notification);
}
public void OnTweetNotification() { // may be called multiple times, once for each type of notification
private void OnTweetNotification() { // may be called multiple times, once for each type of notification
if (Config.EnableTrayHighlight && !ContainsFocus) {
trayIcon.HasNotifications = true;
}
@ -454,7 +458,7 @@ public void SaveVideo(string url, string username) {
browser.SaveVideo(url, username);
}
public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
private void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
if (Arguments.HasFlag(Arguments.ArgHttpVideo)) {
videoUrl = Regex.Replace(videoUrl, "^https://", "http://");
}
@ -486,7 +490,7 @@ public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavasc
}
}
public void StopVideo() {
private void StopVideo() {
videoPlayer?.Close();
}
@ -502,12 +506,12 @@ public bool ShowTweetDetail(string columnId, string chirpId, string fallbackUrl)
return true;
}
public void OnTweetScreenshotReady(string html, int width) {
private void OnTweetScreenshotReady(string html, int width) {
notificationScreenshotManager ??= new TweetScreenshotManager(this, plugins);
notificationScreenshotManager.Trigger(html, width);
}
public void DisplayTooltip(string text) {
private void DisplayTooltip(string text) {
if (string.IsNullOrEmpty(text)) {
toolTip.Hide(this);
}
@ -530,5 +534,75 @@ bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) {
return false;
}
private 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);
}
}
}
}

View File

@ -1,11 +1,11 @@
using System.Windows.Forms;
using CefSharp;
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 TweetLib.Core.Systems.Configuration;
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
namespace TweetDuck.Browser.Handling {

View File

@ -4,6 +4,7 @@
using System.Linq;
using System.Windows.Forms;
using CefSharp;
using TweetLib.Core;
using TweetLib.Utils.Static;
namespace TweetDuck.Browser.Handling {
@ -46,17 +47,22 @@ private static IEnumerable<string> ParseFileType(string type) {
return new string[] { type };
}
switch (type) {
case "image/jpeg": return new string[] { ".jpg", ".jpeg" };
case "image/png": return new string[] { ".png" };
case "image/gif": return new string[] { ".gif" };
case "image/webp": return new string[] { ".webp" };
case "video/mp4": return new string[] { ".mp4" };
case "video/quicktime": return new string[] { ".mov", ".qt" };
string[] extensions = type switch {
"image/jpeg" => new string[] { ".jpg", ".jpeg" },
"image/png" => new string[] { ".png" },
"image/gif" => new string[] { ".gif" },
"image/webp" => new string[] { ".webp" },
"video/mp4" => new string[] { ".mp4" },
"video/quicktime" => new string[] { ".mov", ".qt" },
_ => StringUtils.EmptyArray
};
if (extensions.Length == 0) {
App.Logger.Warn("Unknown file type: " + type);
Debugger.Break();
}
Debugger.Break();
return StringUtils.EmptyArray;
return extensions;
}
}
}

View File

@ -9,6 +9,7 @@
using TweetLib.Browser.Interfaces;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Browser.Notification {
abstract partial class FormNotificationBase : Form {
@ -59,7 +60,9 @@ protected virtual Point PrimaryLocation {
protected virtual bool CanDragWindow => true;
public new Point Location {
get { return base.Location; }
get {
return base.Location;
}
set {
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;

View File

@ -61,7 +61,9 @@ private static int FontSizeLevel {
private int? prevFontSize;
public virtual bool RequiresResize {
get { return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel; }
get {
return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel;
}
set {
if (value) {

View File

@ -2,6 +2,7 @@
using System.ComponentModel;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetLib.Core.Systems.Configuration;
using Res = TweetDuck.Properties.Resources;
namespace TweetDuck.Browser {
@ -20,7 +21,9 @@ public enum Behavior { // keep order
public event EventHandler ClickClose;
public bool Visible {
get { return notifyIcon.Visible; }
get {
return notifyIcon.Visible;
}
set {
notifyIcon.Visible = value;
@ -30,7 +33,9 @@ public bool Visible {
}
public bool HasNotifications {
get { return hasNotifications; }
get {
return hasNotifications;
}
set {
if (hasNotifications != value) {
@ -74,7 +79,7 @@ protected override void Dispose(bool disposing) {
private void UpdateIcon() {
if (Visible) {
notifyIcon.Icon = hasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray;
notifyIcon.Icon = HasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray;
}
}

View File

@ -66,7 +66,7 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDe
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; }");
browserComponent.RunScript("gen:gdpr", "TD.storage.Account.prototype.requiresConsent = function() { return false; }");
}
};
}
@ -126,7 +126,7 @@ public void HideVideoOverlay(bool focus) {
browser.GetBrowser().GetHost().SendFocusEvent(true);
}
browserImpl.ScriptExecutor.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
browserComponent.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
}
}
}

View File

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

View File

@ -1,110 +0,0 @@
using System;
using System.Drawing;
using TweetDuck.Dialogs;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Utils.Serialization.Converters;
using TweetLib.Utils.Static;
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; }
public event EventHandler ProgramRestartRequested;
private readonly FileConfigInstance<UserConfig> infoUser;
private readonly FileConfigInstance<SystemConfig> infoSystem;
private readonly PluginConfigInstance<PluginConfig> infoPlugins;
private readonly IConfigInstance<BaseConfig>[] infoList;
public ConfigManager(UserConfig userConfig, Paths paths) {
FilePaths = paths;
User = userConfig;
System = new SystemConfig();
Plugins = new PluginConfig();
infoList = new IConfigInstance<BaseConfig>[] {
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
infoUser.Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
infoUser.Serializer.RegisterTypeConverter(typeof(Point), new BasicTypeConverter<Point> {
ConvertToString = value => $"{value.X} {value.Y}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Point(elements[0], elements[1]);
}
});
infoUser.Serializer.RegisterTypeConverter(typeof(Size), new BasicTypeConverter<Size> {
ConvertToString = value => $"{value.Width} {value.Height}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Size(elements[0], elements[1]);
}
});
}
public void LoadAll() {
infoUser.Load();
infoSystem.Load();
infoPlugins.Load();
}
public void SaveAll() {
infoUser.Save();
infoSystem.Save();
infoPlugins.Save();
}
public void ReloadAll() {
infoUser.Reload();
infoSystem.Reload();
infoPlugins.Reload();
}
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);
}
IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance) {
Type instanceType = instance.GetType();
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);
}
}
}

View File

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Configuration {
sealed class PluginConfig : BaseConfig, IPluginConfig {
private static readonly string[] DefaultDisabled = {
"official/clear-columns",
"official/reply-account"
};
// CONFIGURATION DATA
private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
// EVENTS
public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
// END OF CONFIG
protected override BaseConfig ConstructWithDefaults() {
return new PluginConfig();
}
// INTERFACE IMPLEMENTATION
IEnumerable<string> IPluginConfig.DisabledPlugins => disabled;
void IPluginConfig.Reset(IEnumerable<string> newDisabledPlugins) {
disabled.Clear();
disabled.UnionWith(newDisabledPlugins);
}
public void SetEnabled(Plugin plugin, bool enabled) {
if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))) {
PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
this.Save();
}
}
public bool IsEnabled(Plugin plugin) {
return !disabled.Contains(plugin.Identifier);
}
}
}

View File

@ -1,7 +1,8 @@
using TweetLib.Core.Systems.Configuration;
using TweetLib.Core;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Configuration {
sealed class SystemConfig : BaseConfig {
sealed class SystemConfig : BaseConfig<SystemConfig> {
private bool _hardwareAcceleration = true;
public bool ClearCacheAutomatically { get; set; } = true;
@ -11,12 +12,12 @@ sealed class SystemConfig : BaseConfig {
public bool HardwareAcceleration {
get => _hardwareAcceleration;
set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, App.ConfigManager.TriggerProgramRestartRequested);
}
// END OF CONFIG
protected override BaseConfig ConstructWithDefaults() {
public override SystemConfig ConstructWithDefaults() {
return new SystemConfig();
}
}

View File

@ -3,14 +3,15 @@
using System.Drawing;
using TweetDuck.Browser;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetLib.Core;
using TweetLib.Core.Application;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Utils.Data;
namespace TweetDuck.Configuration {
sealed class UserConfig : BaseConfig, IAppUserConfiguration {
sealed class UserConfig : BaseConfig<UserConfig>, IAppUserConfiguration {
public bool FirstRun { get; set; } = true;
[SuppressMessage("ReSharper", "UnusedMember.Global")]
@ -45,7 +46,7 @@ sealed class UserConfig : BaseConfig, IAppUserConfiguration {
private string _spellCheckLanguage = "en-US";
public string TranslationTarget { get; set; } = "en";
public int CalendarFirstDay { get; set; } = -1;
public int CalendarFirstDay { get; set; } = -1;
private TrayIcon.Behavior _trayBehavior = TrayIcon.Behavior.Disabled;
public bool EnableTrayHighlight { get; set; } = true;
@ -121,32 +122,32 @@ public TrayIcon.Behavior TrayBehavior {
public bool EnableSmoothScrolling {
get => _enableSmoothScrolling;
set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, App.ConfigManager.TriggerProgramRestartRequested);
}
public bool EnableTouchAdjustment {
get => _enableTouchAdjustment;
set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, App.ConfigManager.TriggerProgramRestartRequested);
}
public bool EnableColorProfileDetection {
get => _enableColorProfileDetection;
set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, App.ConfigManager.TriggerProgramRestartRequested);
}
public bool UseSystemProxyForAllConnections {
get => _useSystemProxyForAllConnections;
set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, App.ConfigManager.TriggerProgramRestartRequested);
}
public string CustomCefArgs {
get => _customCefArgs;
set => UpdatePropertyWithCallback(ref _customCefArgs, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _customCefArgs, value, App.ConfigManager.TriggerProgramRestartRequested);
}
public string SpellCheckLanguage {
get => _spellCheckLanguage;
set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, Program.Config.TriggerProgramRestartRequested);
set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, App.ConfigManager.TriggerProgramRestartRequested);
}
// EVENTS
@ -163,7 +164,7 @@ public void TriggerOptionsDialogClosed() {
// END OF CONFIG
protected override BaseConfig ConstructWithDefaults() {
public override UserConfig ConstructWithDefaults() {
return new UserConfig();
}
}

View File

@ -2,6 +2,7 @@
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TweetLib.Utils.Data;
namespace TweetDuck.Controls {
static class ControlExtensions {
@ -80,5 +81,23 @@ public static void EnableMultilineShortcuts(this TextBox textBox) {
}
};
}
public static void Save(this WindowState state, Form form) {
state.Bounds = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds;
state.IsMaximized = form.WindowState == FormWindowState.Maximized;
}
public static void Restore(this WindowState state, Form form, bool firstTimeFullscreen) {
if (state.Bounds != Rectangle.Empty) {
form.DesktopBounds = state.Bounds;
form.WindowState = state.IsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
}
if ((state.Bounds == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) {
form.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
form.WindowState = FormWindowState.Maximized;
state.Save(form);
}
}
}
}

View File

@ -9,7 +9,6 @@
namespace TweetDuck.Dialogs {
sealed partial class FormAbout : Form, FormManager.IAppDialog {
private const string TipsLink = "https://github.com/chylex/TweetDuck/wiki";
private const string IssuesLink = "https://github.com/chylex/TweetDuck/issues";
public FormAbout() {
InitializeComponent();
@ -20,7 +19,7 @@ public FormAbout() {
labelWebsite.Links.Add(new LinkLabel.Link(0, labelWebsite.Text.Length, Program.Website));
labelTips.Links.Add(new LinkLabel.Link(0, labelTips.Text.Length, TipsLink));
labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, IssuesLink));
labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, Lib.IssueTrackerUrl));
try {
pictureLogo.Image = Image.FromFile(Path.Combine(App.ResourcesPath, "images/logo.png"));

View File

@ -38,11 +38,7 @@ public static bool Question(string caption, string text, string buttonAccept, st
return Show(caption, text, MessageBoxIcon.Question, buttonAccept, buttonCancel);
}
public static bool Show(string caption, string text, MessageBoxIcon icon, string button) {
return Show(caption, text, icon, button, null);
}
public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel) {
public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel = null) {
using FormMessage message = new FormMessage(caption, text, icon);
if (buttonCancel == null) {

View File

@ -7,6 +7,8 @@
using TweetDuck.Plugins;
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Dialogs {
sealed partial class FormPlugins : Form, FormManager.IAppDialog {
@ -28,14 +30,18 @@ public FormPlugins(PluginManager pluginManager) : this() {
Size = new Size(Math.Max(MinimumSize.Width, targetSize.Width), Math.Max(MinimumSize.Height, targetSize.Height));
}
Shown += (sender, args) => { ReloadPluginList(); };
Shown += (sender, args) => {
ReloadPluginList();
};
FormClosed += (sender, args) => {
Config.PluginsWindowSize = Size;
Config.Save();
};
ResizeEnd += (sender, args) => { timerLayout.Start(); };
ResizeEnd += (sender, args) => {
timerLayout.Start();
};
}
private int GetPluginOrderIndex(Plugin plugin) {
@ -92,7 +98,7 @@ private void flowLayoutPlugins_Resize(object sender, EventArgs e) {
}
private void btnOpenFolder_Click(object sender, EventArgs e) {
App.SystemHandler.OpenFileExplorer(pluginManager.CustomPluginFolder);
App.SystemHandler.OpenFileExplorer(pluginManager.GetPluginFolder(PluginGroup.Custom));
}
private void btnReload_Click(object sender, EventArgs e) {

View File

@ -9,6 +9,7 @@
using TweetDuck.Dialogs.Settings;
using TweetDuck.Management;
using TweetDuck.Utils;
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.TweetDeck;
using TweetLib.Core.Systems.Updates;
@ -50,14 +51,14 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateChecker up
}
private void PrepareLoad() {
Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
}
private void PrepareUnload() { // TODO refactor this further later
currentTab.Control.OnClosing();
Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
Program.Config.SaveAll();
App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
App.ConfigManager.SaveAll();
}
private void Config_ProgramRestartRequested(object sender, EventArgs e) {

View File

@ -6,6 +6,7 @@
using TweetDuck.Management;
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Utils.Static;
namespace TweetDuck.Dialogs.Settings {
@ -133,7 +134,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
case State.Reset:
if (FormMessage.Warning("Reset TweetDuck Options", "This will reset the selected items. Are you sure you want to proceed?", FormMessage.Yes, FormMessage.No)) {
Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
if (SelectedItems.HasFlag(ProfileManager.Items.UserConfig)) {
Program.Config.User.Reset();
@ -143,7 +144,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
Program.Config.System.Reset();
}
Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
if (SelectedItems.HasFlag(ProfileManager.Items.PluginData)) {
Program.Config.Plugins.Reset();
@ -174,9 +175,9 @@ private void btnContinue_Click(object sender, EventArgs e) {
case State.Import:
if (importManager.Import(SelectedItems)) {
Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
Program.Config.ReloadAll();
Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
App.ConfigManager.ReloadAll();
App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
if (SelectedItemsForceRestart) {
RestartProgram(SelectedItems.HasFlag(ProfileManager.Items.Session) ? new string[] { Arguments.ArgImportCookies } : StringUtils.EmptyArray);

View File

@ -125,7 +125,9 @@ private void checkClearCacheAuto_CheckedChanged(object sender, EventArgs e) {
private void btnEditCefArgs_Click(object sender, EventArgs e) {
DialogSettingsCefArgs form = new DialogSettingsCefArgs(Config.CustomCefArgs);
form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); };
form.VisibleChanged += (sender2, args2) => {
form.MoveToCenter(ParentForm);
};
form.FormClosed += (sender2, args2) => {
RestoreParentForm();
@ -144,7 +146,9 @@ private void btnEditCefArgs_Click(object sender, EventArgs e) {
private void btnEditCSS_Click(object sender, EventArgs e) {
DialogSettingsCSS form = new DialogSettingsCSS(Config.CustomBrowserCSS, Config.CustomNotificationCSS, reinjectBrowserCSS, openDevTools);
form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); };
form.VisibleChanged += (sender2, args2) => {
form.MoveToCenter(ParentForm);
};
form.FormClosed += (sender2, args2) => {
RestoreParentForm();

View File

@ -14,7 +14,7 @@ public override void OnReady() {
#region Feedback
private void btnSendFeedback_Click(object sender, EventArgs e) {
App.SystemHandler.OpenBrowser("https://github.com/chylex/TweetDuck/issues/new");
App.SystemHandler.OpenBrowser(Lib.IssueTrackerUrl + "/new");
}
#endregion

View File

@ -1,42 +0,0 @@
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Controls;
using TweetLib.Utils.Serialization.Converters;
using TweetLib.Utils.Static;
namespace TweetDuck.Dialogs {
sealed class WindowState {
private Rectangle rect;
private bool isMaximized;
public void Save(Form form) {
rect = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds;
isMaximized = form.WindowState == FormWindowState.Maximized;
}
public void Restore(Form form, bool firstTimeFullscreen) {
if (rect != Rectangle.Empty) {
form.DesktopBounds = rect;
form.WindowState = isMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
}
if ((rect == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) {
form.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
form.WindowState = FormWindowState.Maximized;
Save(form);
}
}
public static readonly BasicTypeConverter<WindowState> Converter = new BasicTypeConverter<WindowState> {
ConvertToString = value => $"{(value.isMaximized ? 'M' : '_')}{value.rect.X} {value.rect.Y} {value.rect.Width} {value.rect.Height}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
return new WindowState {
rect = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
isMaximized = value[0] == 'M'
};
}
};
}
}

View File

@ -13,6 +13,8 @@ sealed class LockManager {
private const int CloseNaturallyTimeout = 10000;
private const int CloseKillTimeout = 5000;
public uint WindowRestoreMessage { get; } = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
private readonly LockFile lockFile;
public LockManager(string path) {
@ -33,7 +35,7 @@ private bool LaunchNormally() {
LockResult lockResult = lockFile.Lock();
if (lockResult is LockResult.HasProcess info) {
if (!RestoreProcess(info.Process) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)) {
if (!RestoreProcess(info.Process, WindowRestoreMessage) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)) {
if (!CloseProcess(info.Process)) {
FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
return false;
@ -83,9 +85,9 @@ private static void ShowGenericException(LockResult.Fail fail) {
App.ErrorHandler.HandleException("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", false, fail.Exception);
}
private static bool RestoreProcess(Process process) {
private static bool RestoreProcess(Process process, uint windowRestoreMessage) {
if (process.MainWindowHandle == IntPtr.Zero) { // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint) process.Id, 0);
NativeMethods.BroadcastMessage(windowRestoreMessage, (uint) process.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)) {
return true;

View File

@ -24,8 +24,7 @@ public enum Items {
UserConfig = 1,
SystemConfig = 2,
Session = 4,
PluginData = 8,
All = UserConfig | SystemConfig | Session | PluginData
PluginData = 8
}
private readonly string file;
@ -41,15 +40,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.Config.FilePaths.UserConfig);
stream.WriteFile("config", App.ConfigManager.UserPath);
}
if (items.HasFlag(Items.SystemConfig)) {
stream.WriteFile("system", Program.Config.FilePaths.SystemConfig);
stream.WriteFile("system", App.ConfigManager.SystemPath);
}
if (items.HasFlag(Items.PluginData)) {
stream.WriteFile("plugin.config", Program.Config.FilePaths.PluginConfig);
stream.WriteFile("plugin.config", App.ConfigManager.PluginsPath);
foreach (Plugin plugin in plugins.Plugins) {
foreach (PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))) {
@ -122,21 +121,21 @@ public bool Import(Items items) {
switch (entry.KeyName) {
case "config":
if (items.HasFlag(Items.UserConfig)) {
entry.WriteToFile(Program.Config.FilePaths.UserConfig);
entry.WriteToFile(App.ConfigManager.UserPath);
}
break;
case "system":
if (items.HasFlag(Items.SystemConfig)) {
entry.WriteToFile(Program.Config.FilePaths.SystemConfig);
entry.WriteToFile(App.ConfigManager.SystemPath);
}
break;
case "plugin.config":
if (items.HasFlag(Items.PluginData)) {
entry.WriteToFile(Program.Config.FilePaths.PluginConfig);
entry.WriteToFile(App.ConfigManager.PluginsPath);
}
break;

View File

@ -8,6 +8,7 @@
using TweetDuck.Dialogs;
using TweetLib.Communication.Pipe;
using TweetLib.Core;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Management {
sealed class VideoPlayer : IDisposable {

View File

@ -6,6 +6,7 @@
using TweetLib.Core;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Plugins {
sealed partial class PluginControl : UserControl {
@ -92,6 +93,7 @@ private void btnConfigure_Click(object sender, EventArgs e) {
private void btnToggleState_Click(object sender, EventArgs e) {
pluginManager.Config.SetEnabled(plugin, !pluginManager.Config.IsEnabled(plugin));
pluginManager.Config.Save();
UpdatePluginState();
}

View File

@ -8,19 +8,18 @@
using TweetDuck.Browser.Adapters;
using TweetDuck.Browser.Handling;
using TweetDuck.Configuration;
using TweetDuck.Dialogs;
using TweetDuck.Management;
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.Plugins.Config;
using TweetLib.Core.Features.TweetDeck;
using TweetLib.Core.Resources;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Utils.Collections;
using TweetLib.Utils.Static;
using Win = System.Windows.Forms;
namespace TweetDuck {
@ -30,22 +29,17 @@ static class Program {
public const string Website = "https://tweetduck.chylex.com";
private const string PluginDataFolder = "TD_Plugins";
private const string InstallerFolder = "TD_Updates";
private const string CefDataFolder = "TD_Chromium";
private const string ProgramLogFile = "TD_Log.txt";
private const string ConsoleLogFile = "TD_Console.txt";
public static string ExecutablePath => Win.Application.ExecutablePath;
public static uint WindowRestoreMessage;
private static LockManager lockManager;
private static Reporter errorReporter;
private static LockManager lockManager;
private static bool hasCleanedUp;
public static ConfigManager Config { get; private set; }
public static ConfigObjects<UserConfig, SystemConfig> Config { get; private set; }
internal static void SetupWinForms() {
Win.Application.EnableVisualStyles();
@ -59,112 +53,99 @@ private static void Main() {
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),
Config = new ConfigObjects<UserConfig, SystemConfig>(
new UserConfig(),
new SystemConfig(),
new PluginConfig(new string[] {
"official/clear-columns",
"official/reply-account"
})
);
Lib.AppLauncher launch = Lib.Initialize(new AppBuilder {
Setup = new Setup(),
ErrorHandler = reporter,
SystemHandler = new SystemHandler(),
DialogHandler = new DialogHandler(),
UserConfiguration = userConfig
MessageDialogs = new MessageDialogs(),
FileDialogs = new FileDialogs(),
});
LaunchApp(reporter, userConfig);
}
private static void LaunchApp(Reporter reporter, UserConfig userConfig) {
App.Launch();
errorReporter = reporter;
string storagePath = App.StoragePath;
launch();
}
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")
});
private sealed class Setup : IAppSetup {
public bool IsPortable => File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "makeportable"));
public bool IsDebugLogging => Arguments.HasFlag(Arguments.ArgLogging);
public string CustomDataFolder => Arguments.GetValue(Arguments.ArgDataFolder);
public string ResourceRewriteRules => Arguments.GetValue(Arguments.ArgFreeze);
lockManager = new LockManager(Path.Combine(storagePath, ".lock"));
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
if (!lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))) {
return;
public ConfigManager CreateConfigManager(string storagePath) {
return new ConfigManager<UserConfig, SystemConfig>(storagePath, Config);
}
Config.LoadAll();
if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
ProfileManager.ImportCookies();
}
else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
ProfileManager.DeleteCookies();
public bool TryLockDataFolder(string lockFile) {
lockManager = new LockManager(lockFile);
return lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart));
}
var installerFolderPath = Path.Combine(storagePath, InstallerFolder);
if (Arguments.HasFlag(Arguments.ArgUpdated)) {
WindowsUtils.TryDeleteFolderWhenAble(installerFolderPath, 8000);
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(storagePath, "Service Worker"), 4000);
BrowserCache.TryClearNow();
}
try {
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();
}
BrowserCache.RefreshTimer();
CefSharpSettings.WcfEnabled = false;
CefSettings settings = new CefSettings {
UserAgent = BrowserUtils.UserAgentChrome,
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 CachingResourceProvider<IResourceHandler>(new ResourceProvider());
var pluginManager = new PluginManager(Config.Plugins, Path.Combine(storagePath, PluginDataFolder));
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);
Cef.Initialize(settings, false, new BrowserProcessHandler());
Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser(resourceProvider, pluginManager, new UpdateCheckClient(installerFolderPath));
Win.Application.Run(mainForm);
if (mainForm.UpdateInstaller != null) {
ExitCleanup();
if (mainForm.UpdateInstaller.Launch()) {
Win.Application.Exit();
public void BeforeLaunch() {
if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
ProfileManager.ImportCookies();
}
else {
RestartWithArgsInternal(Arguments.GetCurrentClean());
else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
ProfileManager.DeleteCookies();
}
if (Arguments.HasFlag(Arguments.ArgUpdated)) {
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(App.StoragePath, InstallerFolder), 8000);
WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(App.StoragePath, "Service Worker"), 4000);
BrowserCache.TryClearNow();
}
}
public void Launch(ResourceCache resourceCache, PluginManager pluginManager) {
string storagePath = App.StoragePath;
BrowserCache.RefreshTimer();
CefSharpSettings.WcfEnabled = false;
CefSettings settings = new CefSettings {
UserAgent = BrowserUtils.UserAgentChrome,
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
};
CefSchemeHandlerFactory.Register(settings, new TweetDuckSchemeHandler(resourceCache));
CefSchemeHandlerFactory.Register(settings, new PluginSchemeHandler(resourceCache, pluginManager));
CefUtils.ParseCommandLineArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
Cef.Initialize(settings, false, new BrowserProcessHandler());
Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
var updateCheckClient = new UpdateCheckClient(Path.Combine(storagePath, InstallerFolder));
var mainForm = new FormBrowser(resourceCache, pluginManager, updateCheckClient, lockManager.WindowRestoreMessage);
Win.Application.Run(mainForm);
if (mainForm.UpdateInstaller != null) {
ExitCleanup();
if (mainForm.UpdateInstaller.Launch()) {
Win.Application.Exit();
}
else {
RestartWithArgsInternal(Arguments.GetCurrentClean());
}
}
}
}
@ -213,7 +194,7 @@ private static void ExitCleanup() {
return;
}
Config.SaveAll();
App.Close();
Cef.Shutdown();
BrowserCache.Exit();

View File

@ -48,7 +48,7 @@ public void HandleException(string caption, string message, bool canIgnore, Exce
};
btnOpenLog.Click += (sender, args) => {
if (!App.Logger.OpenLogFile()) {
if (!OpenLogFile()) {
FormMessage.Error("Error Log", "Cannot open error log.", FormMessage.OK);
}
};
@ -61,6 +61,16 @@ public void HandleException(string caption, string message, bool canIgnore, Exce
});
}
private static bool OpenLogFile() {
try {
using (Process.Start(App.Logger.LogFilePath)) {}
} catch (Exception) {
return false;
}
return true;
}
public sealed class ExpandedLogException : Exception {
private readonly string details;

View File

@ -62,14 +62,14 @@
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="Application\DialogHandler.cs" />
<Compile Include="Application\Logger.cs" />
<Compile Include="Application\FileDialogs.cs" />
<Compile Include="Application\MessageDialogs.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\CefSchemeResourceVisitor.cs" />
<Compile Include="Browser\Adapters\CefResourceRequestHandler.cs" />
<Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
<Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
@ -80,8 +80,6 @@
<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\Handling\ContextMenuBase.cs" />
@ -98,8 +96,6 @@
<Compile Include="Browser\Notification\SoundNotification.cs" />
<Compile Include="Browser\TweetDeckBrowser.cs" />
<Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\ConfigManager.cs" />
<Compile Include="Configuration\PluginConfig.cs" />
<Compile Include="Configuration\SystemConfig.cs" />
<Compile Include="Configuration\UserConfig.cs" />
<Compile Include="Controls\ControlExtensions.cs" />

View File

@ -10,7 +10,6 @@
namespace TweetDuck.Utils {
static class BrowserUtils {
public static string UserAgentVanilla => Program.BrandName + " " + System.Windows.Forms.Application.ProductVersion;
public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + Cef.ChromiumVersion + " Safari/537.36";
private static UserConfig Config => Program.Config.User;

View File

@ -3,15 +3,11 @@
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() {}

View File

@ -1,8 +1,9 @@
using System;
using TweetLib.Browser.Request;
namespace TweetLib.Browser.Interfaces {
public interface ICustomSchemeHandler<T> where T : class {
public interface ICustomSchemeHandler {
string Protocol { get; }
T? Resolve(Uri uri);
SchemeResource? Resolve(Uri uri);
}
}

View File

@ -0,0 +1,8 @@
using TweetLib.Browser.Request;
namespace TweetLib.Browser.Interfaces {
public interface ISchemeResourceVisitor<T> {
T Status(SchemeResource.Status status);
T File(SchemeResource.File file);
}
}

View File

@ -0,0 +1,38 @@
using System.Net;
using TweetLib.Browser.Interfaces;
namespace TweetLib.Browser.Request {
public abstract class SchemeResource {
private SchemeResource() {}
public abstract T Visit<T>(ISchemeResourceVisitor<T> visitor);
public sealed class Status : SchemeResource {
public HttpStatusCode Code { get; }
public string Message { get; }
public Status(HttpStatusCode code, string message) {
Code = code;
Message = message;
}
public override T Visit<T>(ISchemeResourceVisitor<T> visitor) {
return visitor.Status(this);
}
}
public sealed class File : SchemeResource {
public byte[] Contents { get; }
public string Extension { get; }
public File(byte[] contents, string extension) {
Contents = contents;
Extension = extension;
}
public override T Visit<T>(ISchemeResourceVisitor<T> visitor) {
return visitor.File(this);
}
}
}
}

View File

@ -1,29 +1,91 @@
using System;
using System.IO;
using System.Linq;
using TweetLib.Core.Application;
using TweetLib.Core.Features;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Resources;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Core.Systems.Logging;
using TweetLib.Utils.Static;
using Version = TweetDuck.Version;
namespace TweetLib.Core {
public static class App {
private static IAppSetup Setup { get; } = Validate(Builder.Setup, nameof(Builder.Setup));
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
public static readonly bool IsPortable = Setup.IsPortable;
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") : Validate(Builder.Startup, nameof(Builder.Startup)).GetDataFolder();
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
public static IAppLogger Logger { get; } = Validate(Builder.Logger, nameof(Builder.Logger));
public static IAppErrorHandler ErrorHandler { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
public static IAppSystemHandler SystemHandler { get; } = Validate(Builder.SystemHandler, nameof(Builder.SystemHandler));
public static IAppDialogHandler DialogHandler { get; } = Validate(Builder.DialogHandler, nameof(Builder.DialogHandler));
public static IAppUserConfiguration UserConfiguration { get; } = Validate(Builder.UserConfiguration, nameof(Builder.UserConfiguration));
public static Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
public static void Launch() {
public static IAppErrorHandler ErrorHandler { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
public static IAppSystemHandler SystemHandler { get; } = Validate(Builder.SystemHandler, nameof(Builder.SystemHandler));
public static IAppMessageDialogs MessageDialogs { get; } = Validate(Builder.MessageDialogs, nameof(Builder.MessageDialogs));
public static IAppFileDialogs? FileDialogs { get; } = Builder.FileDialogs;
internal static IAppUserConfiguration UserConfiguration => ConfigManager.User;
private static string GetDataFolder() {
string? custom = Setup.CustomDataFolder;
if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))) {
if (Path.GetInvalidPathChars().Any(custom.Contains)) {
throw new AppException("Data Folder Invalid", "The data folder contains invalid characters:\n" + custom);
}
else if (!Path.IsPathRooted(custom)) {
throw new AppException("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n" + custom);
}
return Environment.ExpandEnvironmentVariables(custom);
}
else {
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), custom ?? Lib.BrandName);
}
}
internal static void Launch() {
if (!FileUtils.CheckFolderWritePermission(StoragePath)) {
throw new AppException("Permission Error", "TweetDuck does not have write permissions to the storage folder: " + StoragePath);
}
if (!Setup.TryLockDataFolder(Path.Combine(StoragePath, ".lock"))) {
return;
}
ConfigManager.LoadAll();
Setup.BeforeLaunch();
var resourceRewriteRules = Setup.ResourceRewriteRules;
if (resourceRewriteRules != null) {
try {
BaseResourceRequestHandler.LoadResourceRewriteRules(resourceRewriteRules);
} catch (Exception e) {
throw new AppException("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message);
}
}
WebUtils.DefaultUserAgent = Lib.BrandName + " " + Version.Tag;
if (UserConfiguration.UseSystemProxyForAllConnections) {
WebUtils.EnableSystemProxy();
}
var resourceCache = new ResourceCache();
var pluginManager = new PluginManager(ConfigManager.Plugins, PluginPath, Path.Combine(StoragePath, "TD_Plugins"));
Setup.Launch(resourceCache, pluginManager);
}
public static void Close() {
ConfigManager.SaveAll();
}
// Setup
@ -46,19 +108,19 @@ private static T Validate<T>(T? obj, string name) where T : class {
}
public sealed class AppBuilder {
public AppStartup? Startup { get; set; }
public IAppLogger? Logger { get; set; }
public IAppSetup? Setup { get; set; }
public IAppErrorHandler? ErrorHandler { get; set; }
public IAppSystemHandler? SystemHandler { get; set; }
public IAppDialogHandler? DialogHandler { get; set; }
public IAppUserConfiguration? UserConfiguration { get; set; }
public IAppMessageDialogs? MessageDialogs { get; set; }
public IAppFileDialogs? FileDialogs { get; set; }
internal static AppBuilder? Instance { get; private set; }
internal void Build() {
internal Lib.AppLauncher Build() {
Instance = this;
App.Initialize();
Instance = null;
return App.Launch;
}
}
}

View File

@ -4,10 +4,6 @@ namespace TweetLib.Core.Application {
public sealed class AppException : Exception {
public string Title { get; }
internal AppException(string title) {
this.Title = title;
}
internal AppException(string title, string message) : base(message) {
this.Title = title;
}

View File

@ -1,27 +0,0 @@
using System;
using System.IO;
using System.Linq;
namespace TweetLib.Core.Application {
public sealed class AppStartup {
public string? CustomDataFolder { get; set; }
internal string GetDataFolder() {
string? custom = CustomDataFolder;
if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))) {
if (Path.GetInvalidPathChars().Any(custom.Contains)) {
throw new AppException("Data Folder Invalid", "The data folder contains invalid characters:\n" + custom);
}
else if (!Path.IsPathRooted(custom)) {
throw new AppException("Data Folder Invalid", "The data folder has to be either a simple folder name, or a full path:\n" + custom);
}
return Environment.ExpandEnvironmentVariables(custom);
}
else {
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), custom ?? Lib.BrandName);
}
}
}
}

View File

@ -1,11 +0,0 @@
using System;
using TweetLib.Core.Systems.Dialogs;
namespace TweetLib.Core.Application {
public interface IAppDialogHandler {
void Information(string caption, string text, string buttonAccept, string? buttonCancel = null);
void Error(string caption, string text, string buttonAccept, string? buttonCancel = null);
void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted);
}
}

View File

@ -0,0 +1,8 @@
using System;
using TweetLib.Core.Systems.Dialogs;
namespace TweetLib.Core.Application {
public interface IAppFileDialogs {
void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted);
}
}

View File

@ -1,8 +0,0 @@
namespace TweetLib.Core.Application {
public interface IAppLogger {
bool Debug(string message);
bool Info(string message);
bool Error(string message);
bool OpenLogFile();
}
}

View File

@ -0,0 +1,8 @@
using TweetLib.Core.Systems.Dialogs;
namespace TweetLib.Core.Application {
public interface IAppMessageDialogs {
void Information(string caption, string text, string buttonAccept = Dialogs.OK);
void Error(string caption, string text, string buttonAccept = Dialogs.OK);
}
}

View File

@ -0,0 +1,20 @@
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Resources;
using TweetLib.Core.Systems.Configuration;
namespace TweetLib.Core.Application {
public interface IAppSetup {
bool IsPortable { get; }
bool IsDebugLogging { get; }
string? CustomDataFolder { get; }
string? ResourceRewriteRules { get; }
ConfigManager CreateConfigManager(string storagePath);
bool TryLockDataFolder(string lockFile);
void BeforeLaunch();
void Launch(ResourceCache resourceCache, PluginManager pluginManager);
}
}

View File

@ -1,10 +1,19 @@
namespace TweetLib.Core.Application {
public interface IAppSystemHandler {
void OpenAssociatedProgram(string path);
void OpenBrowser(string? url);
void OpenFileExplorer(string path);
void CopyImageFromFile(string path);
void CopyText(string text);
void SearchText(string text);
OpenAssociatedProgramFunc? OpenAssociatedProgram { get; }
CopyImageFromFileFunc? CopyImageFromFile { get; }
CopyTextFunc? CopyText { get; }
SearchTextFunc? SearchText { get; }
public delegate void OpenAssociatedProgramFunc(string path);
public delegate void CopyImageFromFileFunc(string path);
public delegate void CopyTextFunc(string text);
public delegate void SearchTextFunc(string text);
}
}

View File

@ -3,8 +3,8 @@
namespace TweetLib.Core.Application {
public interface IAppUserConfiguration {
string CustomBrowserCSS { get; }
string CustomNotificationCSS { get; }
string? CustomBrowserCSS { get; }
string? CustomNotificationCSS { get; }
string? DismissedUpdate { get; }
bool ExpandLinksOnHover { get; }
bool FirstRun { get; }
@ -20,6 +20,7 @@ public interface IAppUserConfiguration {
int CalendarFirstDay { get; }
string TranslationTarget { get; }
ImageQuality TwitterImageQuality { get; }
bool UseSystemProxyForAllConnections { get; }
event EventHandler MuteToggled;
event EventHandler OptionsDialogClosed;

View File

@ -1,9 +1,7 @@
using System;
using System.Text.RegularExpressions;
using TweetLib.Browser.Contexts;
using TweetLib.Browser.Interfaces;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Systems.Dialogs;
using TweetLib.Utils.Static;
namespace TweetLib.Core.Features {
@ -13,14 +11,14 @@ internal class BaseContextMenu : IContextMenuHandler {
public BaseContextMenu(IBrowserComponent browser) {
this.browser = browser;
this.fileDownloadManager = new FileDownloadManager(browser);
this.fileDownloadManager = new FileDownloadManager(browser.FileDownloader);
}
public virtual void Show(IContextMenuBuilder menu, Context context) {
if (context.Selection is { Editable: false } selection) {
AddSearchSelectionItems(menu, selection.Text);
menu.AddSeparator();
menu.AddAction("Apply ROT13", () => App.DialogHandler.Information("ROT13", StringUtils.ConvertRot13(selection.Text), Dialogs.OK));
menu.AddAction("Apply ROT13", () => App.MessageDialogs.Information("ROT13", StringUtils.ConvertRot13(selection.Text)));
menu.AddSeparator();
}
@ -32,13 +30,13 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
Match match = TwitterUrls.RegexAccount.Match(link.CopyUrl);
if (match.Success) {
menu.AddAction(TextOpen("account"), OpenLink(link.Url));
menu.AddAction(TextCopy("account"), CopyText(link.CopyUrl));
menu.AddAction("Copy account username", CopyText(match.Groups[1].Value));
AddOpenAction(menu, TextOpen("account"), link.Url);
AddCopyAction(menu, TextCopy("account"), link.CopyUrl);
AddCopyAction(menu, "Copy account username", match.Groups[1].Value);
}
else {
menu.AddAction(TextOpen("link"), OpenLink(link.Url));
menu.AddAction(TextCopy("link"), CopyText(link.CopyUrl));
AddOpenAction(menu, TextOpen("link"), link.Url);
AddCopyAction(menu, TextCopy("link"), link.CopyUrl);
}
menu.AddSeparator();
@ -49,24 +47,37 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
switch (media.MediaType) {
case Media.Type.Image:
menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url));
menu.AddAction(TextOpen("image"), OpenLink(media.Url));
menu.AddAction(TextCopy("image"), CopyText(media.Url));
menu.AddAction("Copy image", () => fileDownloadManager.CopyImage(media.Url));
menu.AddAction(TextSave("image"), () => fileDownloadManager.SaveImages(new string[] { media.Url }, tweet?.MediaAuthor));
if (fileDownloadManager.SupportsViewingImage) {
menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url));
}
var imageUrls = tweet?.ImageUrls;
if (imageUrls?.Length > 1) {
menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor));
AddOpenAction(menu, TextOpen("image"), media.Url);
AddCopyAction(menu, TextCopy("image"), media.Url);
if (fileDownloadManager.SupportsCopyingImage) {
menu.AddAction("Copy image", () => fileDownloadManager.CopyImage(media.Url));
}
if (fileDownloadManager.SupportsFileSaving) {
menu.AddAction(TextSave("image"), () => fileDownloadManager.SaveImages(new string[] { media.Url }, tweet?.MediaAuthor));
var imageUrls = tweet?.ImageUrls;
if (imageUrls?.Length > 1) {
menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor));
}
}
menu.AddSeparator();
break;
case Media.Type.Video:
menu.AddAction(TextOpen("video"), OpenLink(media.Url));
menu.AddAction(TextCopy("video"), CopyText(media.Url));
menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor));
AddOpenAction(menu, TextOpen("video"), media.Url);
AddCopyAction(menu, TextCopy("video"), media.Url);
if (fileDownloadManager.SupportsFileSaving) {
menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor));
}
menu.AddSeparator();
break;
}
@ -74,22 +85,26 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
}
protected virtual void AddSearchSelectionItems(IContextMenuBuilder menu, string selectedText) {
menu.AddAction("Search in browser", () => {
App.SystemHandler.SearchText(selectedText);
DeselectAll();
});
if (App.SystemHandler.SearchText is {} searchText) {
menu.AddAction("Search in browser", () => {
searchText(selectedText);
DeselectAll();
});
}
}
protected void DeselectAll() {
browser.RunScript("gen:deselect", "window.getSelection().removeAllRanges()");
}
protected static Action OpenLink(string url) {
return () => App.SystemHandler.OpenBrowser(url);
protected static void AddOpenAction(IContextMenuBuilder menu, string title, string url) {
menu.AddAction(title, () => App.SystemHandler.OpenBrowser(url));
}
protected static Action CopyText(string text) {
return () => App.SystemHandler.CopyText(text);
protected static void AddCopyAction(IContextMenuBuilder menu, string title, string textToCopy) {
if (App.SystemHandler.CopyText is {} copyText) {
menu.AddAction(title, () => copyText(textToCopy));
}
}
}
}

View File

@ -7,7 +7,7 @@
using TweetLib.Utils.Static;
namespace TweetLib.Core.Features {
public class BaseResourceRequestHandler : IResourceRequestHandler {
internal class BaseResourceRequestHandler : IResourceRequestHandler {
private static readonly Regex TweetDeckResourceUrl = new (@"/dist/(.*?)\.(.*?)\.(css|js)$");
private static readonly SortedList<string, string> TweetDeckHashes = new (4);

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using TweetLib.Browser.Interfaces;
@ -7,16 +8,21 @@
using TweetLib.Utils.Static;
namespace TweetLib.Core.Features {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class FileDownloadManager {
private readonly IBrowserComponent browser;
public bool SupportsViewingImage => App.SystemHandler.OpenAssociatedProgram != null;
public bool SupportsCopyingImage => App.SystemHandler.CopyImageFromFile != null;
public bool SupportsFileSaving => App.FileDialogs != null;
internal FileDownloadManager(IBrowserComponent browser) {
this.browser = browser;
private readonly IFileDownloader fileDownloader;
internal FileDownloadManager(IFileDownloader fileDownloader) {
this.fileDownloader = fileDownloader;
}
private void DownloadTempImage(string url, Action<string> process) {
string? staticFileName = TwitterUrls.GetImageFileName(url);
string file = Path.Combine(browser.FileDownloader.CacheFolder, staticFileName ?? Path.GetRandomFileName());
string file = Path.Combine(fileDownloader.CacheFolder, staticFileName ?? Path.GetRandomFileName());
if (staticFileName != null && FileUtils.FileExistsAndNotEmpty(file)) {
process(file);
@ -27,14 +33,18 @@ void OnSuccess() {
}
static void OnFailure(Exception ex) {
App.DialogHandler.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, Dialogs.OK);
App.MessageDialogs.Error("Image Download", "An error occurred while downloading the image: " + ex.Message);
}
browser.FileDownloader.DownloadFile(url, file, OnSuccess, OnFailure);
fileDownloader.DownloadFile(url, file, OnSuccess, OnFailure);
}
}
public void ViewImage(string url) {
if (App.SystemHandler.OpenAssociatedProgram == null) {
return;
}
DownloadTempImage(url, static path => {
string ext = Path.GetExtension(path);
@ -42,16 +52,24 @@ public void ViewImage(string url) {
App.SystemHandler.OpenAssociatedProgram(path);
}
else {
App.DialogHandler.Error("Image Download", "Unknown image file extension: " + ext, Dialogs.OK);
App.MessageDialogs.Error("Image Download", "Unknown image file extension: " + ext);
}
});
}
public void CopyImage(string url) {
DownloadTempImage(url, App.SystemHandler.CopyImageFromFile);
if (App.SystemHandler.CopyImageFromFile is {} copyImageFromFile) {
DownloadTempImage(url, new Action<string>(copyImageFromFile));
}
}
public void SaveImages(string[] urls, string? author) {
var fileDialogs = App.FileDialogs;
if (fileDialogs == null) {
App.MessageDialogs.Error("Image Download", "Saving files is not supported!");
return;
}
if (urls.Length == 0) {
return;
}
@ -70,26 +88,32 @@ public void SaveImages(string[] urls, string? author) {
Filters = new [] { new FileDialogFilter(oneImage ? "Image" : "Images", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) }
};
App.DialogHandler.SaveFile(settings, path => {
fileDialogs.SaveFile(settings, path => {
static void OnFailure(Exception ex) {
App.DialogHandler.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, Dialogs.OK);
App.MessageDialogs.Error("Image Download", "An error occurred while downloading the image: " + ex.Message);
}
if (oneImage) {
browser.FileDownloader.DownloadFile(firstImageLink, path, null, OnFailure);
fileDownloader.DownloadFile(firstImageLink, path, null, OnFailure);
}
else {
string pathBase = Path.ChangeExtension(path, null);
string pathExt = Path.GetExtension(path);
for (int index = 0; index < urls.Length; index++) {
browser.FileDownloader.DownloadFile(urls[index], $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
fileDownloader.DownloadFile(urls[index], $"{pathBase} {index + 1}{pathExt}", null, OnFailure);
}
}
});
}
public void SaveVideo(string url, string? author) {
var fileDialogs = App.FileDialogs;
if (fileDialogs == null) {
App.MessageDialogs.Error("Video Download", "Saving files is not supported!");
return;
}
string? filename = TwitterUrls.GetFileNameFromUrl(url);
string? ext = Path.GetExtension(filename);
@ -100,12 +124,12 @@ public void SaveVideo(string url, string? author) {
Filters = new [] { new FileDialogFilter("Video", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) }
};
App.DialogHandler.SaveFile(settings, path => {
fileDialogs.SaveFile(settings, path => {
static void OnError(Exception ex) {
App.DialogHandler.Error("Video Download", "An error occurred while downloading the video: " + ex.Message, Dialogs.OK);
App.MessageDialogs.Error("Video Download", "An error occurred while downloading the video: " + ex.Message);
}
browser.FileDownloader.DownloadFile(url, path, null, OnError);
fileDownloader.DownloadFile(url, path, null, OnError);
});
}
}

View File

@ -17,8 +17,8 @@ public class Tweet : NotificationBrowser {
private readonly PluginManager pluginManager;
protected Tweet(IBrowserComponent browserComponent, INotificationInterface notificationInterface, ICommonInterface commonInterface, PluginManager pluginManager, Func<NotificationBrowser, BrowserSetup> setup) : base(browserComponent, setup) {
this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface));
this.browserComponent.PageLoadEnd += BrowserComponentOnPageLoadEnd;
this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface));
this.notificationInterface = notificationInterface;
this.pluginManager = pluginManager;
@ -56,10 +56,10 @@ public override void Show(IContextMenuBuilder menu, Context context) {
menu.AddSeparator();
if (context.Notification is {} notification) {
menu.AddAction("Copy tweet address", CopyText(notification.TweetUrl));
AddCopyAction(menu, "Copy tweet address", notification.TweetUrl);
if (!string.IsNullOrEmpty(notification.QuoteUrl)) {
menu.AddAction("Copy quoted tweet address", CopyText(notification.QuoteUrl!));
AddCopyAction(menu, "Copy quoted tweet address", notification.QuoteUrl!);
}
menu.AddSeparator();

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using TweetLib.Core.Features.Plugins.Events;
namespace TweetLib.Core.Features.Plugins.Config {
public interface IPluginConfig {
event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
IEnumerable<string> DisabledPlugins { get; }
void Reset(IEnumerable<string> newDisabledPlugins);
void SetEnabled(Plugin plugin, bool enabled);
bool IsEnabled(Plugin plugin);
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Systems.Configuration;
namespace TweetLib.Core.Features.Plugins.Config {
public sealed class PluginConfig : IConfigObject<PluginConfig> {
internal IEnumerable<string> DisabledPlugins => disabled;
public event EventHandler<PluginChangedStateEventArgs>? PluginChangedState;
private readonly HashSet<string> defaultDisabled;
private readonly HashSet<string> disabled;
public PluginConfig(IEnumerable<string> defaultDisabled) {
this.defaultDisabled = new HashSet<string>(defaultDisabled);
this.disabled = new HashSet<string>(this.defaultDisabled);
}
public PluginConfig ConstructWithDefaults() {
return new PluginConfig(defaultDisabled);
}
internal void Reset(IEnumerable<string> newDisabledPlugins) {
disabled.Clear();
disabled.UnionWith(newDisabledPlugins);
}
internal void ResetToDefault() {
Reset(defaultDisabled);
}
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));
}
}
public bool IsEnabled(Plugin plugin) {
return !disabled.Contains(plugin.Identifier);
}
}
}

View File

@ -5,14 +5,14 @@
using TweetLib.Core.Systems.Configuration;
namespace TweetLib.Core.Features.Plugins.Config {
public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig {
public T Instance { get; }
internal sealed class PluginConfigInstance : IConfigInstance {
public PluginConfig Instance { get; }
private readonly string filename;
public PluginConfigInstance(string filename, T instance) {
this.filename = filename;
public PluginConfigInstance(string filename, PluginConfig instance) {
this.Instance = instance;
this.filename = filename;
}
public void Load() {
@ -58,7 +58,7 @@ public void Reload() {
public void Reset() {
try {
File.Delete(filename);
Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins);
Instance.ResetToDefault();
} catch (Exception e) {
OnException("Could not delete the plugin configuration file.", e);
return;

View File

@ -11,12 +11,11 @@
namespace TweetLib.Core.Features.Plugins {
public sealed class PluginManager {
public string CustomPluginFolder => Path.Combine(App.PluginPath, PluginGroup.Custom.GetSubFolder());
public IEnumerable<Plugin> Plugins => plugins;
public IEnumerable<InjectedString> NotificationInjections => bridge.NotificationInjections;
public IPluginConfig Config { get; }
public PluginConfig Config { get; }
public string PluginFolder { get; }
public string PluginDataFolder { get; }
public event EventHandler<PluginErrorEventArgs>? Reloaded;
@ -27,13 +26,18 @@ public sealed class PluginManager {
private readonly HashSet<Plugin> plugins = new ();
public PluginManager(IPluginConfig config, string pluginDataFolder) {
public PluginManager(PluginConfig config, string pluginFolder, string pluginDataFolder) {
this.Config = config;
this.Config.PluginChangedState += Config_PluginChangedState;
this.PluginFolder = pluginFolder;
this.PluginDataFolder = pluginDataFolder;
this.bridge = new PluginBridge(this);
}
public string GetPluginFolder(PluginGroup group) {
return Path.Combine(PluginFolder, group.GetSubFolder());
}
internal void Register(PluginEnvironment environment, IBrowserComponent browserComponent) {
browserComponent.AttachBridgeObject("$TDP", bridge);
@ -47,7 +51,7 @@ public void Reload() {
var errors = new List<string>(1);
foreach (var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(App.PluginPath, PluginDataFolder, group))) {
foreach (var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(PluginFolder, PluginDataFolder, group))) {
if (result.HasValue) {
plugins.Add(result.Value);
}
@ -111,7 +115,7 @@ public void ConfigurePlugin(Plugin plugin) {
browserExecutor.RunFunction("TDPF_configurePlugin", plugin);
}
else if (plugin.HasConfig) {
App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(PluginFolder.Data));
App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(Enums.PluginFolder.Data));
}
}
}

View File

@ -2,22 +2,25 @@
using System.Linq;
using System.Net;
using TweetLib.Browser.Interfaces;
using TweetLib.Browser.Request;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Resources;
namespace TweetLib.Core.Features.Plugins {
public sealed class PluginSchemeHandler<T> : ICustomSchemeHandler<T> where T : class {
public sealed class PluginSchemeHandler : ICustomSchemeHandler {
private static readonly SchemeResource PathMustBeRelativeToRoot = new SchemeResource.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.");
public string Protocol => "tdp";
private readonly CachingResourceProvider<T> resourceProvider;
private readonly ResourceCache resourceCache;
private readonly PluginBridge bridge;
public PluginSchemeHandler(CachingResourceProvider<T> resourceProvider, PluginManager pluginManager) {
this.resourceProvider = resourceProvider;
public PluginSchemeHandler(ResourceCache resourceCache, PluginManager pluginManager) {
this.resourceCache = resourceCache;
this.bridge = pluginManager.bridge;
}
public T? Resolve(Uri uri) {
public SchemeResource? Resolve(Uri uri) {
if (!uri.IsAbsoluteUri || uri.Scheme != Protocol || !int.TryParse(uri.Authority, out var identifier)) {
return null;
}
@ -35,15 +38,15 @@ public PluginSchemeHandler(CachingResourceProvider<T> resourceProvider, PluginMa
}
}
return resourceProvider.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath);
return new SchemeResource.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath);
}
private T DoReadRootFile(int identifier, string[] segments) {
private SchemeResource DoReadRootFile(int identifier, string[] segments) {
string path = string.Join("/", segments, 1, segments.Length - 1);
Plugin? plugin = bridge.GetPluginFromToken(identifier);
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(PluginFolder.Root, path);
return fullPath.Length == 0 ? resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.") : resourceProvider.CachedFile(fullPath);
return fullPath.Length == 0 ? PathMustBeRelativeToRoot : resourceCache.ReadFile(fullPath);
}
}
}

View File

@ -4,7 +4,7 @@
namespace TweetLib.Core.Features.Plugins {
internal static class PluginScriptGenerator {
public static string GenerateConfig(IPluginConfig config) {
public static string GenerateConfig(PluginConfig config) {
return "window.TD_PLUGINS_DISABLE = [" + string.Join(",", config.DisabledPlugins.Select(static id => '"' + id + '"')) + "]";
}

View File

@ -10,7 +10,6 @@
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Resources;
using TweetLib.Core.Systems.Dialogs;
using TweetLib.Core.Systems.Updates;
using TweetLib.Utils.Static;
using Version = TweetDuck.Version;
@ -21,22 +20,18 @@ public sealed class TweetDeckBrowser : BaseBrowser<TweetDeckBrowser> {
private const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
public TweetDeckFunctions Functions { get; }
public FileDownloadManager FileDownloadManager => new (browserComponent);
public FileDownloadManager FileDownloadManager => new (browserComponent.FileDownloader);
private readonly ISoundNotificationHandler soundNotificationHandler;
private readonly PluginManager pluginManager;
private readonly UpdateChecker updateChecker;
private bool isBrowserReady;
private bool ignoreUpdateCheckError;
private string? prevSoundNotificationPath = null;
public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface tweetDeckInterface, TweetDeckExtraContext extraContext, ISoundNotificationHandler soundNotificationHandler, PluginManager pluginManager, UpdateChecker updateChecker) : base(browserComponent, CreateSetupObject) {
public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface tweetDeckInterface, TweetDeckExtraContext extraContext, ISoundNotificationHandler soundNotificationHandler, PluginManager pluginManager, UpdateChecker? updateChecker = null) : base(browserComponent, CreateSetupObject) {
this.Functions = new TweetDeckFunctions(this.browserComponent);
this.browserComponent.AttachBridgeObject("$TD", new TweetDeckBridgeObject(tweetDeckInterface, this, extraContext));
this.browserComponent.AttachBridgeObject("$TDU", updateChecker.InteractionManager.BridgeObject);
this.soundNotificationHandler = soundNotificationHandler;
this.pluginManager = pluginManager;
@ -45,13 +40,17 @@ public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface
this.pluginManager.Executed += pluginManager_Executed;
this.pluginManager.Reload();
this.updateChecker = updateChecker;
this.updateChecker.CheckFinished += updateChecker_CheckFinished;
this.browserComponent.BrowserLoaded += browserComponent_BrowserLoaded;
this.browserComponent.PageLoadStart += browserComponent_PageLoadStart;
this.browserComponent.PageLoadEnd += browserComponent_PageLoadEnd;
this.browserComponent.AttachBridgeObject("$TD", new TweetDeckBridgeObject(tweetDeckInterface, this, extraContext));
if (updateChecker != null) {
updateChecker.CheckFinished += updateChecker_CheckFinished;
this.browserComponent.AttachBridgeObject("$TDU", updateChecker.InteractionManager.BridgeObject);
}
App.UserConfiguration.MuteToggled += UserConfiguration_GeneralEventHandler;
App.UserConfiguration.OptionsDialogClosed += UserConfiguration_GeneralEventHandler;
App.UserConfiguration.SoundNotificationChanged += UserConfiguration_SoundNotificationChanged;
@ -105,7 +104,7 @@ private void browserComponent_PageLoadEnd(object sender, PageLoadEventArgs e) {
private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
if (e.HasErrors) {
App.DialogHandler.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), Dialogs.OK);
App.MessageDialogs.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors));
}
if (isBrowserReady) {
@ -115,11 +114,13 @@ private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
private void pluginManager_Executed(object sender, PluginErrorEventArgs e) {
if (e.HasErrors) {
App.DialogHandler.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), Dialogs.OK);
App.MessageDialogs.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors));
}
}
private void updateChecker_CheckFinished(object sender, UpdateCheckEventArgs e) {
var updateChecker = (UpdateChecker) sender;
e.Result.Handle(update => {
string tag = update.VersionTag;
@ -201,14 +202,14 @@ public override void Show(IContextMenuBuilder menu, Context context) {
base.Show(menu, context);
if (context.Selection == null && context.Tweet is {} tweet) {
menu.AddAction("Open tweet in browser", OpenLink(tweet.Url));
menu.AddAction("Copy tweet address", CopyText(tweet.Url));
AddOpenAction(menu, "Open tweet in browser", tweet.Url);
AddCopyAction(menu, "Copy tweet address", tweet.Url);
menu.AddAction("Screenshot tweet to clipboard", () => owner.Functions.TriggerTweetScreenshot(tweet.ColumnId, tweet.ChirpId));
menu.AddSeparator();
if (!string.IsNullOrEmpty(tweet.QuoteUrl)) {
menu.AddAction("Open quoted tweet in browser", OpenLink(tweet.QuoteUrl!));
menu.AddAction("Copy quoted tweet address", CopyText(tweet.QuoteUrl!));
AddOpenAction(menu, "Open quoted tweet in browser", tweet.QuoteUrl!);
AddCopyAction(menu, "Copy quoted tweet address", tweet.QuoteUrl!);
menu.AddSeparator();
}
}

View File

@ -1,20 +1,24 @@
using System;
using System.Net;
using TweetLib.Browser.Interfaces;
using TweetLib.Browser.Request;
using TweetLib.Core.Resources;
using TweetLib.Utils.Static;
namespace TweetLib.Core.Features.TweetDeck {
public sealed class TweetDuckSchemeHandler<T> : ICustomSchemeHandler<T> where T : class {
public sealed class TweetDuckSchemeHandler : ICustomSchemeHandler {
private static readonly SchemeResource InvalidUrl = new SchemeResource.Status(HttpStatusCode.NotFound, "Invalid URL.");
private static readonly SchemeResource PathMustBeRelativeToRoot = new SchemeResource.Status(HttpStatusCode.Forbidden, "File path has to be relative to the root folder.");
public string Protocol => "td";
private readonly CachingResourceProvider<T> resourceProvider;
private readonly ResourceCache resourceCache;
public TweetDuckSchemeHandler(CachingResourceProvider<T> resourceProvider) {
this.resourceProvider = resourceProvider;
public TweetDuckSchemeHandler(ResourceCache resourceCache) {
this.resourceCache = resourceCache;
}
public T Resolve(Uri uri) {
public SchemeResource Resolve(Uri uri) {
string? rootPath = uri.Authority switch {
"resources" => App.ResourcesPath,
"guide" => App.GuidePath,
@ -22,11 +26,11 @@ public T Resolve(Uri uri) {
};
if (rootPath == null) {
return resourceProvider.Status(HttpStatusCode.NotFound, "Invalid URL.");
return InvalidUrl;
}
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.CachedFile(filePath);
return filePath.Length == 0 ? PathMustBeRelativeToRoot : resourceCache.ReadFile(filePath);
}
}
}

View File

@ -9,10 +9,13 @@
namespace TweetLib.Core {
public static class Lib {
public const string BrandName = "TweetDuck";
public const string IssueTrackerUrl = "https://github.com/chylex/TweetDuck/issues";
public static CultureInfo Culture { get; } = CultureInfo.CurrentCulture;
public static void Initialize(AppBuilder app) {
public delegate void AppLauncher();
public static AppLauncher Initialize(AppBuilder app) {
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
@ -20,7 +23,7 @@ public static void Initialize(AppBuilder app) {
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
#endif
app.Build();
return app.Build();
}
}
}

View File

@ -1,83 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using TweetLib.Browser.Interfaces;
using IOFile = System.IO.File;
namespace TweetLib.Core.Resources {
public sealed class CachingResourceProvider<T> : IResourceProvider<T> {
private readonly IResourceProvider<T> resourceProvider;
private readonly Dictionary<string, ICachedResource> cache = new ();
public CachingResourceProvider(IResourceProvider<T> resourceProvider) {
this.resourceProvider = resourceProvider;
}
public void ClearCache() {
cache.Clear();
}
public T Status(HttpStatusCode code, string message) {
return resourceProvider.Status(code, message);
}
public T File(byte[] contents, string extension) {
return resourceProvider.File(contents, extension);
}
internal T CachedFile(string path) {
string key = new Uri(path).LocalPath;
if (cache.TryGetValue(key, out var cachedResource)) {
return cachedResource.GetResource(resourceProvider);
}
ICachedResource resource;
try {
resource = new CachedFileResource(IOFile.ReadAllBytes(path), Path.GetExtension(path));
} catch (FileNotFoundException) {
resource = new CachedStatusResource(HttpStatusCode.NotFound, "File not found.");
} catch (DirectoryNotFoundException) {
resource = new CachedStatusResource(HttpStatusCode.NotFound, "Directory not found.");
} catch (Exception e) {
resource = new CachedStatusResource(HttpStatusCode.InternalServerError, e.Message);
}
cache[key] = resource;
return resource.GetResource(resourceProvider);
}
private interface ICachedResource {
T GetResource(IResourceProvider<T> resourceProvider);
}
private sealed class CachedFileResource : ICachedResource {
private readonly byte[] contents;
private readonly string extension;
public CachedFileResource(byte[] contents, string extension) {
this.contents = contents;
this.extension = extension;
}
T ICachedResource.GetResource(IResourceProvider<T> resourceProvider) {
return resourceProvider.File(contents, extension);
}
}
private sealed class CachedStatusResource : ICachedResource {
private readonly HttpStatusCode code;
private readonly string message;
public CachedStatusResource(HttpStatusCode code, string message) {
this.code = code;
this.message = message;
}
public T GetResource(IResourceProvider<T> resourceProvider) {
return resourceProvider.Status(code, message);
}
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using TweetLib.Browser.Request;
using IOFile = System.IO.File;
namespace TweetLib.Core.Resources {
public sealed class ResourceCache {
private readonly Dictionary<string, SchemeResource> cache = new ();
public void ClearCache() {
cache.Clear();
}
internal SchemeResource ReadFile(string path) {
string key = new Uri(path).LocalPath;
if (cache.TryGetValue(key, out var cachedResource)) {
return cachedResource;
}
SchemeResource resource;
try {
resource = new SchemeResource.File(IOFile.ReadAllBytes(path), Path.GetExtension(path));
} catch (FileNotFoundException) {
resource = new SchemeResource.Status(HttpStatusCode.NotFound, "File not found.");
} catch (DirectoryNotFoundException) {
resource = new SchemeResource.Status(HttpStatusCode.NotFound, "Directory not found.");
} catch (Exception e) {
resource = new SchemeResource.Status(HttpStatusCode.InternalServerError, e.Message);
}
cache[key] = resource;
return resource;
}
}
}

View File

@ -2,22 +2,18 @@
using System.Collections.Generic;
namespace TweetLib.Core.Systems.Configuration {
public abstract class BaseConfig {
internal T ConstructWithDefaults<T>() where T : BaseConfig {
return (T) ConstructWithDefaults();
}
public abstract class BaseConfig<T> : IConfigObject<T> where T : BaseConfig<T> {
public abstract T ConstructWithDefaults();
protected abstract BaseConfig ConstructWithDefaults();
protected void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler? eventHandler) {
if (!EqualityComparer<T>.Default.Equals(field, value)) {
protected void UpdatePropertyWithEvent<V>(ref V field, V value, EventHandler? eventHandler) {
if (!EqualityComparer<V>.Default.Equals(field, value)) {
field = value;
eventHandler?.Invoke(this, EventArgs.Empty);
}
}
protected void UpdatePropertyWithCallback<T>(ref T field, T value, Action action) {
if (!EqualityComparer<T>.Default.Equals(field, value)) {
protected void UpdatePropertyWithCallback<V>(ref V field, V value, Action action) {
if (!EqualityComparer<V>.Default.Equals(field, value)) {
field = value;
action();
}

View File

@ -0,0 +1,142 @@
using System;
using System.Drawing;
using System.IO;
using TweetLib.Core.Application;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Utils.Data;
using TweetLib.Utils.Serialization;
using TweetLib.Utils.Serialization.Converters;
using TweetLib.Utils.Static;
namespace TweetLib.Core.Systems.Configuration {
public abstract class ConfigManager {
protected static TypeConverterRegistry ConverterRegistry { get; } = new ();
static ConfigManager() {
ConverterRegistry.Register(typeof(WindowState), new BasicTypeConverter<WindowState> {
ConvertToString = static value => $"{(value.IsMaximized ? 'M' : '_')}{value.Bounds.X} {value.Bounds.Y} {value.Bounds.Width} {value.Bounds.Height}",
ConvertToObject = static value => {
int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
return new WindowState {
Bounds = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
IsMaximized = value[0] == 'M'
};
}
});
ConverterRegistry.Register(typeof(Point), new BasicTypeConverter<Point> {
ConvertToString = static value => $"{value.X} {value.Y}",
ConvertToObject = static value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Point(elements[0], elements[1]);
}
});
ConverterRegistry.Register(typeof(Size), new BasicTypeConverter<Size> {
ConvertToString = static value => $"{value.Width} {value.Height}",
ConvertToObject = static value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Size(elements[0], elements[1]);
}
});
}
public string UserPath { get; }
public string SystemPath { get; }
public string PluginsPath { get; }
public event EventHandler? ProgramRestartRequested;
internal IAppUserConfiguration User { get; }
internal PluginConfig Plugins { get; }
protected ConfigManager(string storagePath, IAppUserConfiguration user, PluginConfig plugins) {
UserPath = Path.Combine(storagePath, "TD_UserConfig.cfg");
SystemPath = Path.Combine(storagePath, "TD_SystemConfig.cfg");
PluginsPath = Path.Combine(storagePath, "TD_PluginConfig.cfg");
User = user;
Plugins = plugins;
}
public abstract void LoadAll();
public abstract void SaveAll();
public abstract void ReloadAll();
internal void Save(IConfigObject instance) {
this.GetInstanceInfo(instance).Save();
}
internal void Reset(IConfigObject instance) {
this.GetInstanceInfo(instance).Reset();
}
public void TriggerProgramRestartRequested() {
ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
}
protected abstract IConfigInstance GetInstanceInfo(IConfigObject instance);
}
public sealed class ConfigManager<TUser, TSystem> : ConfigManager where TUser : class, IAppUserConfiguration, IConfigObject<TUser> where TSystem : class, IConfigObject<TSystem> {
private new TUser User { get; }
private TSystem System { get; }
private readonly FileConfigInstance<TUser> infoUser;
private readonly FileConfigInstance<TSystem> infoSystem;
private readonly PluginConfigInstance infoPlugins;
public ConfigManager(string storagePath, ConfigObjects<TUser, TSystem> configObjects) : base(storagePath, configObjects.User, configObjects.Plugins) {
User = configObjects.User;
System = configObjects.System;
infoUser = new FileConfigInstance<TUser>(UserPath, User, "program options", ConverterRegistry);
infoSystem = new FileConfigInstance<TSystem>(SystemPath, System, "system options", ConverterRegistry);
infoPlugins = new PluginConfigInstance(PluginsPath, Plugins);
}
public override void LoadAll() {
infoUser.Load();
infoSystem.Load();
infoPlugins.Load();
}
public override void SaveAll() {
infoUser.Save();
infoSystem.Save();
infoPlugins.Save();
}
public override void ReloadAll() {
infoUser.Reload();
infoSystem.Reload();
infoPlugins.Reload();
}
protected override IConfigInstance GetInstanceInfo(IConfigObject instance) {
if (instance == User) {
return infoUser;
}
else if (instance == System) {
return infoSystem;
}
else if (instance == Plugins) {
return infoPlugins;
}
else {
throw new ArgumentException("Invalid configuration instance: " + instance.GetType());
}
}
}
public static class ConfigManagerExtensions {
public static void Save(this IConfigObject instance) {
App.ConfigManager.Save(instance);
}
public static void Reset(this IConfigObject instance) {
App.ConfigManager.Reset(instance);
}
}
}

View File

@ -0,0 +1,16 @@
using TweetLib.Core.Application;
using TweetLib.Core.Features.Plugins.Config;
namespace TweetLib.Core.Systems.Configuration {
public sealed class ConfigObjects<TUser, TSystem> where TUser : IAppUserConfiguration where TSystem : IConfigObject {
public TUser User { get; }
public TSystem System { get; }
public PluginConfig Plugins { get; }
public ConfigObjects(TUser user, TSystem system, PluginConfig plugins) {
User = user;
System = system;
Plugins = plugins;
}
}
}

View File

@ -3,25 +3,26 @@
using TweetLib.Utils.Serialization;
namespace TweetLib.Core.Systems.Configuration {
public sealed class FileConfigInstance<T> : IConfigInstance<T> where T : BaseConfig {
internal sealed class FileConfigInstance<T> : IConfigInstance where T : IConfigObject<T> {
public T Instance { get; }
public SimpleObjectSerializer<T> Serializer { get; }
private readonly SimpleObjectSerializer<T> serializer;
private readonly string filenameMain;
private readonly string filenameBackup;
private readonly string identifier;
public FileConfigInstance(string filename, T instance, string identifier) {
public FileConfigInstance(string filename, T instance, string identifier, TypeConverterRegistry converterRegistry) {
this.Instance = instance;
this.serializer = new SimpleObjectSerializer<T>(converterRegistry);
this.filenameMain = filename ?? throw new ArgumentNullException(nameof(filename), "Config file name must not be null!");
this.filenameBackup = filename + ".bak";
this.identifier = identifier;
this.Instance = instance;
this.Serializer = new SimpleObjectSerializer<T>();
}
private void LoadInternal(bool backup) {
Serializer.Read(backup ? filenameBackup : filenameMain, Instance);
serializer.Read(backup ? filenameBackup : filenameMain, Instance);
}
public void Load() {
@ -63,7 +64,7 @@ public void Save() {
File.Move(filenameMain, filenameBackup);
}
Serializer.Write(filenameMain, Instance);
serializer.Write(filenameMain, Instance);
} catch (SerializationSoftException e) {
OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e);
} catch (Exception e) {
@ -76,7 +77,7 @@ public void Reload() {
LoadInternal(false);
} catch (FileNotFoundException) {
try {
Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>());
serializer.Write(filenameMain, Instance.ConstructWithDefaults());
LoadInternal(false);
} catch (Exception e) {
OnException($"Could not regenerate the configuration file for {identifier}.", e);

View File

@ -1,9 +1,6 @@
namespace TweetLib.Core.Systems.Configuration {
public interface IConfigInstance<out T> {
T Instance { get; }
public interface IConfigInstance {
void Save();
void Reload();
void Reset();
}
}

View File

@ -1,5 +0,0 @@
namespace TweetLib.Core.Systems.Configuration {
public interface IConfigManager {
IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
}
}

View File

@ -0,0 +1,7 @@
namespace TweetLib.Core.Systems.Configuration {
public interface IConfigObject {}
public interface IConfigObject<T> : IConfigObject where T : IConfigObject<T> {
T ConstructWithDefaults();
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.IO;
using System.Text;
namespace TweetLib.Core.Systems.Logging {
public sealed class Logger {
public string LogFilePath { get; }
private readonly bool debug;
internal Logger(string logPath, bool debug) {
this.LogFilePath = logPath;
this.debug = debug;
#if DEBUG
this.debug = true;
#endif
}
public bool Debug(string message) {
return debug && Log("DEBUG", message);
}
public bool Info(string message) {
return Log("INFO", message);
}
public bool Warn(string message) {
return Log("WARN", message);
}
public bool Error(string message) {
return Log("ERROR", message);
}
private bool Log(string level, string message) {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[" + level + "] " + message);
#endif
StringBuilder build = new StringBuilder();
if (!File.Exists(LogFilePath)) {
build.Append("Please, report all issues to: ").Append(Lib.IssueTrackerUrl).Append("\r\n\r\n");
}
build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("] ").Append(level).Append("\r\n");
build.Append(message).Append("\r\n\r\n");
try {
File.AppendAllText(LogFilePath, build.ToString(), Encoding.UTF8);
return true;
} catch {
return false;
}
}
}
}

View File

@ -2,7 +2,6 @@
using System.IO;
using System.Net;
using TweetLib.Utils.Static;
using Version = TweetDuck.Version;
namespace TweetLib.Core.Systems.Updates {
public sealed class UpdateInfo {
@ -49,7 +48,7 @@ internal void BeginSilentDownload() {
return;
}
WebClient client = WebUtils.NewClient($"{Lib.BrandName} {Version.Tag}");
WebClient client = WebUtils.NewClient();
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
DownloadStatus = UpdateDownloadStatus.Done;

View File

@ -0,0 +1,8 @@
using System.Drawing;
namespace TweetLib.Utils.Data {
public sealed class WindowState {
public Rectangle Bounds { get; set; }
public bool IsMaximized { get; set; }
}
}

View File

@ -50,16 +50,12 @@ private static string UnescapeStream(StreamReader reader) {
return build.Append(data.Substring(index)).ToString();
}
private readonly TypeConverterRegistry converterRegistry;
private readonly Dictionary<string, PropertyInfo> props;
private readonly Dictionary<Type, ITypeConverter> converters;
public SimpleObjectSerializer() {
public SimpleObjectSerializer(TypeConverterRegistry converterRegistry) {
this.converterRegistry = converterRegistry;
this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(static prop => prop.CanWrite).ToDictionary(static prop => prop.Name);
this.converters = new Dictionary<Type, ITypeConverter>();
}
public void RegisterTypeConverter(Type type, ITypeConverter converter) {
converters[type] = converter;
}
public void Write(string file, T obj) {
@ -69,14 +65,10 @@ public void Write(string file, T obj) {
using (StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))) {
foreach (KeyValuePair<string, PropertyInfo> prop in props) {
Type type = prop.Value.PropertyType;
object value = prop.Value.GetValue(obj);
var type = prop.Value.PropertyType;
var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
if (!converters.TryGetValue(type, out ITypeConverter serializer)) {
serializer = ClrTypeConverter.Instance;
}
if (serializer.TryWriteType(type, value, out string? converted)) {
if (converter.TryWriteType(type, prop.Value.GetValue(obj), out string? converted)) {
if (converted != null) {
writer.Write(prop.Key);
writer.Write(' ');
@ -140,11 +132,10 @@ public void Read(string file, T obj) {
string value = UnescapeLine(line.Substring(space + 1));
if (props.TryGetValue(property, out PropertyInfo info)) {
if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)) {
serializer = ClrTypeConverter.Instance;
}
var type = info.PropertyType;
var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
if (serializer.TryReadType(info.PropertyType, value, out object? converted)) {
if (converter.TryReadType(type, value, out object? converted)) {
info.SetValue(obj, converted);
}
else {

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
namespace TweetLib.Utils.Serialization {
public sealed class TypeConverterRegistry {
private readonly Dictionary<Type, ITypeConverter> converters = new ();
public void Register(Type type, ITypeConverter converter) {
converters[type] = converter;
}
public ITypeConverter? TryGet(Type type) {
return converters.TryGetValue(type, out var converter) ? converter : null;
}
}
}