diff --git a/Application/DialogHandler.cs b/Application/FileDialogs.cs
similarity index 70%
rename from Application/DialogHandler.cs
rename to Application/FileDialogs.cs
index 23868c40..b2907ec6 100644
--- a/Application/DialogHandler.cs
+++ b/Application/FileDialogs.cs
@@ -2,21 +2,12 @@ using System;
using System.Linq;
using System.Text;
using System.Windows.Forms;
-using TweetDuck.Dialogs;
using TweetDuck.Management;
using TweetLib.Core.Application;
using TweetLib.Core.Systems.Dialogs;
namespace TweetDuck.Application {
- sealed class DialogHandler : IAppDialogHandler {
- public void Information(string caption, string text, string buttonAccept, string buttonCancel = null) {
- FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept, buttonCancel));
- }
-
- public void Error(string caption, string text, string buttonAccept, string buttonCancel = null) {
- FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept, buttonCancel));
- }
-
+ sealed class FileDialogs : IAppFileDialogs {
public void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted) {
static string FormatFilter(FileDialogFilter filter) {
var builder = new StringBuilder();
diff --git a/Application/Logger.cs b/Application/Logger.cs
deleted file mode 100644
index 687949c8..00000000
--- a/Application/Logger.cs
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/Application/MessageDialogs.cs b/Application/MessageDialogs.cs
new file mode 100644
index 00000000..23bea118
--- /dev/null
+++ b/Application/MessageDialogs.cs
@@ -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));
+ }
+ }
+}
diff --git a/Application/SystemHandler.cs b/Application/SystemHandler.cs
index 91191019..d93ac253 100644
--- a/Application/SystemHandler.cs
+++ b/Application/SystemHandler.cs
@@ -4,26 +4,15 @@ using System.Drawing;
using System.IO;
using System.Windows.Forms;
using TweetDuck.Browser;
-using TweetDuck.Configuration;
using TweetDuck.Dialogs;
using TweetDuck.Management;
using TweetLib.Core;
using TweetLib.Core.Application;
using TweetLib.Core.Features.Twitter;
+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 @@ namespace TweetDuck.Application {
}
}
- 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 @@ namespace TweetDuck.Application {
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 @@ namespace TweetDuck.Application {
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);
+ };
}
}
diff --git a/Browser/Adapters/CefResourceProvider.cs b/Browser/Adapters/CefResourceProvider.cs
deleted file mode 100644
index 4fc9ff5c..00000000
--- a/Browser/Adapters/CefResourceProvider.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/Browser/Adapters/CefSchemeHandlerFactory.cs b/Browser/Adapters/CefSchemeHandlerFactory.cs
index 9c6a7f56..db1ba2f8 100644
--- a/Browser/Adapters/CefSchemeHandlerFactory.cs
+++ b/Browser/Adapters/CefSchemeHandlerFactory.cs
@@ -5,7 +5,7 @@ using TweetLib.Browser.Interfaces;
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 @@ namespace TweetDuck.Browser.Adapters {
});
}
- 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;
}
}
}
diff --git a/Browser/Adapters/CefSchemeResourceVisitor.cs b/Browser/Adapters/CefSchemeResourceVisitor.cs
new file mode 100644
index 00000000..9d3c2d3f
--- /dev/null
+++ b/Browser/Adapters/CefSchemeResourceVisitor.cs
@@ -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;
+ }
+ }
+}
diff --git a/Browser/FormBrowser.cs b/Browser/FormBrowser.cs
index f485dd06..28f67dd6 100644
--- a/Browser/FormBrowser.cs
+++ b/Browser/FormBrowser.cs
@@ -21,6 +21,7 @@ using TweetLib.Core.Features.Notifications;
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 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
}
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 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
// callback handlers
- public void OnIntroductionClosed(bool showGuide) {
+ private void OnIntroductionClosed(bool showGuide) {
if (Config.FirstRun) {
Config.FirstRun = false;
Config.Save();
@@ -372,7 +376,7 @@ namespace TweetDuck.Browser {
}
}
- public void OpenContextMenu() {
+ private void OpenContextMenu() {
contextMenu.Show(this, PointToClient(Cursor.Position));
}
@@ -428,7 +432,7 @@ namespace TweetDuck.Browser {
}
}
- public void OpenProfileImport() {
+ private void OpenProfileImport() {
FormManager.TryFind<FormSettings>()?.Close();
using DialogSettingsManage dialog = new DialogSettingsManage(plugins, true);
@@ -440,11 +444,11 @@ namespace TweetDuck.Browser {
}
}
- 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 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
}
}
- public void StopVideo() {
+ private void StopVideo() {
videoPlayer?.Close();
}
@@ -502,12 +506,12 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
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);
+ }
+ }
}
}
diff --git a/Browser/Handling/ContextMenuBrowser.cs b/Browser/Handling/ContextMenuBrowser.cs
index 689726cc..c7fad0fa 100644
--- a/Browser/Handling/ContextMenuBrowser.cs
+++ b/Browser/Handling/ContextMenuBrowser.cs
@@ -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 {
diff --git a/Browser/Handling/FileDialogHandler.cs b/Browser/Handling/FileDialogHandler.cs
index a1751c25..29676aa2 100644
--- a/Browser/Handling/FileDialogHandler.cs
+++ b/Browser/Handling/FileDialogHandler.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Windows.Forms;
using CefSharp;
+using TweetLib.Core;
using TweetLib.Utils.Static;
namespace TweetDuck.Browser.Handling {
@@ -46,17 +47,22 @@ namespace TweetDuck.Browser.Handling {
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;
}
}
}
diff --git a/Browser/Notification/FormNotificationBase.cs b/Browser/Notification/FormNotificationBase.cs
index 1a2d4d69..199db797 100644
--- a/Browser/Notification/FormNotificationBase.cs
+++ b/Browser/Notification/FormNotificationBase.cs
@@ -9,6 +9,7 @@ using TweetDuck.Utils;
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 @@ namespace TweetDuck.Browser.Notification {
protected virtual bool CanDragWindow => true;
public new Point Location {
- get { return base.Location; }
+ get {
+ return base.Location;
+ }
set {
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
diff --git a/Browser/Notification/FormNotificationMain.cs b/Browser/Notification/FormNotificationMain.cs
index 7ef45660..ccfa9c2c 100644
--- a/Browser/Notification/FormNotificationMain.cs
+++ b/Browser/Notification/FormNotificationMain.cs
@@ -61,7 +61,9 @@ namespace TweetDuck.Browser.Notification {
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) {
diff --git a/Browser/TrayIcon.cs b/Browser/TrayIcon.cs
index 86fd3a38..44b83ccb 100644
--- a/Browser/TrayIcon.cs
+++ b/Browser/TrayIcon.cs
@@ -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 @@ namespace TweetDuck.Browser {
public event EventHandler ClickClose;
public bool Visible {
- get { return notifyIcon.Visible; }
+ get {
+ return notifyIcon.Visible;
+ }
set {
notifyIcon.Visible = value;
@@ -30,7 +33,9 @@ namespace TweetDuck.Browser {
}
public bool HasNotifications {
- get { return hasNotifications; }
+ get {
+ return hasNotifications;
+ }
set {
if (hasNotifications != value) {
@@ -74,7 +79,7 @@ namespace TweetDuck.Browser {
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;
}
}
diff --git a/Browser/TweetDeckBrowser.cs b/Browser/TweetDeckBrowser.cs
index 572c638a..300d8e2a 100644
--- a/Browser/TweetDeckBrowser.cs
+++ b/Browser/TweetDeckBrowser.cs
@@ -66,7 +66,7 @@ namespace TweetDuck.Browser {
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 @@ namespace TweetDuck.Browser {
browser.GetBrowser().GetHost().SendFocusEvent(true);
}
- browserImpl.ScriptExecutor.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
+ browserComponent.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()");
}
}
}
diff --git a/Browser/TweetDeckInterfaceImpl.cs b/Browser/TweetDeckInterfaceImpl.cs
deleted file mode 100644
index 548d7fc3..00000000
--- a/Browser/TweetDeckInterfaceImpl.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Configuration/ConfigManager.cs b/Configuration/ConfigManager.cs
deleted file mode 100644
index 358074d3..00000000
--- a/Configuration/ConfigManager.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Configuration/PluginConfig.cs b/Configuration/PluginConfig.cs
deleted file mode 100644
index 9e1283de..00000000
--- a/Configuration/PluginConfig.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Configuration/SystemConfig.cs b/Configuration/SystemConfig.cs
index 5ddebef3..28692d47 100644
--- a/Configuration/SystemConfig.cs
+++ b/Configuration/SystemConfig.cs
@@ -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 @@ namespace TweetDuck.Configuration {
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();
}
}
diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 86c06dec..18755c89 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -3,14 +3,15 @@ using System.Diagnostics.CodeAnalysis;
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 @@ namespace TweetDuck.Configuration {
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 @@ namespace TweetDuck.Configuration {
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 @@ namespace TweetDuck.Configuration {
// END OF CONFIG
- protected override BaseConfig ConstructWithDefaults() {
+ public override UserConfig ConstructWithDefaults() {
return new UserConfig();
}
}
diff --git a/Controls/ControlExtensions.cs b/Controls/ControlExtensions.cs
index b2654a1c..abbcbb89 100644
--- a/Controls/ControlExtensions.cs
+++ b/Controls/ControlExtensions.cs
@@ -2,6 +2,7 @@ using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
+using TweetLib.Utils.Data;
namespace TweetDuck.Controls {
static class ControlExtensions {
@@ -80,5 +81,23 @@ namespace TweetDuck.Controls {
}
};
}
+
+ 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);
+ }
+ }
}
}
diff --git a/Dialogs/FormAbout.cs b/Dialogs/FormAbout.cs
index 1a31434e..96eacbc0 100644
--- a/Dialogs/FormAbout.cs
+++ b/Dialogs/FormAbout.cs
@@ -9,7 +9,6 @@ using TweetLib.Core;
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 @@ namespace TweetDuck.Dialogs {
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"));
diff --git a/Dialogs/FormMessage.cs b/Dialogs/FormMessage.cs
index 27ffb0c9..3b2bf2c7 100644
--- a/Dialogs/FormMessage.cs
+++ b/Dialogs/FormMessage.cs
@@ -38,11 +38,7 @@ namespace TweetDuck.Dialogs {
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) {
diff --git a/Dialogs/FormPlugins.cs b/Dialogs/FormPlugins.cs
index ae99d7a3..eb690f25 100644
--- a/Dialogs/FormPlugins.cs
+++ b/Dialogs/FormPlugins.cs
@@ -7,6 +7,8 @@ using TweetDuck.Management;
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 @@ namespace TweetDuck.Dialogs {
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 @@ namespace TweetDuck.Dialogs {
}
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) {
diff --git a/Dialogs/FormSettings.cs b/Dialogs/FormSettings.cs
index e61de79d..a16a0415 100644
--- a/Dialogs/FormSettings.cs
+++ b/Dialogs/FormSettings.cs
@@ -9,6 +9,7 @@ using TweetDuck.Controls;
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 @@ namespace TweetDuck.Dialogs {
}
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) {
diff --git a/Dialogs/Settings/DialogSettingsManage.cs b/Dialogs/Settings/DialogSettingsManage.cs
index d70cbb80..4109ef98 100644
--- a/Dialogs/Settings/DialogSettingsManage.cs
+++ b/Dialogs/Settings/DialogSettingsManage.cs
@@ -6,6 +6,7 @@ using TweetDuck.Configuration;
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 @@ namespace TweetDuck.Dialogs.Settings {
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 @@ namespace TweetDuck.Dialogs.Settings {
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 @@ namespace TweetDuck.Dialogs.Settings {
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);
diff --git a/Dialogs/Settings/TabSettingsAdvanced.cs b/Dialogs/Settings/TabSettingsAdvanced.cs
index 709e05cc..299a55da 100644
--- a/Dialogs/Settings/TabSettingsAdvanced.cs
+++ b/Dialogs/Settings/TabSettingsAdvanced.cs
@@ -125,7 +125,9 @@ namespace TweetDuck.Dialogs.Settings {
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 @@ namespace TweetDuck.Dialogs.Settings {
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();
diff --git a/Dialogs/Settings/TabSettingsFeedback.cs b/Dialogs/Settings/TabSettingsFeedback.cs
index c03faeea..8b333bea 100644
--- a/Dialogs/Settings/TabSettingsFeedback.cs
+++ b/Dialogs/Settings/TabSettingsFeedback.cs
@@ -14,7 +14,7 @@ namespace TweetDuck.Dialogs.Settings {
#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
diff --git a/Dialogs/WindowState.cs b/Dialogs/WindowState.cs
deleted file mode 100644
index 6df8f38a..00000000
--- a/Dialogs/WindowState.cs
+++ /dev/null
@@ -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'
- };
- }
- };
- }
-}
diff --git a/Management/LockManager.cs b/Management/LockManager.cs
index f477f63b..277a1776 100644
--- a/Management/LockManager.cs
+++ b/Management/LockManager.cs
@@ -13,6 +13,8 @@ namespace TweetDuck.Management {
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 @@ namespace TweetDuck.Management {
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 @@ namespace TweetDuck.Management {
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;
diff --git a/Management/ProfileManager.cs b/Management/ProfileManager.cs
index 1fd4cd76..2d43b2c0 100644
--- a/Management/ProfileManager.cs
+++ b/Management/ProfileManager.cs
@@ -24,8 +24,7 @@ namespace TweetDuck.Management {
UserConfig = 1,
SystemConfig = 2,
Session = 4,
- PluginData = 8,
- All = UserConfig | SystemConfig | Session | PluginData
+ PluginData = 8
}
private readonly string file;
@@ -41,15 +40,15 @@ namespace TweetDuck.Management {
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 @@ namespace TweetDuck.Management {
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;
diff --git a/Management/VideoPlayer.cs b/Management/VideoPlayer.cs
index 5a300158..ad147aec 100644
--- a/Management/VideoPlayer.cs
+++ b/Management/VideoPlayer.cs
@@ -8,6 +8,7 @@ using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetLib.Communication.Pipe;
using TweetLib.Core;
+using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Management {
sealed class VideoPlayer : IDisposable {
diff --git a/Plugins/PluginControl.cs b/Plugins/PluginControl.cs
index 0d62491a..2dfef825 100644
--- a/Plugins/PluginControl.cs
+++ b/Plugins/PluginControl.cs
@@ -6,6 +6,7 @@ using TweetDuck.Utils;
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 @@ namespace TweetDuck.Plugins {
private void btnToggleState_Click(object sender, EventArgs e) {
pluginManager.Config.SetEnabled(plugin, !pluginManager.Config.IsEnabled(plugin));
+ pluginManager.Config.Save();
UpdatePluginState();
}
diff --git a/Program.cs b/Program.cs
index da18b52a..221260bd 100644
--- a/Program.cs
+++ b/Program.cs
@@ -8,19 +8,18 @@ using TweetDuck.Browser;
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 @@ namespace TweetDuck {
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 @@ namespace TweetDuck {
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 @@ namespace TweetDuck {
return;
}
- Config.SaveAll();
+ App.Close();
Cef.Shutdown();
BrowserCache.Exit();
diff --git a/Reporter.cs b/Reporter.cs
index 5c75a443..8606b47b 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -48,7 +48,7 @@ namespace TweetDuck {
};
btnOpenLog.Click += (sender, args) => {
- if (!App.Logger.OpenLogFile()) {
+ if (!OpenLogFile()) {
FormMessage.Error("Error Log", "Cannot open error log.", FormMessage.OK);
}
};
@@ -61,6 +61,16 @@ namespace TweetDuck {
});
}
+ 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;
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 6917f1d5..487f37e1 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -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" />
diff --git a/Utils/BrowserUtils.cs b/Utils/BrowserUtils.cs
index 06a6fbc4..5838104e 100644
--- a/Utils/BrowserUtils.cs
+++ b/Utils/BrowserUtils.cs
@@ -10,7 +10,6 @@ using TweetLib.Browser.Interfaces;
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;
diff --git a/lib/TweetLib.Browser/Base/BaseBrowser.cs b/lib/TweetLib.Browser/Base/BaseBrowser.cs
index 21a67218..67c56473 100644
--- a/lib/TweetLib.Browser/Base/BaseBrowser.cs
+++ b/lib/TweetLib.Browser/Base/BaseBrowser.cs
@@ -3,15 +3,11 @@ using TweetLib.Browser.Interfaces;
namespace TweetLib.Browser.Base {
public class BaseBrowser<T> : IDisposable where T : BaseBrowser<T> {
- public IScriptExecutor ScriptExecutor { get; }
-
protected readonly IBrowserComponent browserComponent;
protected BaseBrowser(IBrowserComponent browserComponent, Func<T, BrowserSetup> setup) {
this.browserComponent = browserComponent;
this.browserComponent.Setup(setup((T) this));
-
- this.ScriptExecutor = browserComponent;
}
public virtual void Dispose() {}
diff --git a/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs b/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs
index 5974960e..bb71ae5a 100644
--- a/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs
+++ b/lib/TweetLib.Browser/Interfaces/ICustomSchemeHandler.cs
@@ -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);
}
}
diff --git a/lib/TweetLib.Browser/Interfaces/ISchemeResourceVisitor.cs b/lib/TweetLib.Browser/Interfaces/ISchemeResourceVisitor.cs
new file mode 100644
index 00000000..9bd2117a
--- /dev/null
+++ b/lib/TweetLib.Browser/Interfaces/ISchemeResourceVisitor.cs
@@ -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);
+ }
+}
diff --git a/lib/TweetLib.Browser/Request/SchemeResource.cs b/lib/TweetLib.Browser/Request/SchemeResource.cs
new file mode 100644
index 00000000..2434f6f4
--- /dev/null
+++ b/lib/TweetLib.Browser/Request/SchemeResource.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/lib/TweetLib.Core/App.cs b/lib/TweetLib.Core/App.cs
index 6fd92fea..4d59de71 100644
--- a/lib/TweetLib.Core/App.cs
+++ b/lib/TweetLib.Core/App.cs
@@ -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 @@ namespace TweetLib.Core {
}
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;
}
}
}
diff --git a/lib/TweetLib.Core/Application/AppException.cs b/lib/TweetLib.Core/Application/AppException.cs
index f3555522..80ae2dc5 100644
--- a/lib/TweetLib.Core/Application/AppException.cs
+++ b/lib/TweetLib.Core/Application/AppException.cs
@@ -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;
}
diff --git a/lib/TweetLib.Core/Application/AppStartup.cs b/lib/TweetLib.Core/Application/AppStartup.cs
deleted file mode 100644
index 22adcd68..00000000
--- a/lib/TweetLib.Core/Application/AppStartup.cs
+++ /dev/null
@@ -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);
- }
- }
- }
-}
diff --git a/lib/TweetLib.Core/Application/IAppDialogHandler.cs b/lib/TweetLib.Core/Application/IAppDialogHandler.cs
deleted file mode 100644
index a8fe3aa2..00000000
--- a/lib/TweetLib.Core/Application/IAppDialogHandler.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/lib/TweetLib.Core/Application/IAppFileDialogs.cs b/lib/TweetLib.Core/Application/IAppFileDialogs.cs
new file mode 100644
index 00000000..80a89cc9
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppFileDialogs.cs
@@ -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);
+ }
+}
diff --git a/lib/TweetLib.Core/Application/IAppLogger.cs b/lib/TweetLib.Core/Application/IAppLogger.cs
deleted file mode 100644
index 9389b283..00000000
--- a/lib/TweetLib.Core/Application/IAppLogger.cs
+++ /dev/null
@@ -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();
- }
-}
diff --git a/lib/TweetLib.Core/Application/IAppMessageDialogs.cs b/lib/TweetLib.Core/Application/IAppMessageDialogs.cs
new file mode 100644
index 00000000..18878c1a
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppMessageDialogs.cs
@@ -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);
+ }
+}
diff --git a/lib/TweetLib.Core/Application/IAppSetup.cs b/lib/TweetLib.Core/Application/IAppSetup.cs
new file mode 100644
index 00000000..72db9d33
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppSetup.cs
@@ -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);
+ }
+}
diff --git a/lib/TweetLib.Core/Application/IAppSystemHandler.cs b/lib/TweetLib.Core/Application/IAppSystemHandler.cs
index 2eda33c7..04563f8d 100644
--- a/lib/TweetLib.Core/Application/IAppSystemHandler.cs
+++ b/lib/TweetLib.Core/Application/IAppSystemHandler.cs
@@ -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);
}
}
diff --git a/lib/TweetLib.Core/Application/IAppUserConfiguration.cs b/lib/TweetLib.Core/Application/IAppUserConfiguration.cs
index b9f6a31c..5ea16b57 100644
--- a/lib/TweetLib.Core/Application/IAppUserConfiguration.cs
+++ b/lib/TweetLib.Core/Application/IAppUserConfiguration.cs
@@ -3,8 +3,8 @@ using TweetLib.Core.Features.Twitter;
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 @@ namespace TweetLib.Core.Application {
int CalendarFirstDay { get; }
string TranslationTarget { get; }
ImageQuality TwitterImageQuality { get; }
+ bool UseSystemProxyForAllConnections { get; }
event EventHandler MuteToggled;
event EventHandler OptionsDialogClosed;
diff --git a/lib/TweetLib.Core/Features/BaseContextMenu.cs b/lib/TweetLib.Core/Features/BaseContextMenu.cs
index fb62408c..53b8cdba 100644
--- a/lib/TweetLib.Core/Features/BaseContextMenu.cs
+++ b/lib/TweetLib.Core/Features/BaseContextMenu.cs
@@ -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 @@ namespace TweetLib.Core.Features {
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 @@ namespace TweetLib.Core.Features {
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 @@ namespace TweetLib.Core.Features {
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 @@ namespace TweetLib.Core.Features {
}
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));
+ }
}
}
}
diff --git a/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs b/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs
index d4176146..116774b2 100644
--- a/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs
+++ b/lib/TweetLib.Core/Features/BaseResourceRequestHandler.cs
@@ -7,7 +7,7 @@ using TweetLib.Browser.Request;
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);
diff --git a/lib/TweetLib.Core/Features/FileDownloadManager.cs b/lib/TweetLib.Core/Features/FileDownloadManager.cs
index 623c2463..3ecfa030 100644
--- a/lib/TweetLib.Core/Features/FileDownloadManager.cs
+++ b/lib/TweetLib.Core/Features/FileDownloadManager.cs
@@ -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.Core.Systems.Dialogs;
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 @@ namespace TweetLib.Core.Features {
}
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 @@ namespace TweetLib.Core.Features {
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 @@ namespace TweetLib.Core.Features {
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 @@ namespace TweetLib.Core.Features {
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);
});
}
}
diff --git a/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs b/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs
index 565cbfa1..e7c4fdfe 100644
--- a/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs
+++ b/lib/TweetLib.Core/Features/Notifications/NotificationBrowser.Tweet.cs
@@ -17,8 +17,8 @@ namespace TweetLib.Core.Features.Notifications {
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 @@ namespace TweetLib.Core.Features.Notifications {
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();
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs b/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
deleted file mode 100644
index 6d09f90e..00000000
--- a/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs
new file mode 100644
index 00000000..fd0ee996
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs
@@ -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);
+ }
+ }
+}
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
index b88d5a32..8babcc22 100644
--- a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
@@ -5,14 +5,14 @@ using System.Text;
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 @@ namespace TweetLib.Core.Features.Plugins.Config {
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;
diff --git a/lib/TweetLib.Core/Features/Plugins/PluginManager.cs b/lib/TweetLib.Core/Features/Plugins/PluginManager.cs
index 1f3008d6..066c482c 100644
--- a/lib/TweetLib.Core/Features/Plugins/PluginManager.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginManager.cs
@@ -11,12 +11,11 @@ using TweetLib.Utils.Data;
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 @@ namespace TweetLib.Core.Features.Plugins {
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 @@ namespace TweetLib.Core.Features.Plugins {
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 @@ namespace TweetLib.Core.Features.Plugins {
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));
}
}
}
diff --git a/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs b/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs
index 0ae9f242..0bb37525 100644
--- a/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginSchemeHandler.cs
@@ -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 @@ namespace TweetLib.Core.Features.Plugins {
}
}
- 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);
}
}
}
diff --git a/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs b/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
index d649d684..dc7d96d6 100644
--- a/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
@@ -4,7 +4,7 @@ using TweetLib.Core.Features.Plugins.Enums;
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 + '"')) + "]";
}
diff --git a/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs b/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs
index 4a9df223..ea791b2f 100644
--- a/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs
+++ b/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs
@@ -10,7 +10,6 @@ using TweetLib.Core.Features.Plugins.Enums;
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 @@ namespace TweetLib.Core.Features.TweetDeck {
private const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)";
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 @@ namespace TweetLib.Core.Features.TweetDeck {
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 @@ namespace TweetLib.Core.Features.TweetDeck {
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 @@ namespace TweetLib.Core.Features.TweetDeck {
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 @@ namespace TweetLib.Core.Features.TweetDeck {
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();
}
}
diff --git a/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs b/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs
index edd0c902..a7033c2e 100644
--- a/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs
+++ b/lib/TweetLib.Core/Features/TweetDeck/TweetDuckSchemeHandler.cs
@@ -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 @@ namespace TweetLib.Core.Features.TweetDeck {
};
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);
}
}
}
diff --git a/lib/TweetLib.Core/Lib.cs b/lib/TweetLib.Core/Lib.cs
index 3b02607e..c17607cb 100644
--- a/lib/TweetLib.Core/Lib.cs
+++ b/lib/TweetLib.Core/Lib.cs
@@ -9,10 +9,13 @@ using System.Threading;
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 @@ namespace TweetLib.Core {
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
#endif
- app.Build();
+ return app.Build();
}
}
}
diff --git a/lib/TweetLib.Core/Resources/CachingResourceProvider.cs b/lib/TweetLib.Core/Resources/CachingResourceProvider.cs
deleted file mode 100644
index 543a98ea..00000000
--- a/lib/TweetLib.Core/Resources/CachingResourceProvider.cs
+++ /dev/null
@@ -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);
- }
- }
- }
-}
diff --git a/lib/TweetLib.Core/Resources/ResourceCache.cs b/lib/TweetLib.Core/Resources/ResourceCache.cs
new file mode 100644
index 00000000..ef8411a3
--- /dev/null
+++ b/lib/TweetLib.Core/Resources/ResourceCache.cs
@@ -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;
+ }
+ }
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs b/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs
index 7f439160..855cf6c6 100644
--- a/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/BaseConfig.cs
@@ -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();
}
diff --git a/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs b/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
new file mode 100644
index 00000000..c8d6fc4d
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
@@ -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);
+ }
+ }
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs b/lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs
new file mode 100644
index 00000000..d6a2fa70
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs
@@ -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;
+ }
+ }
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs b/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs
index e64b9274..02a250c8 100644
--- a/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/FileConfigInstance.cs
@@ -3,25 +3,26 @@ using System.IO;
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 @@ namespace TweetLib.Core.Systems.Configuration {
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 @@ namespace TweetLib.Core.Systems.Configuration {
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);
diff --git a/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs b/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs
index cde35044..566e4568 100644
--- a/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/IConfigInstance.cs
@@ -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();
}
}
diff --git a/lib/TweetLib.Core/Systems/Configuration/IConfigManager.cs b/lib/TweetLib.Core/Systems/Configuration/IConfigManager.cs
deleted file mode 100644
index acf534ad..00000000
--- a/lib/TweetLib.Core/Systems/Configuration/IConfigManager.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace TweetLib.Core.Systems.Configuration {
- public interface IConfigManager {
- IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
- }
-}
diff --git a/lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs b/lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs
new file mode 100644
index 00000000..3374d12e
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs
@@ -0,0 +1,7 @@
+namespace TweetLib.Core.Systems.Configuration {
+ public interface IConfigObject {}
+
+ public interface IConfigObject<T> : IConfigObject where T : IConfigObject<T> {
+ T ConstructWithDefaults();
+ }
+}
diff --git a/lib/TweetLib.Core/Systems/Logging/Logger.cs b/lib/TweetLib.Core/Systems/Logging/Logger.cs
new file mode 100644
index 00000000..53221d3c
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Logging/Logger.cs
@@ -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;
+ }
+ }
+ }
+}
diff --git a/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs b/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs
index 587214ac..4eb86ac2 100644
--- a/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs
+++ b/lib/TweetLib.Core/Systems/Updates/UpdateInfo.cs
@@ -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 @@ namespace TweetLib.Core.Systems.Updates {
return;
}
- WebClient client = WebUtils.NewClient($"{Lib.BrandName} {Version.Tag}");
+ WebClient client = WebUtils.NewClient();
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
DownloadStatus = UpdateDownloadStatus.Done;
diff --git a/lib/TweetLib.Utils/Data/WindowState.cs b/lib/TweetLib.Utils/Data/WindowState.cs
new file mode 100644
index 00000000..5c399da5
--- /dev/null
+++ b/lib/TweetLib.Utils/Data/WindowState.cs
@@ -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; }
+ }
+}
diff --git a/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs b/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs
index 332efae5..a48a4725 100644
--- a/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs
+++ b/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs
@@ -50,16 +50,12 @@ namespace TweetLib.Utils.Serialization {
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 @@ namespace TweetLib.Utils.Serialization {
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 @@ namespace TweetLib.Utils.Serialization {
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 {
diff --git a/lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs b/lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs
new file mode 100644
index 00000000..ea2fee13
--- /dev/null
+++ b/lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs
@@ -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;
+ }
+ }
+}