mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-04-21 06:15:47 +02:00
Work on abstracting app logic and making some implementation optional
This commit is contained in:
parent
ec7827df24
commit
fa534f9eb3
Application
Browser
Configuration
Controls
Dialogs
Management
Plugins
Program.csReporter.csTweetDuck.csprojUtils
lib
TweetLib.Browser
Base
Interfaces
Request
TweetLib.Core
App.cs
Application
AppException.csAppStartup.csIAppDialogHandler.csIAppFileDialogs.csIAppLogger.csIAppMessageDialogs.csIAppSetup.csIAppSystemHandler.csIAppUserConfiguration.cs
Features
BaseContextMenu.csBaseResourceRequestHandler.csFileDownloadManager.cs
Lib.csNotifications
Plugins
TweetDeck
Resources
Systems
TweetLib.Utils
@ -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();
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
Application/MessageDialogs.cs
Normal file
15
Application/MessageDialogs.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using TweetDuck.Dialogs;
|
||||
using TweetDuck.Management;
|
||||
using TweetLib.Core.Application;
|
||||
|
||||
namespace TweetDuck.Application {
|
||||
sealed class MessageDialogs : IAppMessageDialogs {
|
||||
public void Information(string caption, string text, string buttonAccept) {
|
||||
FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept));
|
||||
}
|
||||
|
||||
public void Error(string caption, string text, string buttonAccept) {
|
||||
FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
Browser/Adapters/CefSchemeResourceVisitor.cs
Normal file
39
Browser/Adapters/CefSchemeResourceVisitor.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using CefSharp;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
using TweetLib.Browser.Request;
|
||||
|
||||
namespace TweetDuck.Browser.Adapters {
|
||||
internal sealed class CefSchemeResourceVisitor : ISchemeResourceVisitor<IResourceHandler> {
|
||||
public static CefSchemeResourceVisitor Instance { get; } = new CefSchemeResourceVisitor();
|
||||
|
||||
private static readonly SchemeResource.Status FileIsEmpty = new SchemeResource.Status(HttpStatusCode.NoContent, "File is empty.");
|
||||
|
||||
private CefSchemeResourceVisitor() {}
|
||||
|
||||
public IResourceHandler Status(SchemeResource.Status status) {
|
||||
var handler = CreateHandler(Encoding.UTF8.GetBytes(status.Message));
|
||||
handler.StatusCode = (int) status.Code;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public IResourceHandler File(SchemeResource.File file) {
|
||||
byte[] contents = file.Contents;
|
||||
if (contents.Length == 0) {
|
||||
return Status(FileIsEmpty); // FromByteArray crashes CEF internals with no contents
|
||||
}
|
||||
|
||||
var handler = CreateHandler(contents);
|
||||
handler.MimeType = Cef.GetMimeType(file.Extension);
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static ResourceHandler CreateHandler(byte[] bytes) {
|
||||
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
|
||||
handler.Headers.Set("Access-Control-Allow-Origin", "*");
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
187
Program.cs
187
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();
|
||||
|
12
Reporter.cs
12
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;
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
|
@ -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() {}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
38
lib/TweetLib.Browser/Request/SchemeResource.cs
Normal file
38
lib/TweetLib.Browser/Request/SchemeResource.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Net;
|
||||
using TweetLib.Browser.Interfaces;
|
||||
|
||||
namespace TweetLib.Browser.Request {
|
||||
public abstract class SchemeResource {
|
||||
private SchemeResource() {}
|
||||
|
||||
public abstract T Visit<T>(ISchemeResourceVisitor<T> visitor);
|
||||
|
||||
public sealed class Status : SchemeResource {
|
||||
public HttpStatusCode Code { get; }
|
||||
public string Message { get; }
|
||||
|
||||
public Status(HttpStatusCode code, string message) {
|
||||
Code = code;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public override T Visit<T>(ISchemeResourceVisitor<T> visitor) {
|
||||
return visitor.Status(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class File : SchemeResource {
|
||||
public byte[] Contents { get; }
|
||||
public string Extension { get; }
|
||||
|
||||
public File(byte[] contents, string extension) {
|
||||
Contents = contents;
|
||||
Extension = extension;
|
||||
}
|
||||
|
||||
public override T Visit<T>(ISchemeResourceVisitor<T> visitor) {
|
||||
return visitor.File(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
8
lib/TweetLib.Core/Application/IAppFileDialogs.cs
Normal file
8
lib/TweetLib.Core/Application/IAppFileDialogs.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
using TweetLib.Core.Systems.Dialogs;
|
||||
|
||||
namespace TweetLib.Core.Application {
|
||||
public interface IAppFileDialogs {
|
||||
void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
8
lib/TweetLib.Core/Application/IAppMessageDialogs.cs
Normal file
8
lib/TweetLib.Core/Application/IAppMessageDialogs.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using TweetLib.Core.Systems.Dialogs;
|
||||
|
||||
namespace TweetLib.Core.Application {
|
||||
public interface IAppMessageDialogs {
|
||||
void Information(string caption, string text, string buttonAccept = Dialogs.OK);
|
||||
void Error(string caption, string text, string buttonAccept = Dialogs.OK);
|
||||
}
|
||||
}
|
20
lib/TweetLib.Core/Application/IAppSetup.cs
Normal file
20
lib/TweetLib.Core/Application/IAppSetup.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using TweetLib.Core.Features.Plugins;
|
||||
using TweetLib.Core.Resources;
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
|
||||
namespace TweetLib.Core.Application {
|
||||
public interface IAppSetup {
|
||||
bool IsPortable { get; }
|
||||
bool IsDebugLogging { get; }
|
||||
string? CustomDataFolder { get; }
|
||||
string? ResourceRewriteRules { get; }
|
||||
|
||||
ConfigManager CreateConfigManager(string storagePath);
|
||||
|
||||
bool TryLockDataFolder(string lockFile);
|
||||
|
||||
void BeforeLaunch();
|
||||
|
||||
void Launch(ResourceCache resourceCache, PluginManager pluginManager);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
43
lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs
Normal file
43
lib/TweetLib.Core/Features/Plugins/Config/PluginConfig.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TweetLib.Core.Features.Plugins.Events;
|
||||
using TweetLib.Core.Systems.Configuration;
|
||||
|
||||
namespace TweetLib.Core.Features.Plugins.Config {
|
||||
public sealed class PluginConfig : IConfigObject<PluginConfig> {
|
||||
internal IEnumerable<string> DisabledPlugins => disabled;
|
||||
|
||||
public event EventHandler<PluginChangedStateEventArgs>? PluginChangedState;
|
||||
|
||||
private readonly HashSet<string> defaultDisabled;
|
||||
private readonly HashSet<string> disabled;
|
||||
|
||||
public PluginConfig(IEnumerable<string> defaultDisabled) {
|
||||
this.defaultDisabled = new HashSet<string>(defaultDisabled);
|
||||
this.disabled = new HashSet<string>(this.defaultDisabled);
|
||||
}
|
||||
|
||||
public PluginConfig ConstructWithDefaults() {
|
||||
return new PluginConfig(defaultDisabled);
|
||||
}
|
||||
|
||||
internal void Reset(IEnumerable<string> newDisabledPlugins) {
|
||||
disabled.Clear();
|
||||
disabled.UnionWith(newDisabledPlugins);
|
||||
}
|
||||
|
||||
internal void ResetToDefault() {
|
||||
Reset(defaultDisabled);
|
||||
}
|
||||
|
||||
public void SetEnabled(Plugin plugin, bool enabled) {
|
||||
if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))) {
|
||||
PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(Plugin plugin) {
|
||||
return !disabled.Contains(plugin.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 + '"')) + "]";
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
lib/TweetLib.Core/Resources/ResourceCache.cs
Normal file
38
lib/TweetLib.Core/Resources/ResourceCache.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using TweetLib.Browser.Request;
|
||||
using IOFile = System.IO.File;
|
||||
|
||||
namespace TweetLib.Core.Resources {
|
||||
public sealed class ResourceCache {
|
||||
private readonly Dictionary<string, SchemeResource> cache = new ();
|
||||
|
||||
public void ClearCache() {
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
internal SchemeResource ReadFile(string path) {
|
||||
string key = new Uri(path).LocalPath;
|
||||
|
||||
if (cache.TryGetValue(key, out var cachedResource)) {
|
||||
return cachedResource;
|
||||
}
|
||||
|
||||
SchemeResource resource;
|
||||
try {
|
||||
resource = new SchemeResource.File(IOFile.ReadAllBytes(path), Path.GetExtension(path));
|
||||
} catch (FileNotFoundException) {
|
||||
resource = new SchemeResource.Status(HttpStatusCode.NotFound, "File not found.");
|
||||
} catch (DirectoryNotFoundException) {
|
||||
resource = new SchemeResource.Status(HttpStatusCode.NotFound, "Directory not found.");
|
||||
} catch (Exception e) {
|
||||
resource = new SchemeResource.Status(HttpStatusCode.InternalServerError, e.Message);
|
||||
}
|
||||
|
||||
cache[key] = resource;
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
142
lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
Normal file
142
lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using TweetLib.Core.Application;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
using TweetLib.Utils.Data;
|
||||
using TweetLib.Utils.Serialization;
|
||||
using TweetLib.Utils.Serialization.Converters;
|
||||
using TweetLib.Utils.Static;
|
||||
|
||||
namespace TweetLib.Core.Systems.Configuration {
|
||||
public abstract class ConfigManager {
|
||||
protected static TypeConverterRegistry ConverterRegistry { get; } = new ();
|
||||
|
||||
static ConfigManager() {
|
||||
ConverterRegistry.Register(typeof(WindowState), new BasicTypeConverter<WindowState> {
|
||||
ConvertToString = static value => $"{(value.IsMaximized ? 'M' : '_')}{value.Bounds.X} {value.Bounds.Y} {value.Bounds.Width} {value.Bounds.Height}",
|
||||
ConvertToObject = static value => {
|
||||
int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
|
||||
|
||||
return new WindowState {
|
||||
Bounds = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
|
||||
IsMaximized = value[0] == 'M'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ConverterRegistry.Register(typeof(Point), new BasicTypeConverter<Point> {
|
||||
ConvertToString = static value => $"{value.X} {value.Y}",
|
||||
ConvertToObject = static value => {
|
||||
int[] elements = StringUtils.ParseInts(value, ' ');
|
||||
return new Point(elements[0], elements[1]);
|
||||
}
|
||||
});
|
||||
|
||||
ConverterRegistry.Register(typeof(Size), new BasicTypeConverter<Size> {
|
||||
ConvertToString = static value => $"{value.Width} {value.Height}",
|
||||
ConvertToObject = static value => {
|
||||
int[] elements = StringUtils.ParseInts(value, ' ');
|
||||
return new Size(elements[0], elements[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public string UserPath { get; }
|
||||
public string SystemPath { get; }
|
||||
public string PluginsPath { get; }
|
||||
|
||||
public event EventHandler? ProgramRestartRequested;
|
||||
|
||||
internal IAppUserConfiguration User { get; }
|
||||
internal PluginConfig Plugins { get; }
|
||||
|
||||
protected ConfigManager(string storagePath, IAppUserConfiguration user, PluginConfig plugins) {
|
||||
UserPath = Path.Combine(storagePath, "TD_UserConfig.cfg");
|
||||
SystemPath = Path.Combine(storagePath, "TD_SystemConfig.cfg");
|
||||
PluginsPath = Path.Combine(storagePath, "TD_PluginConfig.cfg");
|
||||
|
||||
User = user;
|
||||
Plugins = plugins;
|
||||
}
|
||||
|
||||
public abstract void LoadAll();
|
||||
public abstract void SaveAll();
|
||||
public abstract void ReloadAll();
|
||||
|
||||
internal void Save(IConfigObject instance) {
|
||||
this.GetInstanceInfo(instance).Save();
|
||||
}
|
||||
|
||||
internal void Reset(IConfigObject instance) {
|
||||
this.GetInstanceInfo(instance).Reset();
|
||||
}
|
||||
|
||||
public void TriggerProgramRestartRequested() {
|
||||
ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected abstract IConfigInstance GetInstanceInfo(IConfigObject instance);
|
||||
}
|
||||
|
||||
public sealed class ConfigManager<TUser, TSystem> : ConfigManager where TUser : class, IAppUserConfiguration, IConfigObject<TUser> where TSystem : class, IConfigObject<TSystem> {
|
||||
private new TUser User { get; }
|
||||
private TSystem System { get; }
|
||||
|
||||
private readonly FileConfigInstance<TUser> infoUser;
|
||||
private readonly FileConfigInstance<TSystem> infoSystem;
|
||||
private readonly PluginConfigInstance infoPlugins;
|
||||
|
||||
public ConfigManager(string storagePath, ConfigObjects<TUser, TSystem> configObjects) : base(storagePath, configObjects.User, configObjects.Plugins) {
|
||||
User = configObjects.User;
|
||||
System = configObjects.System;
|
||||
|
||||
infoUser = new FileConfigInstance<TUser>(UserPath, User, "program options", ConverterRegistry);
|
||||
infoSystem = new FileConfigInstance<TSystem>(SystemPath, System, "system options", ConverterRegistry);
|
||||
infoPlugins = new PluginConfigInstance(PluginsPath, Plugins);
|
||||
}
|
||||
|
||||
public override void LoadAll() {
|
||||
infoUser.Load();
|
||||
infoSystem.Load();
|
||||
infoPlugins.Load();
|
||||
}
|
||||
|
||||
public override void SaveAll() {
|
||||
infoUser.Save();
|
||||
infoSystem.Save();
|
||||
infoPlugins.Save();
|
||||
}
|
||||
|
||||
public override void ReloadAll() {
|
||||
infoUser.Reload();
|
||||
infoSystem.Reload();
|
||||
infoPlugins.Reload();
|
||||
}
|
||||
|
||||
protected override IConfigInstance GetInstanceInfo(IConfigObject instance) {
|
||||
if (instance == User) {
|
||||
return infoUser;
|
||||
}
|
||||
else if (instance == System) {
|
||||
return infoSystem;
|
||||
}
|
||||
else if (instance == Plugins) {
|
||||
return infoPlugins;
|
||||
}
|
||||
else {
|
||||
throw new ArgumentException("Invalid configuration instance: " + instance.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfigManagerExtensions {
|
||||
public static void Save(this IConfigObject instance) {
|
||||
App.ConfigManager.Save(instance);
|
||||
}
|
||||
|
||||
public static void Reset(this IConfigObject instance) {
|
||||
App.ConfigManager.Reset(instance);
|
||||
}
|
||||
}
|
||||
}
|
16
lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs
Normal file
16
lib/TweetLib.Core/Systems/Configuration/ConfigObjects.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using TweetLib.Core.Application;
|
||||
using TweetLib.Core.Features.Plugins.Config;
|
||||
|
||||
namespace TweetLib.Core.Systems.Configuration {
|
||||
public sealed class ConfigObjects<TUser, TSystem> where TUser : IAppUserConfiguration where TSystem : IConfigObject {
|
||||
public TUser User { get; }
|
||||
public TSystem System { get; }
|
||||
public PluginConfig Plugins { get; }
|
||||
|
||||
public ConfigObjects(TUser user, TSystem system, PluginConfig plugins) {
|
||||
User = user;
|
||||
System = system;
|
||||
Plugins = plugins;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
namespace TweetLib.Core.Systems.Configuration {
|
||||
public interface IConfigManager {
|
||||
IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
|
||||
}
|
||||
}
|
7
lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs
Normal file
7
lib/TweetLib.Core/Systems/Configuration/IConfigObject.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace TweetLib.Core.Systems.Configuration {
|
||||
public interface IConfigObject {}
|
||||
|
||||
public interface IConfigObject<T> : IConfigObject where T : IConfigObject<T> {
|
||||
T ConstructWithDefaults();
|
||||
}
|
||||
}
|
57
lib/TweetLib.Core/Systems/Logging/Logger.cs
Normal file
57
lib/TweetLib.Core/Systems/Logging/Logger.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace TweetLib.Core.Systems.Logging {
|
||||
public sealed class Logger {
|
||||
public string LogFilePath { get; }
|
||||
|
||||
private readonly bool debug;
|
||||
|
||||
internal Logger(string logPath, bool debug) {
|
||||
this.LogFilePath = logPath;
|
||||
this.debug = debug;
|
||||
#if DEBUG
|
||||
this.debug = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool Debug(string message) {
|
||||
return debug && Log("DEBUG", message);
|
||||
}
|
||||
|
||||
public bool Info(string message) {
|
||||
return Log("INFO", message);
|
||||
}
|
||||
|
||||
public bool Warn(string message) {
|
||||
return Log("WARN", message);
|
||||
}
|
||||
|
||||
public bool Error(string message) {
|
||||
return Log("ERROR", message);
|
||||
}
|
||||
|
||||
private bool Log(string level, string message) {
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[" + level + "] " + message);
|
||||
#endif
|
||||
|
||||
StringBuilder build = new StringBuilder();
|
||||
|
||||
if (!File.Exists(LogFilePath)) {
|
||||
build.Append("Please, report all issues to: ").Append(Lib.IssueTrackerUrl).Append("\r\n\r\n");
|
||||
}
|
||||
|
||||
build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("] ").Append(level).Append("\r\n");
|
||||
build.Append(message).Append("\r\n\r\n");
|
||||
|
||||
try {
|
||||
File.AppendAllText(LogFilePath, build.ToString(), Encoding.UTF8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
8
lib/TweetLib.Utils/Data/WindowState.cs
Normal file
8
lib/TweetLib.Utils/Data/WindowState.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace TweetLib.Utils.Data {
|
||||
public sealed class WindowState {
|
||||
public Rectangle Bounds { get; set; }
|
||||
public bool IsMaximized { get; set; }
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
16
lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs
Normal file
16
lib/TweetLib.Utils/Serialization/TypeConverterRegistry.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TweetLib.Utils.Serialization {
|
||||
public sealed class TypeConverterRegistry {
|
||||
private readonly Dictionary<Type, ITypeConverter> converters = new ();
|
||||
|
||||
public void Register(Type type, ITypeConverter converter) {
|
||||
converters[type] = converter;
|
||||
}
|
||||
|
||||
public ITypeConverter? TryGet(Type type) {
|
||||
return converters.TryGetValue(type, out var converter) ? converter : null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user