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.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.IO; using System.Windows.Forms; using TweetDuck.Browser; -using TweetDuck.Configuration; using TweetDuck.Dialogs; using TweetDuck.Management; using TweetLib.Core; using TweetLib.Core.Application; using TweetLib.Core.Features.Twitter; +using TweetLib.Core.Systems.Configuration; namespace TweetDuck.Application { sealed class SystemHandler : IAppSystemHandler { - public void OpenAssociatedProgram(string path) { - try { - using (Process.Start(new ProcessStartInfo { - FileName = path, - ErrorDialog = true - })) {} - } catch (Exception e) { - App.ErrorHandler.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e); - } - } - public void OpenBrowser(string url) { if (string.IsNullOrWhiteSpace(url)) { return; @@ -92,7 +81,19 @@ public void OpenFileExplorer(string path) { } } - public void CopyImageFromFile(string path) { + public IAppSystemHandler.OpenAssociatedProgramFunc OpenAssociatedProgram { get; } = path => { + try { + using (Process.Start(new ProcessStartInfo { + FileName = path, + ErrorDialog = true + })) {} + } catch (Exception e) { + App.ErrorHandler.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e); + } + }; + + + public IAppSystemHandler.CopyImageFromFileFunc CopyImageFromFile { get; } = path => { FormManager.RunOnUIThreadAsync(() => { Image image; @@ -105,18 +106,18 @@ public void CopyImageFromFile(string path) { ClipboardManager.SetImage(image); }); - } + }; - public void CopyText(string text) { + public IAppSystemHandler.CopyTextFunc CopyText { get; } = text => { FormManager.RunOnUIThreadAsync(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText)); - } + }; - public void SearchText(string text) { + public IAppSystemHandler.SearchTextFunc SearchText { get; } = text => { if (string.IsNullOrWhiteSpace(text)) { return; } - FormManager.RunOnUIThreadAsync(() => { + void PerformSearch() { var config = Program.Config.User; string searchUrl = config.SearchEngineUrl; @@ -138,15 +139,17 @@ public void SearchText(string text) { settings.FormClosed += (sender, args) => { if (args.CloseReason == CloseReason.UserClosing && config.SearchEngineUrl != searchUrl) { - SearchText(text); + PerformSearch(); } }; } } else { - OpenBrowser(searchUrl + Uri.EscapeUriString(text)); + App.SystemHandler.OpenBrowser(searchUrl + Uri.EscapeUriString(text)); } - }); - } + } + + FormManager.RunOnUIThreadAsync(PerformSearch); + }; } } 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 @@ namespace TweetDuck.Browser.Adapters { internal sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory { - public static void Register(CefSettings settings, ICustomSchemeHandler<IResourceHandler> handler) { + public static void Register(CefSettings settings, ICustomSchemeHandler handler) { settings.RegisterScheme(new CefCustomScheme { SchemeName = handler.Protocol, IsStandard = false, @@ -16,14 +16,14 @@ public static void Register(CefSettings settings, ICustomSchemeHandler<IResource }); } - private readonly ICustomSchemeHandler<IResourceHandler> handler; + private readonly ICustomSchemeHandler handler; - private CefSchemeHandlerFactory(ICustomSchemeHandler<IResourceHandler> handler) { + private CefSchemeHandlerFactory(ICustomSchemeHandler handler) { this.handler = handler; } public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) { - return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri) : null; + return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(CefSchemeResourceVisitor.Instance) : null; } } } 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.Plugins; using TweetLib.Core.Features.TweetDeck; using TweetLib.Core.Resources; +using TweetLib.Core.Systems.Configuration; using TweetLib.Core.Systems.Updates; namespace TweetDuck.Browser { @@ -51,11 +52,12 @@ public bool IsWaiting { private readonly FormNotificationTweet notification; #pragma warning restore IDE0069 // Disposable fields should be disposed - private readonly CachingResourceProvider<IResourceHandler> resourceProvider; + private readonly ResourceCache resourceCache; private readonly ITweetDeckInterface tweetDeckInterface; private readonly PluginManager plugins; private readonly UpdateChecker updates; private readonly ContextMenu contextMenu; + private readonly uint windowRestoreMessage; private bool isLoaded; private FormWindowState prevState; @@ -63,12 +65,12 @@ public bool IsWaiting { private TweetScreenshotManager notificationScreenshotManager; private VideoPlayer videoPlayer; - public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, PluginManager pluginManager, IUpdateCheckClient updateCheckClient) { + public FormBrowser(ResourceCache resourceCache, PluginManager pluginManager, IUpdateCheckClient updateCheckClient, uint windowRestoreMessage) { InitializeComponent(); Text = Program.BrandName; - this.resourceProvider = resourceProvider; + this.resourceCache = resourceCache; this.plugins = pluginManager; @@ -84,19 +86,21 @@ public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, P this.browser = new TweetDeckBrowser(this, plugins, tweetDeckInterface, updates); this.contextMenu = ContextMenuBrowser.CreateMenu(this); + this.windowRestoreMessage = windowRestoreMessage; + Controls.Add(new MenuStrip { Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update + Config.MuteToggled += Config_MuteToggled; + Config.TrayBehaviorChanged += Config_TrayBehaviorChanged; + Disposed += (sender, args) => { Config.MuteToggled -= Config_MuteToggled; Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged; browser.Dispose(); }; - Config.MuteToggled += Config_MuteToggled; - this.trayIcon.ClickRestore += trayIcon_ClickRestore; this.trayIcon.ClickClose += trayIcon_ClickClose; - Config.TrayBehaviorChanged += Config_TrayBehaviorChanged; UpdateTray(); @@ -162,7 +166,7 @@ private void FormBrowser_Activated(object sender, EventArgs e) { trayIcon.HasNotifications = false; - if (!browser.Enabled) { // when taking a screenshot, the window is unfocused and + if (!browser.Enabled) { // when taking a screenshot, the window is unfocused and browser.Enabled = true; // the browser is disabled; if the user clicks back into } // the window, enable the browser again } @@ -308,7 +312,7 @@ private void updateInteractionManager_UpdateDismissed(object sender, UpdateInfo } protected override void WndProc(ref Message m) { - if (isLoaded && m.Msg == Program.WindowRestoreMessage) { + if (isLoaded && m.Msg == windowRestoreMessage) { using Process me = Process.GetCurrentProcess(); if (me.Id == m.WParam.ToInt32()) { @@ -345,10 +349,10 @@ public void ResumeNotification() { public void ReloadToTweetDeck() { #if DEBUG Resources.ResourceHotSwap.Run(); - resourceProvider.ClearCache(); + resourceCache.ClearCache(); #else if (ModifierKeys.HasFlag(Keys.Shift)) { - resourceProvider.ClearCache(); + resourceCache.ClearCache(); } #endif @@ -361,7 +365,7 @@ public void OpenDevTools() { // callback handlers - public void OnIntroductionClosed(bool showGuide) { + private void OnIntroductionClosed(bool showGuide) { if (Config.FirstRun) { Config.FirstRun = false; Config.Save(); @@ -372,7 +376,7 @@ public void OnIntroductionClosed(bool showGuide) { } } - public void OpenContextMenu() { + private void OpenContextMenu() { contextMenu.Show(this, PointToClient(Cursor.Position)); } @@ -428,7 +432,7 @@ public void OpenPlugins() { } } - public void OpenProfileImport() { + private void OpenProfileImport() { FormManager.TryFind<FormSettings>()?.Close(); using DialogSettingsManage dialog = new DialogSettingsManage(plugins, true); @@ -440,11 +444,11 @@ public void OpenProfileImport() { } } - public void ShowDesktopNotification(DesktopNotification notification) { + private void ShowDesktopNotification(DesktopNotification notification) { this.notification.ShowNotification(notification); } - public void OnTweetNotification() { // may be called multiple times, once for each type of notification + private void OnTweetNotification() { // may be called multiple times, once for each type of notification if (Config.EnableTrayHighlight && !ContainsFocus) { trayIcon.HasNotifications = true; } @@ -454,7 +458,7 @@ public void SaveVideo(string url, string username) { browser.SaveVideo(url, username); } - public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) { + private void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) { if (Arguments.HasFlag(Arguments.ArgHttpVideo)) { videoUrl = Regex.Replace(videoUrl, "^https://", "http://"); } @@ -486,7 +490,7 @@ public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavasc } } - public void StopVideo() { + private void StopVideo() { videoPlayer?.Close(); } @@ -502,12 +506,12 @@ public bool ShowTweetDetail(string columnId, string chirpId, string fallbackUrl) return true; } - public void OnTweetScreenshotReady(string html, int width) { + private void OnTweetScreenshotReady(string html, int width) { notificationScreenshotManager ??= new TweetScreenshotManager(this, plugins); notificationScreenshotManager.Trigger(html, width); } - public void DisplayTooltip(string text) { + private void DisplayTooltip(string text) { if (string.IsNullOrEmpty(text)) { toolTip.Hide(this); } @@ -530,5 +534,75 @@ bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) { return false; } + + private sealed class TweetDeckInterfaceImpl : ITweetDeckInterface { + private readonly FormBrowser form; + + public TweetDeckInterfaceImpl(FormBrowser form) { + this.form = form; + } + + public void Alert(string type, string contents) { + MessageBoxIcon icon = type switch { + "error" => MessageBoxIcon.Error, + "warning" => MessageBoxIcon.Warning, + "info" => MessageBoxIcon.Information, + _ => MessageBoxIcon.None + }; + + FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK); + } + + public void DisplayTooltip(string text) { + form.InvokeAsyncSafe(() => form.DisplayTooltip(text)); + } + + public void FixClipboard() { + form.InvokeAsyncSafe(ClipboardManager.StripHtmlStyles); + } + + public int GetIdleSeconds() { + return NativeMethods.GetIdleSeconds(); + } + + public void OnIntroductionClosed(bool showGuide) { + form.InvokeAsyncSafe(() => form.OnIntroductionClosed(showGuide)); + } + + public void OnSoundNotification() { + form.InvokeAsyncSafe(form.OnTweetNotification); + } + + public void OpenContextMenu() { + form.InvokeAsyncSafe(form.OpenContextMenu); + } + + public void OpenProfileImport() { + form.InvokeAsyncSafe(form.OpenProfileImport); + } + + public void PlayVideo(string videoUrl, string tweetUrl, string username, object callShowOverlay) { + form.InvokeAsyncSafe(() => form.PlayVideo(videoUrl, tweetUrl, username, (IJavascriptCallback) callShowOverlay)); + } + + public void ScreenshotTweet(string html, int width) { + form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width)); + } + + public void ShowDesktopNotification(DesktopNotification notification) { + form.InvokeAsyncSafe(() => { + form.OnTweetNotification(); + form.ShowDesktopNotification(notification); + }); + } + + public void StopVideo() { + form.InvokeAsyncSafe(form.StopVideo); + } + + public Task ExecuteCallback(object callback, params object[] parameters) { + return ((IJavascriptCallback) callback).ExecuteAsync(parameters); + } + } } } 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.Linq; using System.Windows.Forms; using CefSharp; +using TweetLib.Core; using TweetLib.Utils.Static; namespace TweetDuck.Browser.Handling { @@ -46,17 +47,22 @@ private static IEnumerable<string> ParseFileType(string type) { return new string[] { type }; } - switch (type) { - case "image/jpeg": return new string[] { ".jpg", ".jpeg" }; - case "image/png": return new string[] { ".png" }; - case "image/gif": return new string[] { ".gif" }; - case "image/webp": return new string[] { ".webp" }; - case "video/mp4": return new string[] { ".mp4" }; - case "video/quicktime": return new string[] { ".mov", ".qt" }; + string[] extensions = type switch { + "image/jpeg" => new string[] { ".jpg", ".jpeg" }, + "image/png" => new string[] { ".png" }, + "image/gif" => new string[] { ".gif" }, + "image/webp" => new string[] { ".webp" }, + "video/mp4" => new string[] { ".mp4" }, + "video/quicktime" => new string[] { ".mov", ".qt" }, + _ => StringUtils.EmptyArray + }; + + if (extensions.Length == 0) { + App.Logger.Warn("Unknown file type: " + type); + Debugger.Break(); } - Debugger.Break(); - return StringUtils.EmptyArray; + return extensions; } } } 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 TweetLib.Browser.Interfaces; using TweetLib.Core.Features.Notifications; using TweetLib.Core.Features.Twitter; +using TweetLib.Core.Systems.Configuration; namespace TweetDuck.Browser.Notification { abstract partial class FormNotificationBase : Form { @@ -59,7 +60,9 @@ protected virtual Point PrimaryLocation { protected virtual bool CanDragWindow => true; public new Point Location { - get { return base.Location; } + get { + return base.Location; + } set { Visible = (base.Location = value) != ControlExtensions.InvisibleLocation; 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 @@ private static int FontSizeLevel { private int? prevFontSize; public virtual bool RequiresResize { - get { return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel; } + get { + return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel; + } set { if (value) { 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 @@ public enum Behavior { // keep order public event EventHandler ClickClose; public bool Visible { - get { return notifyIcon.Visible; } + get { + return notifyIcon.Visible; + } set { notifyIcon.Visible = value; @@ -30,7 +33,9 @@ public bool Visible { } public bool HasNotifications { - get { return hasNotifications; } + get { + return hasNotifications; + } set { if (hasNotifications != value) { @@ -74,7 +79,7 @@ protected override void Dispose(bool disposing) { private void UpdateIcon() { if (Visible) { - notifyIcon.Icon = hasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray; + notifyIcon.Icon = HasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray; } } 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 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDe if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) { browserComponent.PageLoadEnd += (sender, args) => { if (TwitterUrls.IsTweetDeck(args.Url)) { - browserImpl.ScriptExecutor.RunScript("gen:gdpr", "TD.storage.Account.prototype.requiresConsent = function() { return false; }"); + browserComponent.RunScript("gen:gdpr", "TD.storage.Account.prototype.requiresConsent = function() { return false; }"); } }; } @@ -126,7 +126,7 @@ public void HideVideoOverlay(bool focus) { browser.GetBrowser().GetHost().SendFocusEvent(true); } - browserImpl.ScriptExecutor.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()"); + browserComponent.RunScript("gen:hidevideo", "$('#td-video-player-overlay').remove()"); } } } 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 @@ sealed class SystemConfig : BaseConfig { public bool HardwareAcceleration { get => _hardwareAcceleration; - set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, App.ConfigManager.TriggerProgramRestartRequested); } // END OF CONFIG - protected override BaseConfig ConstructWithDefaults() { + public override SystemConfig ConstructWithDefaults() { return new SystemConfig(); } } 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.Drawing; using TweetDuck.Browser; using TweetDuck.Controls; -using TweetDuck.Dialogs; +using TweetLib.Core; using TweetLib.Core.Application; using TweetLib.Core.Features.Notifications; using TweetLib.Core.Features.Twitter; using TweetLib.Core.Systems.Configuration; +using TweetLib.Utils.Data; namespace TweetDuck.Configuration { - sealed class UserConfig : BaseConfig, IAppUserConfiguration { + sealed class UserConfig : BaseConfig<UserConfig>, IAppUserConfiguration { public bool FirstRun { get; set; } = true; [SuppressMessage("ReSharper", "UnusedMember.Global")] @@ -45,7 +46,7 @@ sealed class UserConfig : BaseConfig, IAppUserConfiguration { private string _spellCheckLanguage = "en-US"; public string TranslationTarget { get; set; } = "en"; - public int CalendarFirstDay { get; set; } = -1; + public int CalendarFirstDay { get; set; } = -1; private TrayIcon.Behavior _trayBehavior = TrayIcon.Behavior.Disabled; public bool EnableTrayHighlight { get; set; } = true; @@ -121,32 +122,32 @@ public TrayIcon.Behavior TrayBehavior { public bool EnableSmoothScrolling { get => _enableSmoothScrolling; - set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, App.ConfigManager.TriggerProgramRestartRequested); } public bool EnableTouchAdjustment { get => _enableTouchAdjustment; - set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, App.ConfigManager.TriggerProgramRestartRequested); } public bool EnableColorProfileDetection { get => _enableColorProfileDetection; - set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, App.ConfigManager.TriggerProgramRestartRequested); } public bool UseSystemProxyForAllConnections { get => _useSystemProxyForAllConnections; - set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, App.ConfigManager.TriggerProgramRestartRequested); } public string CustomCefArgs { get => _customCefArgs; - set => UpdatePropertyWithCallback(ref _customCefArgs, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _customCefArgs, value, App.ConfigManager.TriggerProgramRestartRequested); } public string SpellCheckLanguage { get => _spellCheckLanguage; - set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, Program.Config.TriggerProgramRestartRequested); + set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, App.ConfigManager.TriggerProgramRestartRequested); } // EVENTS @@ -163,7 +164,7 @@ public void TriggerOptionsDialogClosed() { // END OF CONFIG - protected override BaseConfig ConstructWithDefaults() { + public override UserConfig ConstructWithDefaults() { return new UserConfig(); } } 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.Drawing; using System.Linq; using System.Windows.Forms; +using TweetLib.Utils.Data; namespace TweetDuck.Controls { static class ControlExtensions { @@ -80,5 +81,23 @@ public static void EnableMultilineShortcuts(this TextBox textBox) { } }; } + + public static void Save(this WindowState state, Form form) { + state.Bounds = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds; + state.IsMaximized = form.WindowState == FormWindowState.Maximized; + } + + public static void Restore(this WindowState state, Form form, bool firstTimeFullscreen) { + if (state.Bounds != Rectangle.Empty) { + form.DesktopBounds = state.Bounds; + form.WindowState = state.IsMaximized ? FormWindowState.Maximized : FormWindowState.Normal; + } + + if ((state.Bounds == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) { + form.DesktopBounds = Screen.PrimaryScreen.WorkingArea; + form.WindowState = FormWindowState.Maximized; + state.Save(form); + } + } } } 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 @@ namespace TweetDuck.Dialogs { sealed partial class FormAbout : Form, FormManager.IAppDialog { private const string TipsLink = "https://github.com/chylex/TweetDuck/wiki"; - private const string IssuesLink = "https://github.com/chylex/TweetDuck/issues"; public FormAbout() { InitializeComponent(); @@ -20,7 +19,7 @@ public FormAbout() { labelWebsite.Links.Add(new LinkLabel.Link(0, labelWebsite.Text.Length, Program.Website)); labelTips.Links.Add(new LinkLabel.Link(0, labelTips.Text.Length, TipsLink)); - labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, IssuesLink)); + labelIssues.Links.Add(new LinkLabel.Link(0, labelIssues.Text.Length, Lib.IssueTrackerUrl)); try { pictureLogo.Image = Image.FromFile(Path.Combine(App.ResourcesPath, "images/logo.png")); 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 @@ public static bool Question(string caption, string text, string buttonAccept, st return Show(caption, text, MessageBoxIcon.Question, buttonAccept, buttonCancel); } - public static bool Show(string caption, string text, MessageBoxIcon icon, string button) { - return Show(caption, text, icon, button, null); - } - - public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel) { + public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel = null) { using FormMessage message = new FormMessage(caption, text, icon); if (buttonCancel == null) { 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.Plugins; using TweetLib.Core; using TweetLib.Core.Features.Plugins; +using TweetLib.Core.Features.Plugins.Enums; +using TweetLib.Core.Systems.Configuration; namespace TweetDuck.Dialogs { sealed partial class FormPlugins : Form, FormManager.IAppDialog { @@ -28,14 +30,18 @@ public FormPlugins(PluginManager pluginManager) : this() { Size = new Size(Math.Max(MinimumSize.Width, targetSize.Width), Math.Max(MinimumSize.Height, targetSize.Height)); } - Shown += (sender, args) => { ReloadPluginList(); }; + Shown += (sender, args) => { + ReloadPluginList(); + }; FormClosed += (sender, args) => { Config.PluginsWindowSize = Size; Config.Save(); }; - ResizeEnd += (sender, args) => { timerLayout.Start(); }; + ResizeEnd += (sender, args) => { + timerLayout.Start(); + }; } private int GetPluginOrderIndex(Plugin plugin) { @@ -92,7 +98,7 @@ private void flowLayoutPlugins_Resize(object sender, EventArgs e) { } private void btnOpenFolder_Click(object sender, EventArgs e) { - App.SystemHandler.OpenFileExplorer(pluginManager.CustomPluginFolder); + App.SystemHandler.OpenFileExplorer(pluginManager.GetPluginFolder(PluginGroup.Custom)); } private void btnReload_Click(object sender, EventArgs e) { 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.Dialogs.Settings; using TweetDuck.Management; using TweetDuck.Utils; +using TweetLib.Core; using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.TweetDeck; using TweetLib.Core.Systems.Updates; @@ -50,14 +51,14 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateChecker up } private void PrepareLoad() { - Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested; + App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested; } private void PrepareUnload() { // TODO refactor this further later currentTab.Control.OnClosing(); - Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested; - Program.Config.SaveAll(); + App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested; + App.ConfigManager.SaveAll(); } private void Config_ProgramRestartRequested(object sender, EventArgs e) { 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.Management; using TweetLib.Core; using TweetLib.Core.Features.Plugins; +using TweetLib.Core.Systems.Configuration; using TweetLib.Utils.Static; namespace TweetDuck.Dialogs.Settings { @@ -133,7 +134,7 @@ private void btnContinue_Click(object sender, EventArgs e) { case State.Reset: if (FormMessage.Warning("Reset TweetDuck Options", "This will reset the selected items. Are you sure you want to proceed?", FormMessage.Yes, FormMessage.No)) { - Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested; + App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested; if (SelectedItems.HasFlag(ProfileManager.Items.UserConfig)) { Program.Config.User.Reset(); @@ -143,7 +144,7 @@ private void btnContinue_Click(object sender, EventArgs e) { Program.Config.System.Reset(); } - Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested; + App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested; if (SelectedItems.HasFlag(ProfileManager.Items.PluginData)) { Program.Config.Plugins.Reset(); @@ -174,9 +175,9 @@ private void btnContinue_Click(object sender, EventArgs e) { case State.Import: if (importManager.Import(SelectedItems)) { - Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested; - Program.Config.ReloadAll(); - Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested; + App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested; + App.ConfigManager.ReloadAll(); + App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested; if (SelectedItemsForceRestart) { RestartProgram(SelectedItems.HasFlag(ProfileManager.Items.Session) ? new string[] { Arguments.ArgImportCookies } : StringUtils.EmptyArray); 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 @@ private void checkClearCacheAuto_CheckedChanged(object sender, EventArgs e) { private void btnEditCefArgs_Click(object sender, EventArgs e) { DialogSettingsCefArgs form = new DialogSettingsCefArgs(Config.CustomCefArgs); - form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); }; + form.VisibleChanged += (sender2, args2) => { + form.MoveToCenter(ParentForm); + }; form.FormClosed += (sender2, args2) => { RestoreParentForm(); @@ -144,7 +146,9 @@ private void btnEditCefArgs_Click(object sender, EventArgs e) { private void btnEditCSS_Click(object sender, EventArgs e) { DialogSettingsCSS form = new DialogSettingsCSS(Config.CustomBrowserCSS, Config.CustomNotificationCSS, reinjectBrowserCSS, openDevTools); - form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); }; + form.VisibleChanged += (sender2, args2) => { + form.MoveToCenter(ParentForm); + }; form.FormClosed += (sender2, args2) => { RestoreParentForm(); 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 @@ public override void OnReady() { #region Feedback private void btnSendFeedback_Click(object sender, EventArgs e) { - App.SystemHandler.OpenBrowser("https://github.com/chylex/TweetDuck/issues/new"); + App.SystemHandler.OpenBrowser(Lib.IssueTrackerUrl + "/new"); } #endregion 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 @@ sealed class LockManager { private const int CloseNaturallyTimeout = 10000; private const int CloseKillTimeout = 5000; + public uint WindowRestoreMessage { get; } = NativeMethods.RegisterWindowMessage("TweetDuckRestore"); + private readonly LockFile lockFile; public LockManager(string path) { @@ -33,7 +35,7 @@ private bool LaunchNormally() { LockResult lockResult = lockFile.Lock(); if (lockResult is LockResult.HasProcess info) { - if (!RestoreProcess(info.Process) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)) { + if (!RestoreProcess(info.Process, WindowRestoreMessage) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)) { if (!CloseProcess(info.Process)) { FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK); return false; @@ -83,9 +85,9 @@ private static void ShowGenericException(LockResult.Fail fail) { App.ErrorHandler.HandleException("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", false, fail.Exception); } - private static bool RestoreProcess(Process process) { + private static bool RestoreProcess(Process process, uint windowRestoreMessage) { if (process.MainWindowHandle == IntPtr.Zero) { // restore if the original process is in tray - NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint) process.Id, 0); + NativeMethods.BroadcastMessage(windowRestoreMessage, (uint) process.Id, 0); if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)) { return true; 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 @@ public enum Items { UserConfig = 1, SystemConfig = 2, Session = 4, - PluginData = 8, - All = UserConfig | SystemConfig | Session | PluginData + PluginData = 8 } private readonly string file; @@ -41,15 +40,15 @@ public bool Export(Items items) { using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None)); if (items.HasFlag(Items.UserConfig)) { - stream.WriteFile("config", Program.Config.FilePaths.UserConfig); + stream.WriteFile("config", App.ConfigManager.UserPath); } if (items.HasFlag(Items.SystemConfig)) { - stream.WriteFile("system", Program.Config.FilePaths.SystemConfig); + stream.WriteFile("system", App.ConfigManager.SystemPath); } if (items.HasFlag(Items.PluginData)) { - stream.WriteFile("plugin.config", Program.Config.FilePaths.PluginConfig); + stream.WriteFile("plugin.config", App.ConfigManager.PluginsPath); foreach (Plugin plugin in plugins.Plugins) { foreach (PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))) { @@ -122,21 +121,21 @@ public bool Import(Items items) { switch (entry.KeyName) { case "config": if (items.HasFlag(Items.UserConfig)) { - entry.WriteToFile(Program.Config.FilePaths.UserConfig); + entry.WriteToFile(App.ConfigManager.UserPath); } break; case "system": if (items.HasFlag(Items.SystemConfig)) { - entry.WriteToFile(Program.Config.FilePaths.SystemConfig); + entry.WriteToFile(App.ConfigManager.SystemPath); } break; case "plugin.config": if (items.HasFlag(Items.PluginData)) { - entry.WriteToFile(Program.Config.FilePaths.PluginConfig); + entry.WriteToFile(App.ConfigManager.PluginsPath); } break; 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.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 TweetLib.Core; using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins.Enums; +using TweetLib.Core.Systems.Configuration; namespace TweetDuck.Plugins { sealed partial class PluginControl : UserControl { @@ -92,6 +93,7 @@ private void btnConfigure_Click(object sender, EventArgs e) { private void btnToggleState_Click(object sender, EventArgs e) { pluginManager.Config.SetEnabled(plugin, !pluginManager.Config.IsEnabled(plugin)); + pluginManager.Config.Save(); UpdatePluginState(); } 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.Adapters; using TweetDuck.Browser.Handling; using TweetDuck.Configuration; -using TweetDuck.Dialogs; using TweetDuck.Management; using TweetDuck.Updates; using TweetDuck.Utils; using TweetLib.Core; using TweetLib.Core.Application; -using TweetLib.Core.Features; using TweetLib.Core.Features.Chromium; using TweetLib.Core.Features.Plugins; +using TweetLib.Core.Features.Plugins.Config; using TweetLib.Core.Features.TweetDeck; using TweetLib.Core.Resources; +using TweetLib.Core.Systems.Configuration; using TweetLib.Utils.Collections; -using TweetLib.Utils.Static; using Win = System.Windows.Forms; namespace TweetDuck { @@ -30,22 +29,17 @@ static class Program { public const string Website = "https://tweetduck.chylex.com"; - private const string PluginDataFolder = "TD_Plugins"; private const string InstallerFolder = "TD_Updates"; private const string CefDataFolder = "TD_Chromium"; - - private const string ProgramLogFile = "TD_Log.txt"; private const string ConsoleLogFile = "TD_Console.txt"; public static string ExecutablePath => Win.Application.ExecutablePath; - public static uint WindowRestoreMessage; - - private static LockManager lockManager; private static Reporter errorReporter; + private static LockManager lockManager; private static bool hasCleanedUp; - public static ConfigManager Config { get; private set; } + public static ConfigObjects<UserConfig, SystemConfig> Config { get; private set; } internal static void SetupWinForms() { Win.Application.EnableVisualStyles(); @@ -59,112 +53,99 @@ private static void Main() { SetupWinForms(); Cef.EnableHighDPISupport(); - var startup = new AppStartup { - CustomDataFolder = Arguments.GetValue(Arguments.ArgDataFolder) - }; - var reporter = new Reporter(); - var userConfig = new UserConfig(); - Lib.Initialize(new AppBuilder { - Startup = startup, - Logger = new Logger(ProgramLogFile), + Config = new ConfigObjects<UserConfig, SystemConfig>( + new UserConfig(), + new SystemConfig(), + new PluginConfig(new string[] { + "official/clear-columns", + "official/reply-account" + }) + ); + + Lib.AppLauncher launch = Lib.Initialize(new AppBuilder { + Setup = new Setup(), ErrorHandler = reporter, SystemHandler = new SystemHandler(), - DialogHandler = new DialogHandler(), - UserConfiguration = userConfig + MessageDialogs = new MessageDialogs(), + FileDialogs = new FileDialogs(), }); - LaunchApp(reporter, userConfig); - } - - private static void LaunchApp(Reporter reporter, UserConfig userConfig) { - App.Launch(); - errorReporter = reporter; - string storagePath = App.StoragePath; + launch(); + } - Config = new ConfigManager(userConfig, new ConfigManager.Paths { - UserConfig = Path.Combine(storagePath, "TD_UserConfig.cfg"), - SystemConfig = Path.Combine(storagePath, "TD_SystemConfig.cfg"), - PluginConfig = Path.Combine(storagePath, "TD_PluginConfig.cfg") - }); + private sealed class Setup : IAppSetup { + public bool IsPortable => File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "makeportable")); + public bool IsDebugLogging => Arguments.HasFlag(Arguments.ArgLogging); + public string CustomDataFolder => Arguments.GetValue(Arguments.ArgDataFolder); + public string ResourceRewriteRules => Arguments.GetValue(Arguments.ArgFreeze); - lockManager = new LockManager(Path.Combine(storagePath, ".lock")); - WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore"); - - if (!lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))) { - return; + public ConfigManager CreateConfigManager(string storagePath) { + return new ConfigManager<UserConfig, SystemConfig>(storagePath, Config); } - Config.LoadAll(); - - if (Arguments.HasFlag(Arguments.ArgImportCookies)) { - ProfileManager.ImportCookies(); - } - else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) { - ProfileManager.DeleteCookies(); + public bool TryLockDataFolder(string lockFile) { + lockManager = new LockManager(lockFile); + return lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart)); } - var installerFolderPath = Path.Combine(storagePath, InstallerFolder); - - if (Arguments.HasFlag(Arguments.ArgUpdated)) { - WindowsUtils.TryDeleteFolderWhenAble(installerFolderPath, 8000); - WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(storagePath, "Service Worker"), 4000); - BrowserCache.TryClearNow(); - } - - try { - BaseResourceRequestHandler.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze)); - } catch (Exception e) { - FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: " + e.Message, FormMessage.OK); - return; - } - - WebUtils.DefaultUserAgent = BrowserUtils.UserAgentVanilla; - - if (Config.User.UseSystemProxyForAllConnections) { - WebUtils.EnableSystemProxy(); - } - - BrowserCache.RefreshTimer(); - - CefSharpSettings.WcfEnabled = false; - - CefSettings settings = new CefSettings { - UserAgent = BrowserUtils.UserAgentChrome, - BrowserSubprocessPath = Path.Combine(App.ProgramPath, BrandName + ".Browser.exe"), - CachePath = storagePath, - UserDataPath = Path.Combine(storagePath, CefDataFolder), - LogFile = Path.Combine(storagePath, ConsoleLogFile), - #if !DEBUG - LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable - #endif - }; - - var resourceProvider = new CachingResourceProvider<IResourceHandler>(new ResourceProvider()); - var pluginManager = new PluginManager(Config.Plugins, Path.Combine(storagePath, PluginDataFolder)); - - CefSchemeHandlerFactory.Register(settings, new TweetDuckSchemeHandler<IResourceHandler>(resourceProvider)); - CefSchemeHandlerFactory.Register(settings, new PluginSchemeHandler<IResourceHandler>(resourceProvider, pluginManager)); - - CefUtils.ParseCommandLineArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs); - BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs); - - Cef.Initialize(settings, false, new BrowserProcessHandler()); - - Win.Application.ApplicationExit += (sender, args) => ExitCleanup(); - FormBrowser mainForm = new FormBrowser(resourceProvider, pluginManager, new UpdateCheckClient(installerFolderPath)); - Win.Application.Run(mainForm); - - if (mainForm.UpdateInstaller != null) { - ExitCleanup(); - - if (mainForm.UpdateInstaller.Launch()) { - Win.Application.Exit(); + public void BeforeLaunch() { + if (Arguments.HasFlag(Arguments.ArgImportCookies)) { + ProfileManager.ImportCookies(); } - else { - RestartWithArgsInternal(Arguments.GetCurrentClean()); + else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) { + ProfileManager.DeleteCookies(); + } + + if (Arguments.HasFlag(Arguments.ArgUpdated)) { + WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(App.StoragePath, InstallerFolder), 8000); + WindowsUtils.TryDeleteFolderWhenAble(Path.Combine(App.StoragePath, "Service Worker"), 4000); + BrowserCache.TryClearNow(); + } + } + + public void Launch(ResourceCache resourceCache, PluginManager pluginManager) { + string storagePath = App.StoragePath; + + BrowserCache.RefreshTimer(); + + CefSharpSettings.WcfEnabled = false; + + CefSettings settings = new CefSettings { + UserAgent = BrowserUtils.UserAgentChrome, + BrowserSubprocessPath = Path.Combine(App.ProgramPath, BrandName + ".Browser.exe"), + CachePath = storagePath, + UserDataPath = Path.Combine(storagePath, CefDataFolder), + LogFile = Path.Combine(storagePath, ConsoleLogFile), + #if !DEBUG + LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable + #endif + }; + + CefSchemeHandlerFactory.Register(settings, new TweetDuckSchemeHandler(resourceCache)); + CefSchemeHandlerFactory.Register(settings, new PluginSchemeHandler(resourceCache, pluginManager)); + + CefUtils.ParseCommandLineArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs); + BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs); + + Cef.Initialize(settings, false, new BrowserProcessHandler()); + + Win.Application.ApplicationExit += (sender, args) => ExitCleanup(); + var updateCheckClient = new UpdateCheckClient(Path.Combine(storagePath, InstallerFolder)); + var mainForm = new FormBrowser(resourceCache, pluginManager, updateCheckClient, lockManager.WindowRestoreMessage); + Win.Application.Run(mainForm); + + if (mainForm.UpdateInstaller != null) { + ExitCleanup(); + + if (mainForm.UpdateInstaller.Launch()) { + Win.Application.Exit(); + } + else { + RestartWithArgsInternal(Arguments.GetCurrentClean()); + } } } } @@ -213,7 +194,7 @@ private static void ExitCleanup() { return; } - Config.SaveAll(); + App.Close(); Cef.Shutdown(); BrowserCache.Exit(); diff --git a/Reporter.cs b/Reporter.cs index 5c75a443..8606b47b 100644 --- a/Reporter.cs +++ b/Reporter.cs @@ -48,7 +48,7 @@ public void HandleException(string caption, string message, bool canIgnore, Exce }; btnOpenLog.Click += (sender, args) => { - if (!App.Logger.OpenLogFile()) { + if (!OpenLogFile()) { FormMessage.Error("Error Log", "Cannot open error log.", FormMessage.OK); } }; @@ -61,6 +61,16 @@ public void HandleException(string caption, string message, bool canIgnore, Exce }); } + private static bool OpenLogFile() { + try { + using (Process.Start(App.Logger.LogFilePath)) {} + } catch (Exception) { + return false; + } + + return true; + } + public sealed class ExpandedLogException : Exception { private readonly string details; 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 @@ 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 @@ 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 @@ private static T Validate<T>(T? obj, string name) where T : class { } public sealed class AppBuilder { - public AppStartup? Startup { get; set; } - public IAppLogger? Logger { get; set; } + public IAppSetup? Setup { get; set; } public IAppErrorHandler? ErrorHandler { get; set; } public IAppSystemHandler? SystemHandler { get; set; } - public IAppDialogHandler? DialogHandler { get; set; } - public IAppUserConfiguration? UserConfiguration { get; set; } + public IAppMessageDialogs? MessageDialogs { get; set; } + public IAppFileDialogs? FileDialogs { get; set; } internal static AppBuilder? Instance { get; private set; } - internal void Build() { + internal Lib.AppLauncher Build() { Instance = this; App.Initialize(); Instance = null; + return App.Launch; } } } 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 @@ namespace TweetLib.Core.Application { public interface IAppUserConfiguration { - string CustomBrowserCSS { get; } - string CustomNotificationCSS { get; } + string? CustomBrowserCSS { get; } + string? CustomNotificationCSS { get; } string? DismissedUpdate { get; } bool ExpandLinksOnHover { get; } bool FirstRun { get; } @@ -20,6 +20,7 @@ public interface IAppUserConfiguration { int CalendarFirstDay { get; } string TranslationTarget { get; } ImageQuality TwitterImageQuality { get; } + bool UseSystemProxyForAllConnections { get; } event EventHandler MuteToggled; event EventHandler OptionsDialogClosed; 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 @@ internal class BaseContextMenu : IContextMenuHandler { public BaseContextMenu(IBrowserComponent browser) { this.browser = browser; - this.fileDownloadManager = new FileDownloadManager(browser); + this.fileDownloadManager = new FileDownloadManager(browser.FileDownloader); } public virtual void Show(IContextMenuBuilder menu, Context context) { if (context.Selection is { Editable: false } selection) { AddSearchSelectionItems(menu, selection.Text); menu.AddSeparator(); - menu.AddAction("Apply ROT13", () => App.DialogHandler.Information("ROT13", StringUtils.ConvertRot13(selection.Text), Dialogs.OK)); + menu.AddAction("Apply ROT13", () => App.MessageDialogs.Information("ROT13", StringUtils.ConvertRot13(selection.Text))); menu.AddSeparator(); } @@ -32,13 +30,13 @@ public virtual void Show(IContextMenuBuilder menu, Context context) { Match match = TwitterUrls.RegexAccount.Match(link.CopyUrl); if (match.Success) { - menu.AddAction(TextOpen("account"), OpenLink(link.Url)); - menu.AddAction(TextCopy("account"), CopyText(link.CopyUrl)); - menu.AddAction("Copy account username", CopyText(match.Groups[1].Value)); + AddOpenAction(menu, TextOpen("account"), link.Url); + AddCopyAction(menu, TextCopy("account"), link.CopyUrl); + AddCopyAction(menu, "Copy account username", match.Groups[1].Value); } else { - menu.AddAction(TextOpen("link"), OpenLink(link.Url)); - menu.AddAction(TextCopy("link"), CopyText(link.CopyUrl)); + AddOpenAction(menu, TextOpen("link"), link.Url); + AddCopyAction(menu, TextCopy("link"), link.CopyUrl); } menu.AddSeparator(); @@ -49,24 +47,37 @@ public virtual void Show(IContextMenuBuilder menu, Context context) { switch (media.MediaType) { case Media.Type.Image: - menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url)); - menu.AddAction(TextOpen("image"), OpenLink(media.Url)); - menu.AddAction(TextCopy("image"), CopyText(media.Url)); - menu.AddAction("Copy image", () => fileDownloadManager.CopyImage(media.Url)); - menu.AddAction(TextSave("image"), () => fileDownloadManager.SaveImages(new string[] { media.Url }, tweet?.MediaAuthor)); + if (fileDownloadManager.SupportsViewingImage) { + menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url)); + } - var imageUrls = tweet?.ImageUrls; - if (imageUrls?.Length > 1) { - menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor)); + AddOpenAction(menu, TextOpen("image"), media.Url); + AddCopyAction(menu, TextCopy("image"), media.Url); + + if (fileDownloadManager.SupportsCopyingImage) { + menu.AddAction("Copy image", () => fileDownloadManager.CopyImage(media.Url)); + } + + if (fileDownloadManager.SupportsFileSaving) { + menu.AddAction(TextSave("image"), () => fileDownloadManager.SaveImages(new string[] { media.Url }, tweet?.MediaAuthor)); + + var imageUrls = tweet?.ImageUrls; + if (imageUrls?.Length > 1) { + menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor)); + } } menu.AddSeparator(); break; case Media.Type.Video: - menu.AddAction(TextOpen("video"), OpenLink(media.Url)); - menu.AddAction(TextCopy("video"), CopyText(media.Url)); - menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor)); + AddOpenAction(menu, TextOpen("video"), media.Url); + AddCopyAction(menu, TextCopy("video"), media.Url); + + if (fileDownloadManager.SupportsFileSaving) { + menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor)); + } + menu.AddSeparator(); break; } @@ -74,22 +85,26 @@ public virtual void Show(IContextMenuBuilder menu, Context context) { } protected virtual void AddSearchSelectionItems(IContextMenuBuilder menu, string selectedText) { - menu.AddAction("Search in browser", () => { - App.SystemHandler.SearchText(selectedText); - DeselectAll(); - }); + if (App.SystemHandler.SearchText is {} searchText) { + menu.AddAction("Search in browser", () => { + searchText(selectedText); + DeselectAll(); + }); + } } protected void DeselectAll() { browser.RunScript("gen:deselect", "window.getSelection().removeAllRanges()"); } - protected static Action OpenLink(string url) { - return () => App.SystemHandler.OpenBrowser(url); + protected static void AddOpenAction(IContextMenuBuilder menu, string title, string url) { + menu.AddAction(title, () => App.SystemHandler.OpenBrowser(url)); } - protected static Action CopyText(string text) { - return () => App.SystemHandler.CopyText(text); + protected static void AddCopyAction(IContextMenuBuilder menu, string title, string textToCopy) { + if (App.SystemHandler.CopyText is {} copyText) { + menu.AddAction(title, () => copyText(textToCopy)); + } } } } 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.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.Utils.Static; namespace TweetLib.Core.Features { + [SuppressMessage("ReSharper", "MemberCanBeInternal")] public sealed class FileDownloadManager { - private readonly IBrowserComponent browser; + public bool SupportsViewingImage => App.SystemHandler.OpenAssociatedProgram != null; + public bool SupportsCopyingImage => App.SystemHandler.CopyImageFromFile != null; + public bool SupportsFileSaving => App.FileDialogs != null; - internal FileDownloadManager(IBrowserComponent browser) { - this.browser = browser; + private readonly IFileDownloader fileDownloader; + + internal FileDownloadManager(IFileDownloader fileDownloader) { + this.fileDownloader = fileDownloader; } private void DownloadTempImage(string url, Action<string> process) { string? staticFileName = TwitterUrls.GetImageFileName(url); - string file = Path.Combine(browser.FileDownloader.CacheFolder, staticFileName ?? Path.GetRandomFileName()); + string file = Path.Combine(fileDownloader.CacheFolder, staticFileName ?? Path.GetRandomFileName()); if (staticFileName != null && FileUtils.FileExistsAndNotEmpty(file)) { process(file); @@ -27,14 +33,18 @@ void OnSuccess() { } static void OnFailure(Exception ex) { - App.DialogHandler.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, Dialogs.OK); + App.MessageDialogs.Error("Image Download", "An error occurred while downloading the image: " + ex.Message); } - browser.FileDownloader.DownloadFile(url, file, OnSuccess, OnFailure); + fileDownloader.DownloadFile(url, file, OnSuccess, OnFailure); } } public void ViewImage(string url) { + if (App.SystemHandler.OpenAssociatedProgram == null) { + return; + } + DownloadTempImage(url, static path => { string ext = Path.GetExtension(path); @@ -42,16 +52,24 @@ public void ViewImage(string url) { App.SystemHandler.OpenAssociatedProgram(path); } else { - App.DialogHandler.Error("Image Download", "Unknown image file extension: " + ext, Dialogs.OK); + App.MessageDialogs.Error("Image Download", "Unknown image file extension: " + ext); } }); } public void CopyImage(string url) { - DownloadTempImage(url, App.SystemHandler.CopyImageFromFile); + if (App.SystemHandler.CopyImageFromFile is {} copyImageFromFile) { + DownloadTempImage(url, new Action<string>(copyImageFromFile)); + } } public void SaveImages(string[] urls, string? author) { + var fileDialogs = App.FileDialogs; + if (fileDialogs == null) { + App.MessageDialogs.Error("Image Download", "Saving files is not supported!"); + return; + } + if (urls.Length == 0) { return; } @@ -70,26 +88,32 @@ public void SaveImages(string[] urls, string? author) { Filters = new [] { new FileDialogFilter(oneImage ? "Image" : "Images", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) } }; - App.DialogHandler.SaveFile(settings, path => { + fileDialogs.SaveFile(settings, path => { static void OnFailure(Exception ex) { - App.DialogHandler.Error("Image Download", "An error occurred while downloading the image: " + ex.Message, Dialogs.OK); + App.MessageDialogs.Error("Image Download", "An error occurred while downloading the image: " + ex.Message); } if (oneImage) { - browser.FileDownloader.DownloadFile(firstImageLink, path, null, OnFailure); + fileDownloader.DownloadFile(firstImageLink, path, null, OnFailure); } else { string pathBase = Path.ChangeExtension(path, null); string pathExt = Path.GetExtension(path); for (int index = 0; index < urls.Length; index++) { - browser.FileDownloader.DownloadFile(urls[index], $"{pathBase} {index + 1}{pathExt}", null, OnFailure); + fileDownloader.DownloadFile(urls[index], $"{pathBase} {index + 1}{pathExt}", null, OnFailure); } } }); } public void SaveVideo(string url, string? author) { + var fileDialogs = App.FileDialogs; + if (fileDialogs == null) { + App.MessageDialogs.Error("Video Download", "Saving files is not supported!"); + return; + } + string? filename = TwitterUrls.GetFileNameFromUrl(url); string? ext = Path.GetExtension(filename); @@ -100,12 +124,12 @@ public void SaveVideo(string url, string? author) { Filters = new [] { new FileDialogFilter("Video", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) } }; - App.DialogHandler.SaveFile(settings, path => { + fileDialogs.SaveFile(settings, path => { static void OnError(Exception ex) { - App.DialogHandler.Error("Video Download", "An error occurred while downloading the video: " + ex.Message, Dialogs.OK); + App.MessageDialogs.Error("Video Download", "An error occurred while downloading the video: " + ex.Message); } - browser.FileDownloader.DownloadFile(url, path, null, OnError); + fileDownloader.DownloadFile(url, path, null, OnError); }); } } 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 @@ public class Tweet : NotificationBrowser { private readonly PluginManager pluginManager; protected Tweet(IBrowserComponent browserComponent, INotificationInterface notificationInterface, ICommonInterface commonInterface, PluginManager pluginManager, Func<NotificationBrowser, BrowserSetup> setup) : base(browserComponent, setup) { - this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface)); this.browserComponent.PageLoadEnd += BrowserComponentOnPageLoadEnd; + this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface)); this.notificationInterface = notificationInterface; this.pluginManager = pluginManager; @@ -56,10 +56,10 @@ public override void Show(IContextMenuBuilder menu, Context context) { menu.AddSeparator(); if (context.Notification is {} notification) { - menu.AddAction("Copy tweet address", CopyText(notification.TweetUrl)); + AddCopyAction(menu, "Copy tweet address", notification.TweetUrl); if (!string.IsNullOrEmpty(notification.QuoteUrl)) { - menu.AddAction("Copy quoted tweet address", CopyText(notification.QuoteUrl!)); + AddCopyAction(menu, "Copy quoted tweet address", notification.QuoteUrl!); } menu.AddSeparator(); 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 TweetLib.Core.Systems.Configuration; namespace TweetLib.Core.Features.Plugins.Config { - public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig { - public T Instance { get; } + internal sealed class PluginConfigInstance : IConfigInstance { + public PluginConfig Instance { get; } private readonly string filename; - public PluginConfigInstance(string filename, T instance) { - this.filename = filename; + public PluginConfigInstance(string filename, PluginConfig instance) { this.Instance = instance; + this.filename = filename; } public void Load() { @@ -58,7 +58,7 @@ public void Reload() { public void Reset() { try { File.Delete(filename); - Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins); + Instance.ResetToDefault(); } catch (Exception e) { OnException("Could not delete the plugin configuration file.", e); return; 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 @@ namespace TweetLib.Core.Features.Plugins { public sealed class PluginManager { - public string CustomPluginFolder => Path.Combine(App.PluginPath, PluginGroup.Custom.GetSubFolder()); - public IEnumerable<Plugin> Plugins => plugins; public IEnumerable<InjectedString> NotificationInjections => bridge.NotificationInjections; - public IPluginConfig Config { get; } + public PluginConfig Config { get; } + public string PluginFolder { get; } public string PluginDataFolder { get; } public event EventHandler<PluginErrorEventArgs>? Reloaded; @@ -27,13 +26,18 @@ public sealed class PluginManager { private readonly HashSet<Plugin> plugins = new (); - public PluginManager(IPluginConfig config, string pluginDataFolder) { + public PluginManager(PluginConfig config, string pluginFolder, string pluginDataFolder) { this.Config = config; this.Config.PluginChangedState += Config_PluginChangedState; + this.PluginFolder = pluginFolder; this.PluginDataFolder = pluginDataFolder; this.bridge = new PluginBridge(this); } + public string GetPluginFolder(PluginGroup group) { + return Path.Combine(PluginFolder, group.GetSubFolder()); + } + internal void Register(PluginEnvironment environment, IBrowserComponent browserComponent) { browserComponent.AttachBridgeObject("$TDP", bridge); @@ -47,7 +51,7 @@ public void Reload() { var errors = new List<string>(1); - foreach (var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(App.PluginPath, PluginDataFolder, group))) { + foreach (var result in PluginGroups.All.SelectMany(group => PluginLoader.AllInFolder(PluginFolder, PluginDataFolder, group))) { if (result.HasValue) { plugins.Add(result.Value); } @@ -111,7 +115,7 @@ public void ConfigurePlugin(Plugin plugin) { browserExecutor.RunFunction("TDPF_configurePlugin", plugin); } else if (plugin.HasConfig) { - App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(PluginFolder.Data)); + App.SystemHandler.OpenFileExplorer(File.Exists(plugin.ConfigPath) ? plugin.ConfigPath : plugin.GetPluginFolder(Enums.PluginFolder.Data)); } } } 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 @@ public PluginSchemeHandler(CachingResourceProvider<T> resourceProvider, PluginMa } } - return resourceProvider.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath); + return new SchemeResource.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath); } - private T DoReadRootFile(int identifier, string[] segments) { + private SchemeResource DoReadRootFile(int identifier, string[] segments) { string path = string.Join("/", segments, 1, segments.Length - 1); Plugin? plugin = bridge.GetPluginFromToken(identifier); string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(PluginFolder.Root, path); - return fullPath.Length == 0 ? resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.") : resourceProvider.CachedFile(fullPath); + return fullPath.Length == 0 ? PathMustBeRelativeToRoot : resourceCache.ReadFile(fullPath); } } } 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 @@ 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.Events; using TweetLib.Core.Features.Twitter; using TweetLib.Core.Resources; -using TweetLib.Core.Systems.Dialogs; using TweetLib.Core.Systems.Updates; using TweetLib.Utils.Static; using Version = TweetDuck.Version; @@ -21,22 +20,18 @@ public sealed class TweetDeckBrowser : BaseBrowser<TweetDeckBrowser> { private const string BackgroundColorOverride = "setTimeout(function f(){let h=document.head;if(!h){setTimeout(f,5);return;}let e=document.createElement('style');e.innerHTML='body,body::before{background:#1c6399!important;margin:0}';h.appendChild(e);},1)"; public TweetDeckFunctions Functions { get; } - public FileDownloadManager FileDownloadManager => new (browserComponent); + public FileDownloadManager FileDownloadManager => new (browserComponent.FileDownloader); private readonly ISoundNotificationHandler soundNotificationHandler; private readonly PluginManager pluginManager; - private readonly UpdateChecker updateChecker; private bool isBrowserReady; private bool ignoreUpdateCheckError; private string? prevSoundNotificationPath = null; - public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface tweetDeckInterface, TweetDeckExtraContext extraContext, ISoundNotificationHandler soundNotificationHandler, PluginManager pluginManager, UpdateChecker updateChecker) : base(browserComponent, CreateSetupObject) { + public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface tweetDeckInterface, TweetDeckExtraContext extraContext, ISoundNotificationHandler soundNotificationHandler, PluginManager pluginManager, UpdateChecker? updateChecker = null) : base(browserComponent, CreateSetupObject) { this.Functions = new TweetDeckFunctions(this.browserComponent); - this.browserComponent.AttachBridgeObject("$TD", new TweetDeckBridgeObject(tweetDeckInterface, this, extraContext)); - this.browserComponent.AttachBridgeObject("$TDU", updateChecker.InteractionManager.BridgeObject); - this.soundNotificationHandler = soundNotificationHandler; this.pluginManager = pluginManager; @@ -45,13 +40,17 @@ public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface this.pluginManager.Executed += pluginManager_Executed; this.pluginManager.Reload(); - this.updateChecker = updateChecker; - this.updateChecker.CheckFinished += updateChecker_CheckFinished; - this.browserComponent.BrowserLoaded += browserComponent_BrowserLoaded; this.browserComponent.PageLoadStart += browserComponent_PageLoadStart; this.browserComponent.PageLoadEnd += browserComponent_PageLoadEnd; + this.browserComponent.AttachBridgeObject("$TD", new TweetDeckBridgeObject(tweetDeckInterface, this, extraContext)); + + if (updateChecker != null) { + updateChecker.CheckFinished += updateChecker_CheckFinished; + this.browserComponent.AttachBridgeObject("$TDU", updateChecker.InteractionManager.BridgeObject); + } + App.UserConfiguration.MuteToggled += UserConfiguration_GeneralEventHandler; App.UserConfiguration.OptionsDialogClosed += UserConfiguration_GeneralEventHandler; App.UserConfiguration.SoundNotificationChanged += UserConfiguration_SoundNotificationChanged; @@ -105,7 +104,7 @@ private void browserComponent_PageLoadEnd(object sender, PageLoadEventArgs e) { private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) { if (e.HasErrors) { - App.DialogHandler.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), Dialogs.OK); + App.MessageDialogs.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors)); } if (isBrowserReady) { @@ -115,11 +114,13 @@ private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) { private void pluginManager_Executed(object sender, PluginErrorEventArgs e) { if (e.HasErrors) { - App.DialogHandler.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), Dialogs.OK); + App.MessageDialogs.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors)); } } private void updateChecker_CheckFinished(object sender, UpdateCheckEventArgs e) { + var updateChecker = (UpdateChecker) sender; + e.Result.Handle(update => { string tag = update.VersionTag; @@ -201,14 +202,14 @@ public override void Show(IContextMenuBuilder menu, Context context) { base.Show(menu, context); if (context.Selection == null && context.Tweet is {} tweet) { - menu.AddAction("Open tweet in browser", OpenLink(tweet.Url)); - menu.AddAction("Copy tweet address", CopyText(tweet.Url)); + AddOpenAction(menu, "Open tweet in browser", tweet.Url); + AddCopyAction(menu, "Copy tweet address", tweet.Url); menu.AddAction("Screenshot tweet to clipboard", () => owner.Functions.TriggerTweetScreenshot(tweet.ColumnId, tweet.ChirpId)); menu.AddSeparator(); if (!string.IsNullOrEmpty(tweet.QuoteUrl)) { - menu.AddAction("Open quoted tweet in browser", OpenLink(tweet.QuoteUrl!)); - menu.AddAction("Copy quoted tweet address", CopyText(tweet.QuoteUrl!)); + AddOpenAction(menu, "Open quoted tweet in browser", tweet.QuoteUrl!); + AddCopyAction(menu, "Copy quoted tweet address", tweet.QuoteUrl!); menu.AddSeparator(); } } 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 @@ public T Resolve(Uri uri) { }; if (rootPath == null) { - return resourceProvider.Status(HttpStatusCode.NotFound, "Invalid URL."); + return InvalidUrl; } string filePath = FileUtils.ResolveRelativePathSafely(rootPath, uri.AbsolutePath.TrimStart('/')); - return filePath.Length == 0 ? resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the root folder.") : resourceProvider.CachedFile(filePath); + return filePath.Length == 0 ? PathMustBeRelativeToRoot : resourceCache.ReadFile(filePath); } } } 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 @@ namespace TweetLib.Core { public static class Lib { public const string BrandName = "TweetDuck"; + public const string IssueTrackerUrl = "https://github.com/chylex/TweetDuck/issues"; public static CultureInfo Culture { get; } = CultureInfo.CurrentCulture; - public static void Initialize(AppBuilder app) { + public delegate void AppLauncher(); + + public static AppLauncher Initialize(AppBuilder app) { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; @@ -20,7 +23,7 @@ public static void Initialize(AppBuilder app) { CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions #endif - app.Build(); + return app.Build(); } } } 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 TweetLib.Utils.Serialization; namespace TweetLib.Core.Systems.Configuration { - public sealed class FileConfigInstance<T> : IConfigInstance<T> where T : BaseConfig { + internal sealed class FileConfigInstance<T> : IConfigInstance where T : IConfigObject<T> { public T Instance { get; } - public SimpleObjectSerializer<T> Serializer { get; } + + private readonly SimpleObjectSerializer<T> serializer; private readonly string filenameMain; private readonly string filenameBackup; private readonly string identifier; - public FileConfigInstance(string filename, T instance, string identifier) { + public FileConfigInstance(string filename, T instance, string identifier, TypeConverterRegistry converterRegistry) { + this.Instance = instance; + this.serializer = new SimpleObjectSerializer<T>(converterRegistry); + this.filenameMain = filename ?? throw new ArgumentNullException(nameof(filename), "Config file name must not be null!"); this.filenameBackup = filename + ".bak"; this.identifier = identifier; - - this.Instance = instance; - this.Serializer = new SimpleObjectSerializer<T>(); } private void LoadInternal(bool backup) { - Serializer.Read(backup ? filenameBackup : filenameMain, Instance); + serializer.Read(backup ? filenameBackup : filenameMain, Instance); } public void Load() { @@ -63,7 +64,7 @@ public void Save() { File.Move(filenameMain, filenameBackup); } - Serializer.Write(filenameMain, Instance); + serializer.Write(filenameMain, Instance); } catch (SerializationSoftException e) { OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e); } catch (Exception e) { @@ -76,7 +77,7 @@ public void Reload() { LoadInternal(false); } catch (FileNotFoundException) { try { - Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>()); + serializer.Write(filenameMain, Instance.ConstructWithDefaults()); LoadInternal(false); } catch (Exception e) { OnException($"Could not regenerate the configuration file for {identifier}.", e); 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 @@ internal void BeginSilentDownload() { 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 @@ private static string UnescapeStream(StreamReader reader) { return build.Append(data.Substring(index)).ToString(); } + private readonly TypeConverterRegistry converterRegistry; private readonly Dictionary<string, PropertyInfo> props; - private readonly Dictionary<Type, ITypeConverter> converters; - public SimpleObjectSerializer() { + public SimpleObjectSerializer(TypeConverterRegistry converterRegistry) { + this.converterRegistry = converterRegistry; this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(static prop => prop.CanWrite).ToDictionary(static prop => prop.Name); - this.converters = new Dictionary<Type, ITypeConverter>(); - } - - public void RegisterTypeConverter(Type type, ITypeConverter converter) { - converters[type] = converter; } public void Write(string file, T obj) { @@ -69,14 +65,10 @@ public void Write(string file, T obj) { using (StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))) { foreach (KeyValuePair<string, PropertyInfo> prop in props) { - Type type = prop.Value.PropertyType; - object value = prop.Value.GetValue(obj); + var type = prop.Value.PropertyType; + var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance; - if (!converters.TryGetValue(type, out ITypeConverter serializer)) { - serializer = ClrTypeConverter.Instance; - } - - if (serializer.TryWriteType(type, value, out string? converted)) { + if (converter.TryWriteType(type, prop.Value.GetValue(obj), out string? converted)) { if (converted != null) { writer.Write(prop.Key); writer.Write(' '); @@ -140,11 +132,10 @@ public void Read(string file, T obj) { string value = UnescapeLine(line.Substring(space + 1)); if (props.TryGetValue(property, out PropertyInfo info)) { - if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)) { - serializer = ClrTypeConverter.Instance; - } + var type = info.PropertyType; + var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance; - if (serializer.TryReadType(info.PropertyType, value, out object? converted)) { + if (converter.TryReadType(type, value, out object? converted)) { info.SetValue(obj, converted); } else { 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; + } + } +}