mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-04-29 20:34:04 +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.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using TweetDuck.Dialogs;
|
|
||||||
using TweetDuck.Management;
|
using TweetDuck.Management;
|
||||||
using TweetLib.Core.Application;
|
using TweetLib.Core.Application;
|
||||||
using TweetLib.Core.Systems.Dialogs;
|
using TweetLib.Core.Systems.Dialogs;
|
||||||
|
|
||||||
namespace TweetDuck.Application {
|
namespace TweetDuck.Application {
|
||||||
sealed class DialogHandler : IAppDialogHandler {
|
sealed class FileDialogs : IAppFileDialogs {
|
||||||
public void Information(string caption, string text, string buttonAccept, string buttonCancel = null) {
|
|
||||||
FormManager.RunOnUIThreadAsync(() => FormMessage.Information(caption, text, buttonAccept, buttonCancel));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Error(string caption, string text, string buttonAccept, string buttonCancel = null) {
|
|
||||||
FormManager.RunOnUIThreadAsync(() => FormMessage.Error(caption, text, buttonAccept, buttonCancel));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted) {
|
public void SaveFile(SaveFileDialogSettings settings, Action<string> onAccepted) {
|
||||||
static string FormatFilter(FileDialogFilter filter) {
|
static string FormatFilter(FileDialogFilter filter) {
|
||||||
var builder = new StringBuilder();
|
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.IO;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using TweetDuck.Browser;
|
using TweetDuck.Browser;
|
||||||
using TweetDuck.Configuration;
|
|
||||||
using TweetDuck.Dialogs;
|
using TweetDuck.Dialogs;
|
||||||
using TweetDuck.Management;
|
using TweetDuck.Management;
|
||||||
using TweetLib.Core;
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Application;
|
using TweetLib.Core.Application;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetDuck.Application {
|
namespace TweetDuck.Application {
|
||||||
sealed class SystemHandler : IAppSystemHandler {
|
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) {
|
public void OpenBrowser(string url) {
|
||||||
if (string.IsNullOrWhiteSpace(url)) {
|
if (string.IsNullOrWhiteSpace(url)) {
|
||||||
return;
|
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(() => {
|
FormManager.RunOnUIThreadAsync(() => {
|
||||||
Image image;
|
Image image;
|
||||||
|
|
||||||
@ -105,18 +106,18 @@ public void CopyImageFromFile(string path) {
|
|||||||
|
|
||||||
ClipboardManager.SetImage(image);
|
ClipboardManager.SetImage(image);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
public void CopyText(string text) {
|
public IAppSystemHandler.CopyTextFunc CopyText { get; } = text => {
|
||||||
FormManager.RunOnUIThreadAsync(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
|
FormManager.RunOnUIThreadAsync(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
|
||||||
}
|
};
|
||||||
|
|
||||||
public void SearchText(string text) {
|
public IAppSystemHandler.SearchTextFunc SearchText { get; } = text => {
|
||||||
if (string.IsNullOrWhiteSpace(text)) {
|
if (string.IsNullOrWhiteSpace(text)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormManager.RunOnUIThreadAsync(() => {
|
void PerformSearch() {
|
||||||
var config = Program.Config.User;
|
var config = Program.Config.User;
|
||||||
string searchUrl = config.SearchEngineUrl;
|
string searchUrl = config.SearchEngineUrl;
|
||||||
|
|
||||||
@ -138,15 +139,17 @@ public void SearchText(string text) {
|
|||||||
|
|
||||||
settings.FormClosed += (sender, args) => {
|
settings.FormClosed += (sender, args) => {
|
||||||
if (args.CloseReason == CloseReason.UserClosing && config.SearchEngineUrl != searchUrl) {
|
if (args.CloseReason == CloseReason.UserClosing && config.SearchEngineUrl != searchUrl) {
|
||||||
SearchText(text);
|
PerformSearch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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 {
|
namespace TweetDuck.Browser.Adapters {
|
||||||
internal sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory {
|
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 {
|
settings.RegisterScheme(new CefCustomScheme {
|
||||||
SchemeName = handler.Protocol,
|
SchemeName = handler.Protocol,
|
||||||
IsStandard = false,
|
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;
|
this.handler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
|
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.Plugins;
|
||||||
using TweetLib.Core.Features.TweetDeck;
|
using TweetLib.Core.Features.TweetDeck;
|
||||||
using TweetLib.Core.Resources;
|
using TweetLib.Core.Resources;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using TweetLib.Core.Systems.Updates;
|
using TweetLib.Core.Systems.Updates;
|
||||||
|
|
||||||
namespace TweetDuck.Browser {
|
namespace TweetDuck.Browser {
|
||||||
@ -51,11 +52,12 @@ public bool IsWaiting {
|
|||||||
private readonly FormNotificationTweet notification;
|
private readonly FormNotificationTweet notification;
|
||||||
#pragma warning restore IDE0069 // Disposable fields should be disposed
|
#pragma warning restore IDE0069 // Disposable fields should be disposed
|
||||||
|
|
||||||
private readonly CachingResourceProvider<IResourceHandler> resourceProvider;
|
private readonly ResourceCache resourceCache;
|
||||||
private readonly ITweetDeckInterface tweetDeckInterface;
|
private readonly ITweetDeckInterface tweetDeckInterface;
|
||||||
private readonly PluginManager plugins;
|
private readonly PluginManager plugins;
|
||||||
private readonly UpdateChecker updates;
|
private readonly UpdateChecker updates;
|
||||||
private readonly ContextMenu contextMenu;
|
private readonly ContextMenu contextMenu;
|
||||||
|
private readonly uint windowRestoreMessage;
|
||||||
|
|
||||||
private bool isLoaded;
|
private bool isLoaded;
|
||||||
private FormWindowState prevState;
|
private FormWindowState prevState;
|
||||||
@ -63,12 +65,12 @@ public bool IsWaiting {
|
|||||||
private TweetScreenshotManager notificationScreenshotManager;
|
private TweetScreenshotManager notificationScreenshotManager;
|
||||||
private VideoPlayer videoPlayer;
|
private VideoPlayer videoPlayer;
|
||||||
|
|
||||||
public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, PluginManager pluginManager, IUpdateCheckClient updateCheckClient) {
|
public FormBrowser(ResourceCache resourceCache, PluginManager pluginManager, IUpdateCheckClient updateCheckClient, uint windowRestoreMessage) {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
Text = Program.BrandName;
|
Text = Program.BrandName;
|
||||||
|
|
||||||
this.resourceProvider = resourceProvider;
|
this.resourceCache = resourceCache;
|
||||||
|
|
||||||
this.plugins = pluginManager;
|
this.plugins = pluginManager;
|
||||||
|
|
||||||
@ -84,19 +86,21 @@ public FormBrowser(CachingResourceProvider<IResourceHandler> resourceProvider, P
|
|||||||
this.browser = new TweetDeckBrowser(this, plugins, tweetDeckInterface, updates);
|
this.browser = new TweetDeckBrowser(this, plugins, tweetDeckInterface, updates);
|
||||||
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
|
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
|
||||||
|
|
||||||
|
this.windowRestoreMessage = windowRestoreMessage;
|
||||||
|
|
||||||
Controls.Add(new MenuStrip { Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
|
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) => {
|
Disposed += (sender, args) => {
|
||||||
Config.MuteToggled -= Config_MuteToggled;
|
Config.MuteToggled -= Config_MuteToggled;
|
||||||
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
|
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
|
||||||
browser.Dispose();
|
browser.Dispose();
|
||||||
};
|
};
|
||||||
|
|
||||||
Config.MuteToggled += Config_MuteToggled;
|
|
||||||
|
|
||||||
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
|
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
|
||||||
this.trayIcon.ClickClose += trayIcon_ClickClose;
|
this.trayIcon.ClickClose += trayIcon_ClickClose;
|
||||||
Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
|
|
||||||
|
|
||||||
UpdateTray();
|
UpdateTray();
|
||||||
|
|
||||||
@ -162,7 +166,7 @@ private void FormBrowser_Activated(object sender, EventArgs e) {
|
|||||||
|
|
||||||
trayIcon.HasNotifications = false;
|
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
|
browser.Enabled = true; // the browser is disabled; if the user clicks back into
|
||||||
} // the window, enable the browser again
|
} // the window, enable the browser again
|
||||||
}
|
}
|
||||||
@ -308,7 +312,7 @@ private void updateInteractionManager_UpdateDismissed(object sender, UpdateInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override void WndProc(ref Message m) {
|
protected override void WndProc(ref Message m) {
|
||||||
if (isLoaded && m.Msg == Program.WindowRestoreMessage) {
|
if (isLoaded && m.Msg == windowRestoreMessage) {
|
||||||
using Process me = Process.GetCurrentProcess();
|
using Process me = Process.GetCurrentProcess();
|
||||||
|
|
||||||
if (me.Id == m.WParam.ToInt32()) {
|
if (me.Id == m.WParam.ToInt32()) {
|
||||||
@ -345,10 +349,10 @@ public void ResumeNotification() {
|
|||||||
public void ReloadToTweetDeck() {
|
public void ReloadToTweetDeck() {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Resources.ResourceHotSwap.Run();
|
Resources.ResourceHotSwap.Run();
|
||||||
resourceProvider.ClearCache();
|
resourceCache.ClearCache();
|
||||||
#else
|
#else
|
||||||
if (ModifierKeys.HasFlag(Keys.Shift)) {
|
if (ModifierKeys.HasFlag(Keys.Shift)) {
|
||||||
resourceProvider.ClearCache();
|
resourceCache.ClearCache();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -361,7 +365,7 @@ public void OpenDevTools() {
|
|||||||
|
|
||||||
// callback handlers
|
// callback handlers
|
||||||
|
|
||||||
public void OnIntroductionClosed(bool showGuide) {
|
private void OnIntroductionClosed(bool showGuide) {
|
||||||
if (Config.FirstRun) {
|
if (Config.FirstRun) {
|
||||||
Config.FirstRun = false;
|
Config.FirstRun = false;
|
||||||
Config.Save();
|
Config.Save();
|
||||||
@ -372,7 +376,7 @@ public void OnIntroductionClosed(bool showGuide) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenContextMenu() {
|
private void OpenContextMenu() {
|
||||||
contextMenu.Show(this, PointToClient(Cursor.Position));
|
contextMenu.Show(this, PointToClient(Cursor.Position));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +432,7 @@ public void OpenPlugins() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenProfileImport() {
|
private void OpenProfileImport() {
|
||||||
FormManager.TryFind<FormSettings>()?.Close();
|
FormManager.TryFind<FormSettings>()?.Close();
|
||||||
|
|
||||||
using DialogSettingsManage dialog = new DialogSettingsManage(plugins, true);
|
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);
|
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) {
|
if (Config.EnableTrayHighlight && !ContainsFocus) {
|
||||||
trayIcon.HasNotifications = true;
|
trayIcon.HasNotifications = true;
|
||||||
}
|
}
|
||||||
@ -454,7 +458,7 @@ public void SaveVideo(string url, string username) {
|
|||||||
browser.SaveVideo(url, 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)) {
|
if (Arguments.HasFlag(Arguments.ArgHttpVideo)) {
|
||||||
videoUrl = Regex.Replace(videoUrl, "^https://", "http://");
|
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();
|
videoPlayer?.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,12 +506,12 @@ public bool ShowTweetDetail(string columnId, string chirpId, string fallbackUrl)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnTweetScreenshotReady(string html, int width) {
|
private void OnTweetScreenshotReady(string html, int width) {
|
||||||
notificationScreenshotManager ??= new TweetScreenshotManager(this, plugins);
|
notificationScreenshotManager ??= new TweetScreenshotManager(this, plugins);
|
||||||
notificationScreenshotManager.Trigger(html, width);
|
notificationScreenshotManager.Trigger(html, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisplayTooltip(string text) {
|
private void DisplayTooltip(string text) {
|
||||||
if (string.IsNullOrEmpty(text)) {
|
if (string.IsNullOrEmpty(text)) {
|
||||||
toolTip.Hide(this);
|
toolTip.Hide(this);
|
||||||
}
|
}
|
||||||
@ -530,5 +534,75 @@ bool CustomKeyboardHandler.IBrowserKeyHandler.HandleBrowserKey(Keys key) {
|
|||||||
|
|
||||||
return false;
|
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 System.Windows.Forms;
|
||||||
using CefSharp;
|
using CefSharp;
|
||||||
using TweetDuck.Browser.Adapters;
|
using TweetDuck.Browser.Adapters;
|
||||||
using TweetDuck.Configuration;
|
|
||||||
using TweetDuck.Controls;
|
using TweetDuck.Controls;
|
||||||
using TweetLib.Browser.Contexts;
|
using TweetLib.Browser.Contexts;
|
||||||
using TweetLib.Core.Features.TweetDeck;
|
using TweetLib.Core.Features.TweetDeck;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler;
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
namespace TweetDuck.Browser.Handling {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using CefSharp;
|
using CefSharp;
|
||||||
|
using TweetLib.Core;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Handling {
|
namespace TweetDuck.Browser.Handling {
|
||||||
@ -46,17 +47,22 @@ private static IEnumerable<string> ParseFileType(string type) {
|
|||||||
return new string[] { type };
|
return new string[] { type };
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
string[] extensions = type switch {
|
||||||
case "image/jpeg": return new string[] { ".jpg", ".jpeg" };
|
"image/jpeg" => new string[] { ".jpg", ".jpeg" },
|
||||||
case "image/png": return new string[] { ".png" };
|
"image/png" => new string[] { ".png" },
|
||||||
case "image/gif": return new string[] { ".gif" };
|
"image/gif" => new string[] { ".gif" },
|
||||||
case "image/webp": return new string[] { ".webp" };
|
"image/webp" => new string[] { ".webp" },
|
||||||
case "video/mp4": return new string[] { ".mp4" };
|
"video/mp4" => new string[] { ".mp4" },
|
||||||
case "video/quicktime": return new string[] { ".mov", ".qt" };
|
"video/quicktime" => new string[] { ".mov", ".qt" },
|
||||||
|
_ => StringUtils.EmptyArray
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extensions.Length == 0) {
|
||||||
|
App.Logger.Warn("Unknown file type: " + type);
|
||||||
|
Debugger.Break();
|
||||||
}
|
}
|
||||||
|
|
||||||
Debugger.Break();
|
return extensions;
|
||||||
return StringUtils.EmptyArray;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
using TweetLib.Core.Features.Notifications;
|
using TweetLib.Core.Features.Notifications;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetDuck.Browser.Notification {
|
namespace TweetDuck.Browser.Notification {
|
||||||
abstract partial class FormNotificationBase : Form {
|
abstract partial class FormNotificationBase : Form {
|
||||||
@ -59,7 +60,9 @@ protected virtual Point PrimaryLocation {
|
|||||||
protected virtual bool CanDragWindow => true;
|
protected virtual bool CanDragWindow => true;
|
||||||
|
|
||||||
public new Point Location {
|
public new Point Location {
|
||||||
get { return base.Location; }
|
get {
|
||||||
|
return base.Location;
|
||||||
|
}
|
||||||
|
|
||||||
set {
|
set {
|
||||||
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
|
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
|
||||||
|
@ -61,7 +61,9 @@ private static int FontSizeLevel {
|
|||||||
private int? prevFontSize;
|
private int? prevFontSize;
|
||||||
|
|
||||||
public virtual bool RequiresResize {
|
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 {
|
set {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using TweetDuck.Configuration;
|
using TweetDuck.Configuration;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using Res = TweetDuck.Properties.Resources;
|
using Res = TweetDuck.Properties.Resources;
|
||||||
|
|
||||||
namespace TweetDuck.Browser {
|
namespace TweetDuck.Browser {
|
||||||
@ -20,7 +21,9 @@ public enum Behavior { // keep order
|
|||||||
public event EventHandler ClickClose;
|
public event EventHandler ClickClose;
|
||||||
|
|
||||||
public bool Visible {
|
public bool Visible {
|
||||||
get { return notifyIcon.Visible; }
|
get {
|
||||||
|
return notifyIcon.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
set {
|
set {
|
||||||
notifyIcon.Visible = value;
|
notifyIcon.Visible = value;
|
||||||
@ -30,7 +33,9 @@ public bool Visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool HasNotifications {
|
public bool HasNotifications {
|
||||||
get { return hasNotifications; }
|
get {
|
||||||
|
return hasNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
set {
|
set {
|
||||||
if (hasNotifications != value) {
|
if (hasNotifications != value) {
|
||||||
@ -74,7 +79,7 @@ protected override void Dispose(bool disposing) {
|
|||||||
|
|
||||||
private void UpdateIcon() {
|
private void UpdateIcon() {
|
||||||
if (Visible) {
|
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)) {
|
if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) {
|
||||||
browserComponent.PageLoadEnd += (sender, args) => {
|
browserComponent.PageLoadEnd += (sender, args) => {
|
||||||
if (TwitterUrls.IsTweetDeck(args.Url)) {
|
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);
|
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 {
|
namespace TweetDuck.Configuration {
|
||||||
sealed class SystemConfig : BaseConfig {
|
sealed class SystemConfig : BaseConfig<SystemConfig> {
|
||||||
private bool _hardwareAcceleration = true;
|
private bool _hardwareAcceleration = true;
|
||||||
|
|
||||||
public bool ClearCacheAutomatically { get; set; } = true;
|
public bool ClearCacheAutomatically { get; set; } = true;
|
||||||
@ -11,12 +12,12 @@ sealed class SystemConfig : BaseConfig {
|
|||||||
|
|
||||||
public bool HardwareAcceleration {
|
public bool HardwareAcceleration {
|
||||||
get => _hardwareAcceleration;
|
get => _hardwareAcceleration;
|
||||||
set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _hardwareAcceleration, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
// END OF CONFIG
|
// END OF CONFIG
|
||||||
|
|
||||||
protected override BaseConfig ConstructWithDefaults() {
|
public override SystemConfig ConstructWithDefaults() {
|
||||||
return new SystemConfig();
|
return new SystemConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,15 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using TweetDuck.Browser;
|
using TweetDuck.Browser;
|
||||||
using TweetDuck.Controls;
|
using TweetDuck.Controls;
|
||||||
using TweetDuck.Dialogs;
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Application;
|
using TweetLib.Core.Application;
|
||||||
using TweetLib.Core.Features.Notifications;
|
using TweetLib.Core.Features.Notifications;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
using TweetLib.Core.Systems.Configuration;
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
using TweetLib.Utils.Data;
|
||||||
|
|
||||||
namespace TweetDuck.Configuration {
|
namespace TweetDuck.Configuration {
|
||||||
sealed class UserConfig : BaseConfig, IAppUserConfiguration {
|
sealed class UserConfig : BaseConfig<UserConfig>, IAppUserConfiguration {
|
||||||
public bool FirstRun { get; set; } = true;
|
public bool FirstRun { get; set; } = true;
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
@ -45,7 +46,7 @@ sealed class UserConfig : BaseConfig, IAppUserConfiguration {
|
|||||||
private string _spellCheckLanguage = "en-US";
|
private string _spellCheckLanguage = "en-US";
|
||||||
|
|
||||||
public string TranslationTarget { get; set; } = "en";
|
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;
|
private TrayIcon.Behavior _trayBehavior = TrayIcon.Behavior.Disabled;
|
||||||
public bool EnableTrayHighlight { get; set; } = true;
|
public bool EnableTrayHighlight { get; set; } = true;
|
||||||
@ -121,32 +122,32 @@ public TrayIcon.Behavior TrayBehavior {
|
|||||||
|
|
||||||
public bool EnableSmoothScrolling {
|
public bool EnableSmoothScrolling {
|
||||||
get => _enableSmoothScrolling;
|
get => _enableSmoothScrolling;
|
||||||
set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _enableSmoothScrolling, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool EnableTouchAdjustment {
|
public bool EnableTouchAdjustment {
|
||||||
get => _enableTouchAdjustment;
|
get => _enableTouchAdjustment;
|
||||||
set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _enableTouchAdjustment, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool EnableColorProfileDetection {
|
public bool EnableColorProfileDetection {
|
||||||
get => _enableColorProfileDetection;
|
get => _enableColorProfileDetection;
|
||||||
set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _enableColorProfileDetection, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UseSystemProxyForAllConnections {
|
public bool UseSystemProxyForAllConnections {
|
||||||
get => _useSystemProxyForAllConnections;
|
get => _useSystemProxyForAllConnections;
|
||||||
set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _useSystemProxyForAllConnections, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CustomCefArgs {
|
public string CustomCefArgs {
|
||||||
get => _customCefArgs;
|
get => _customCefArgs;
|
||||||
set => UpdatePropertyWithCallback(ref _customCefArgs, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _customCefArgs, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SpellCheckLanguage {
|
public string SpellCheckLanguage {
|
||||||
get => _spellCheckLanguage;
|
get => _spellCheckLanguage;
|
||||||
set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, Program.Config.TriggerProgramRestartRequested);
|
set => UpdatePropertyWithCallback(ref _spellCheckLanguage, value, App.ConfigManager.TriggerProgramRestartRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
// EVENTS
|
// EVENTS
|
||||||
@ -163,7 +164,7 @@ public void TriggerOptionsDialogClosed() {
|
|||||||
|
|
||||||
// END OF CONFIG
|
// END OF CONFIG
|
||||||
|
|
||||||
protected override BaseConfig ConstructWithDefaults() {
|
public override UserConfig ConstructWithDefaults() {
|
||||||
return new UserConfig();
|
return new UserConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using TweetLib.Utils.Data;
|
||||||
|
|
||||||
namespace TweetDuck.Controls {
|
namespace TweetDuck.Controls {
|
||||||
static class ControlExtensions {
|
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 {
|
namespace TweetDuck.Dialogs {
|
||||||
sealed partial class FormAbout : Form, FormManager.IAppDialog {
|
sealed partial class FormAbout : Form, FormManager.IAppDialog {
|
||||||
private const string TipsLink = "https://github.com/chylex/TweetDuck/wiki";
|
private const string TipsLink = "https://github.com/chylex/TweetDuck/wiki";
|
||||||
private const string IssuesLink = "https://github.com/chylex/TweetDuck/issues";
|
|
||||||
|
|
||||||
public FormAbout() {
|
public FormAbout() {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@ -20,7 +19,7 @@ public FormAbout() {
|
|||||||
|
|
||||||
labelWebsite.Links.Add(new LinkLabel.Link(0, labelWebsite.Text.Length, Program.Website));
|
labelWebsite.Links.Add(new LinkLabel.Link(0, labelWebsite.Text.Length, Program.Website));
|
||||||
labelTips.Links.Add(new LinkLabel.Link(0, labelTips.Text.Length, TipsLink));
|
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 {
|
try {
|
||||||
pictureLogo.Image = Image.FromFile(Path.Combine(App.ResourcesPath, "images/logo.png"));
|
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);
|
return Show(caption, text, MessageBoxIcon.Question, buttonAccept, buttonCancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool Show(string caption, string text, MessageBoxIcon icon, string button) {
|
public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel = null) {
|
||||||
return Show(caption, text, icon, button, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Show(string caption, string text, MessageBoxIcon icon, string buttonAccept, string buttonCancel) {
|
|
||||||
using FormMessage message = new FormMessage(caption, text, icon);
|
using FormMessage message = new FormMessage(caption, text, icon);
|
||||||
|
|
||||||
if (buttonCancel == null) {
|
if (buttonCancel == null) {
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
using TweetDuck.Plugins;
|
using TweetDuck.Plugins;
|
||||||
using TweetLib.Core;
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetDuck.Dialogs {
|
namespace TweetDuck.Dialogs {
|
||||||
sealed partial class FormPlugins : Form, FormManager.IAppDialog {
|
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));
|
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) => {
|
FormClosed += (sender, args) => {
|
||||||
Config.PluginsWindowSize = Size;
|
Config.PluginsWindowSize = Size;
|
||||||
Config.Save();
|
Config.Save();
|
||||||
};
|
};
|
||||||
|
|
||||||
ResizeEnd += (sender, args) => { timerLayout.Start(); };
|
ResizeEnd += (sender, args) => {
|
||||||
|
timerLayout.Start();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetPluginOrderIndex(Plugin plugin) {
|
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) {
|
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) {
|
private void btnReload_Click(object sender, EventArgs e) {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
using TweetDuck.Dialogs.Settings;
|
using TweetDuck.Dialogs.Settings;
|
||||||
using TweetDuck.Management;
|
using TweetDuck.Management;
|
||||||
using TweetDuck.Utils;
|
using TweetDuck.Utils;
|
||||||
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
using TweetLib.Core.Features.TweetDeck;
|
using TweetLib.Core.Features.TweetDeck;
|
||||||
using TweetLib.Core.Systems.Updates;
|
using TweetLib.Core.Systems.Updates;
|
||||||
@ -50,14 +51,14 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateChecker up
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void PrepareLoad() {
|
private void PrepareLoad() {
|
||||||
Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
|
App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrepareUnload() { // TODO refactor this further later
|
private void PrepareUnload() { // TODO refactor this further later
|
||||||
currentTab.Control.OnClosing();
|
currentTab.Control.OnClosing();
|
||||||
|
|
||||||
Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
|
App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
|
||||||
Program.Config.SaveAll();
|
App.ConfigManager.SaveAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Config_ProgramRestartRequested(object sender, EventArgs e) {
|
private void Config_ProgramRestartRequested(object sender, EventArgs e) {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using TweetDuck.Management;
|
using TweetDuck.Management;
|
||||||
using TweetLib.Core;
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetDuck.Dialogs.Settings {
|
namespace TweetDuck.Dialogs.Settings {
|
||||||
@ -133,7 +134,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
|
|||||||
|
|
||||||
case State.Reset:
|
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)) {
|
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)) {
|
if (SelectedItems.HasFlag(ProfileManager.Items.UserConfig)) {
|
||||||
Program.Config.User.Reset();
|
Program.Config.User.Reset();
|
||||||
@ -143,7 +144,7 @@ private void btnContinue_Click(object sender, EventArgs e) {
|
|||||||
Program.Config.System.Reset();
|
Program.Config.System.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
|
App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
|
||||||
|
|
||||||
if (SelectedItems.HasFlag(ProfileManager.Items.PluginData)) {
|
if (SelectedItems.HasFlag(ProfileManager.Items.PluginData)) {
|
||||||
Program.Config.Plugins.Reset();
|
Program.Config.Plugins.Reset();
|
||||||
@ -174,9 +175,9 @@ private void btnContinue_Click(object sender, EventArgs e) {
|
|||||||
|
|
||||||
case State.Import:
|
case State.Import:
|
||||||
if (importManager.Import(SelectedItems)) {
|
if (importManager.Import(SelectedItems)) {
|
||||||
Program.Config.ProgramRestartRequested += Config_ProgramRestartRequested;
|
App.ConfigManager.ProgramRestartRequested += Config_ProgramRestartRequested;
|
||||||
Program.Config.ReloadAll();
|
App.ConfigManager.ReloadAll();
|
||||||
Program.Config.ProgramRestartRequested -= Config_ProgramRestartRequested;
|
App.ConfigManager.ProgramRestartRequested -= Config_ProgramRestartRequested;
|
||||||
|
|
||||||
if (SelectedItemsForceRestart) {
|
if (SelectedItemsForceRestart) {
|
||||||
RestartProgram(SelectedItems.HasFlag(ProfileManager.Items.Session) ? new string[] { Arguments.ArgImportCookies } : StringUtils.EmptyArray);
|
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) {
|
private void btnEditCefArgs_Click(object sender, EventArgs e) {
|
||||||
DialogSettingsCefArgs form = new DialogSettingsCefArgs(Config.CustomCefArgs);
|
DialogSettingsCefArgs form = new DialogSettingsCefArgs(Config.CustomCefArgs);
|
||||||
|
|
||||||
form.VisibleChanged += (sender2, args2) => { form.MoveToCenter(ParentForm); };
|
form.VisibleChanged += (sender2, args2) => {
|
||||||
|
form.MoveToCenter(ParentForm);
|
||||||
|
};
|
||||||
|
|
||||||
form.FormClosed += (sender2, args2) => {
|
form.FormClosed += (sender2, args2) => {
|
||||||
RestoreParentForm();
|
RestoreParentForm();
|
||||||
@ -144,7 +146,9 @@ private void btnEditCefArgs_Click(object sender, EventArgs e) {
|
|||||||
private void btnEditCSS_Click(object sender, EventArgs e) {
|
private void btnEditCSS_Click(object sender, EventArgs e) {
|
||||||
DialogSettingsCSS form = new DialogSettingsCSS(Config.CustomBrowserCSS, Config.CustomNotificationCSS, reinjectBrowserCSS, openDevTools);
|
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) => {
|
form.FormClosed += (sender2, args2) => {
|
||||||
RestoreParentForm();
|
RestoreParentForm();
|
||||||
|
@ -14,7 +14,7 @@ public override void OnReady() {
|
|||||||
#region Feedback
|
#region Feedback
|
||||||
|
|
||||||
private void btnSendFeedback_Click(object sender, EventArgs e) {
|
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
|
#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 CloseNaturallyTimeout = 10000;
|
||||||
private const int CloseKillTimeout = 5000;
|
private const int CloseKillTimeout = 5000;
|
||||||
|
|
||||||
|
public uint WindowRestoreMessage { get; } = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
|
||||||
|
|
||||||
private readonly LockFile lockFile;
|
private readonly LockFile lockFile;
|
||||||
|
|
||||||
public LockManager(string path) {
|
public LockManager(string path) {
|
||||||
@ -33,7 +35,7 @@ private bool LaunchNormally() {
|
|||||||
LockResult lockResult = lockFile.Lock();
|
LockResult lockResult = lockFile.Lock();
|
||||||
|
|
||||||
if (lockResult is LockResult.HasProcess info) {
|
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)) {
|
if (!CloseProcess(info.Process)) {
|
||||||
FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
|
FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK);
|
||||||
return false;
|
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);
|
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
|
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)) {
|
if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -24,8 +24,7 @@ public enum Items {
|
|||||||
UserConfig = 1,
|
UserConfig = 1,
|
||||||
SystemConfig = 2,
|
SystemConfig = 2,
|
||||||
Session = 4,
|
Session = 4,
|
||||||
PluginData = 8,
|
PluginData = 8
|
||||||
All = UserConfig | SystemConfig | Session | PluginData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly string file;
|
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));
|
using CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None));
|
||||||
|
|
||||||
if (items.HasFlag(Items.UserConfig)) {
|
if (items.HasFlag(Items.UserConfig)) {
|
||||||
stream.WriteFile("config", Program.Config.FilePaths.UserConfig);
|
stream.WriteFile("config", App.ConfigManager.UserPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.HasFlag(Items.SystemConfig)) {
|
if (items.HasFlag(Items.SystemConfig)) {
|
||||||
stream.WriteFile("system", Program.Config.FilePaths.SystemConfig);
|
stream.WriteFile("system", App.ConfigManager.SystemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.HasFlag(Items.PluginData)) {
|
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 (Plugin plugin in plugins.Plugins) {
|
||||||
foreach (PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))) {
|
foreach (PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))) {
|
||||||
@ -122,21 +121,21 @@ public bool Import(Items items) {
|
|||||||
switch (entry.KeyName) {
|
switch (entry.KeyName) {
|
||||||
case "config":
|
case "config":
|
||||||
if (items.HasFlag(Items.UserConfig)) {
|
if (items.HasFlag(Items.UserConfig)) {
|
||||||
entry.WriteToFile(Program.Config.FilePaths.UserConfig);
|
entry.WriteToFile(App.ConfigManager.UserPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "system":
|
case "system":
|
||||||
if (items.HasFlag(Items.SystemConfig)) {
|
if (items.HasFlag(Items.SystemConfig)) {
|
||||||
entry.WriteToFile(Program.Config.FilePaths.SystemConfig);
|
entry.WriteToFile(App.ConfigManager.SystemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "plugin.config":
|
case "plugin.config":
|
||||||
if (items.HasFlag(Items.PluginData)) {
|
if (items.HasFlag(Items.PluginData)) {
|
||||||
entry.WriteToFile(Program.Config.FilePaths.PluginConfig);
|
entry.WriteToFile(App.ConfigManager.PluginsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
using TweetDuck.Dialogs;
|
using TweetDuck.Dialogs;
|
||||||
using TweetLib.Communication.Pipe;
|
using TweetLib.Communication.Pipe;
|
||||||
using TweetLib.Core;
|
using TweetLib.Core;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetDuck.Management {
|
namespace TweetDuck.Management {
|
||||||
sealed class VideoPlayer : IDisposable {
|
sealed class VideoPlayer : IDisposable {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using TweetLib.Core;
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
using TweetLib.Core.Features.Plugins.Enums;
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetDuck.Plugins {
|
namespace TweetDuck.Plugins {
|
||||||
sealed partial class PluginControl : UserControl {
|
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) {
|
private void btnToggleState_Click(object sender, EventArgs e) {
|
||||||
pluginManager.Config.SetEnabled(plugin, !pluginManager.Config.IsEnabled(plugin));
|
pluginManager.Config.SetEnabled(plugin, !pluginManager.Config.IsEnabled(plugin));
|
||||||
|
pluginManager.Config.Save();
|
||||||
UpdatePluginState();
|
UpdatePluginState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
187
Program.cs
187
Program.cs
@ -8,19 +8,18 @@
|
|||||||
using TweetDuck.Browser.Adapters;
|
using TweetDuck.Browser.Adapters;
|
||||||
using TweetDuck.Browser.Handling;
|
using TweetDuck.Browser.Handling;
|
||||||
using TweetDuck.Configuration;
|
using TweetDuck.Configuration;
|
||||||
using TweetDuck.Dialogs;
|
|
||||||
using TweetDuck.Management;
|
using TweetDuck.Management;
|
||||||
using TweetDuck.Updates;
|
using TweetDuck.Updates;
|
||||||
using TweetDuck.Utils;
|
using TweetDuck.Utils;
|
||||||
using TweetLib.Core;
|
using TweetLib.Core;
|
||||||
using TweetLib.Core.Application;
|
using TweetLib.Core.Application;
|
||||||
using TweetLib.Core.Features;
|
|
||||||
using TweetLib.Core.Features.Chromium;
|
using TweetLib.Core.Features.Chromium;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
|
using TweetLib.Core.Features.Plugins.Config;
|
||||||
using TweetLib.Core.Features.TweetDeck;
|
using TweetLib.Core.Features.TweetDeck;
|
||||||
using TweetLib.Core.Resources;
|
using TweetLib.Core.Resources;
|
||||||
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using TweetLib.Utils.Collections;
|
using TweetLib.Utils.Collections;
|
||||||
using TweetLib.Utils.Static;
|
|
||||||
using Win = System.Windows.Forms;
|
using Win = System.Windows.Forms;
|
||||||
|
|
||||||
namespace TweetDuck {
|
namespace TweetDuck {
|
||||||
@ -30,22 +29,17 @@ static class Program {
|
|||||||
|
|
||||||
public const string Website = "https://tweetduck.chylex.com";
|
public const string Website = "https://tweetduck.chylex.com";
|
||||||
|
|
||||||
private const string PluginDataFolder = "TD_Plugins";
|
|
||||||
private const string InstallerFolder = "TD_Updates";
|
private const string InstallerFolder = "TD_Updates";
|
||||||
private const string CefDataFolder = "TD_Chromium";
|
private const string CefDataFolder = "TD_Chromium";
|
||||||
|
|
||||||
private const string ProgramLogFile = "TD_Log.txt";
|
|
||||||
private const string ConsoleLogFile = "TD_Console.txt";
|
private const string ConsoleLogFile = "TD_Console.txt";
|
||||||
|
|
||||||
public static string ExecutablePath => Win.Application.ExecutablePath;
|
public static string ExecutablePath => Win.Application.ExecutablePath;
|
||||||
|
|
||||||
public static uint WindowRestoreMessage;
|
|
||||||
|
|
||||||
private static LockManager lockManager;
|
|
||||||
private static Reporter errorReporter;
|
private static Reporter errorReporter;
|
||||||
|
private static LockManager lockManager;
|
||||||
private static bool hasCleanedUp;
|
private static bool hasCleanedUp;
|
||||||
|
|
||||||
public static ConfigManager Config { get; private set; }
|
public static ConfigObjects<UserConfig, SystemConfig> Config { get; private set; }
|
||||||
|
|
||||||
internal static void SetupWinForms() {
|
internal static void SetupWinForms() {
|
||||||
Win.Application.EnableVisualStyles();
|
Win.Application.EnableVisualStyles();
|
||||||
@ -59,112 +53,99 @@ private static void Main() {
|
|||||||
SetupWinForms();
|
SetupWinForms();
|
||||||
Cef.EnableHighDPISupport();
|
Cef.EnableHighDPISupport();
|
||||||
|
|
||||||
var startup = new AppStartup {
|
|
||||||
CustomDataFolder = Arguments.GetValue(Arguments.ArgDataFolder)
|
|
||||||
};
|
|
||||||
|
|
||||||
var reporter = new Reporter();
|
var reporter = new Reporter();
|
||||||
var userConfig = new UserConfig();
|
|
||||||
|
|
||||||
Lib.Initialize(new AppBuilder {
|
Config = new ConfigObjects<UserConfig, SystemConfig>(
|
||||||
Startup = startup,
|
new UserConfig(),
|
||||||
Logger = new Logger(ProgramLogFile),
|
new SystemConfig(),
|
||||||
|
new PluginConfig(new string[] {
|
||||||
|
"official/clear-columns",
|
||||||
|
"official/reply-account"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Lib.AppLauncher launch = Lib.Initialize(new AppBuilder {
|
||||||
|
Setup = new Setup(),
|
||||||
ErrorHandler = reporter,
|
ErrorHandler = reporter,
|
||||||
SystemHandler = new SystemHandler(),
|
SystemHandler = new SystemHandler(),
|
||||||
DialogHandler = new DialogHandler(),
|
MessageDialogs = new MessageDialogs(),
|
||||||
UserConfiguration = userConfig
|
FileDialogs = new FileDialogs(),
|
||||||
});
|
});
|
||||||
|
|
||||||
LaunchApp(reporter, userConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void LaunchApp(Reporter reporter, UserConfig userConfig) {
|
|
||||||
App.Launch();
|
|
||||||
|
|
||||||
errorReporter = reporter;
|
errorReporter = reporter;
|
||||||
string storagePath = App.StoragePath;
|
launch();
|
||||||
|
}
|
||||||
|
|
||||||
Config = new ConfigManager(userConfig, new ConfigManager.Paths {
|
private sealed class Setup : IAppSetup {
|
||||||
UserConfig = Path.Combine(storagePath, "TD_UserConfig.cfg"),
|
public bool IsPortable => File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "makeportable"));
|
||||||
SystemConfig = Path.Combine(storagePath, "TD_SystemConfig.cfg"),
|
public bool IsDebugLogging => Arguments.HasFlag(Arguments.ArgLogging);
|
||||||
PluginConfig = Path.Combine(storagePath, "TD_PluginConfig.cfg")
|
public string CustomDataFolder => Arguments.GetValue(Arguments.ArgDataFolder);
|
||||||
});
|
public string ResourceRewriteRules => Arguments.GetValue(Arguments.ArgFreeze);
|
||||||
|
|
||||||
lockManager = new LockManager(Path.Combine(storagePath, ".lock"));
|
public ConfigManager CreateConfigManager(string storagePath) {
|
||||||
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
|
return new ConfigManager<UserConfig, SystemConfig>(storagePath, Config);
|
||||||
|
|
||||||
if (!lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.LoadAll();
|
public bool TryLockDataFolder(string lockFile) {
|
||||||
|
lockManager = new LockManager(lockFile);
|
||||||
if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
|
return lockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart));
|
||||||
ProfileManager.ImportCookies();
|
|
||||||
}
|
|
||||||
else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
|
|
||||||
ProfileManager.DeleteCookies();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var installerFolderPath = Path.Combine(storagePath, InstallerFolder);
|
public void BeforeLaunch() {
|
||||||
|
if (Arguments.HasFlag(Arguments.ArgImportCookies)) {
|
||||||
if (Arguments.HasFlag(Arguments.ArgUpdated)) {
|
ProfileManager.ImportCookies();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
else {
|
else if (Arguments.HasFlag(Arguments.ArgDeleteCookies)) {
|
||||||
RestartWithArgsInternal(Arguments.GetCurrentClean());
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.SaveAll();
|
App.Close();
|
||||||
|
|
||||||
Cef.Shutdown();
|
Cef.Shutdown();
|
||||||
BrowserCache.Exit();
|
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) => {
|
btnOpenLog.Click += (sender, args) => {
|
||||||
if (!App.Logger.OpenLogFile()) {
|
if (!OpenLogFile()) {
|
||||||
FormMessage.Error("Error Log", "Cannot open error log.", FormMessage.OK);
|
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 {
|
public sealed class ExpandedLogException : Exception {
|
||||||
private readonly string details;
|
private readonly string details;
|
||||||
|
|
||||||
|
@ -62,14 +62,14 @@
|
|||||||
<Reference Include="System.Windows.Forms" />
|
<Reference Include="System.Windows.Forms" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Application\DialogHandler.cs" />
|
<Compile Include="Application\FileDialogs.cs" />
|
||||||
<Compile Include="Application\Logger.cs" />
|
<Compile Include="Application\MessageDialogs.cs" />
|
||||||
<Compile Include="Browser\Adapters\CefBrowserComponent.cs" />
|
<Compile Include="Browser\Adapters\CefBrowserComponent.cs" />
|
||||||
<Compile Include="Browser\Adapters\CefContextMenuActionRegistry.cs" />
|
<Compile Include="Browser\Adapters\CefContextMenuActionRegistry.cs" />
|
||||||
<Compile Include="Browser\Adapters\CefContextMenuModel.cs" />
|
<Compile Include="Browser\Adapters\CefContextMenuModel.cs" />
|
||||||
<Compile Include="Browser\Adapters\CefResourceHandlerFactory.cs" />
|
<Compile Include="Browser\Adapters\CefResourceHandlerFactory.cs" />
|
||||||
<Compile Include="Browser\Adapters\CefResourceHandlerRegistry.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\CefResourceRequestHandler.cs" />
|
||||||
<Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
|
<Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" />
|
||||||
<Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
|
<Compile Include="Browser\Handling\BrowserProcessHandler.cs" />
|
||||||
@ -80,8 +80,6 @@
|
|||||||
<Compile Include="Browser\Notification\FormNotificationExample.cs">
|
<Compile Include="Browser\Notification\FormNotificationExample.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Browser\TweetDeckInterfaceImpl.cs" />
|
|
||||||
<Compile Include="Dialogs\WindowState.cs" />
|
|
||||||
<Compile Include="Management\LockManager.cs" />
|
<Compile Include="Management\LockManager.cs" />
|
||||||
<Compile Include="Application\SystemHandler.cs" />
|
<Compile Include="Application\SystemHandler.cs" />
|
||||||
<Compile Include="Browser\Handling\ContextMenuBase.cs" />
|
<Compile Include="Browser\Handling\ContextMenuBase.cs" />
|
||||||
@ -98,8 +96,6 @@
|
|||||||
<Compile Include="Browser\Notification\SoundNotification.cs" />
|
<Compile Include="Browser\Notification\SoundNotification.cs" />
|
||||||
<Compile Include="Browser\TweetDeckBrowser.cs" />
|
<Compile Include="Browser\TweetDeckBrowser.cs" />
|
||||||
<Compile Include="Configuration\Arguments.cs" />
|
<Compile Include="Configuration\Arguments.cs" />
|
||||||
<Compile Include="Configuration\ConfigManager.cs" />
|
|
||||||
<Compile Include="Configuration\PluginConfig.cs" />
|
|
||||||
<Compile Include="Configuration\SystemConfig.cs" />
|
<Compile Include="Configuration\SystemConfig.cs" />
|
||||||
<Compile Include="Configuration\UserConfig.cs" />
|
<Compile Include="Configuration\UserConfig.cs" />
|
||||||
<Compile Include="Controls\ControlExtensions.cs" />
|
<Compile Include="Controls\ControlExtensions.cs" />
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
namespace TweetDuck.Utils {
|
namespace TweetDuck.Utils {
|
||||||
static class BrowserUtils {
|
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";
|
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;
|
private static UserConfig Config => Program.Config.User;
|
||||||
|
@ -3,15 +3,11 @@
|
|||||||
|
|
||||||
namespace TweetLib.Browser.Base {
|
namespace TweetLib.Browser.Base {
|
||||||
public class BaseBrowser<T> : IDisposable where T : BaseBrowser<T> {
|
public class BaseBrowser<T> : IDisposable where T : BaseBrowser<T> {
|
||||||
public IScriptExecutor ScriptExecutor { get; }
|
|
||||||
|
|
||||||
protected readonly IBrowserComponent browserComponent;
|
protected readonly IBrowserComponent browserComponent;
|
||||||
|
|
||||||
protected BaseBrowser(IBrowserComponent browserComponent, Func<T, BrowserSetup> setup) {
|
protected BaseBrowser(IBrowserComponent browserComponent, Func<T, BrowserSetup> setup) {
|
||||||
this.browserComponent = browserComponent;
|
this.browserComponent = browserComponent;
|
||||||
this.browserComponent.Setup(setup((T) this));
|
this.browserComponent.Setup(setup((T) this));
|
||||||
|
|
||||||
this.ScriptExecutor = browserComponent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Dispose() {}
|
public virtual void Dispose() {}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using TweetLib.Browser.Request;
|
||||||
|
|
||||||
namespace TweetLib.Browser.Interfaces {
|
namespace TweetLib.Browser.Interfaces {
|
||||||
public interface ICustomSchemeHandler<T> where T : class {
|
public interface ICustomSchemeHandler {
|
||||||
string Protocol { get; }
|
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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using TweetLib.Core.Application;
|
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 TweetLib.Utils.Static;
|
||||||
|
using Version = TweetDuck.Version;
|
||||||
|
|
||||||
namespace TweetLib.Core {
|
namespace TweetLib.Core {
|
||||||
public static class App {
|
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 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 ResourcesPath = Path.Combine(ProgramPath, "resources");
|
||||||
public static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
public static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
||||||
public static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
|
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 Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
|
||||||
public static IAppErrorHandler ErrorHandler { get; } = Validate(Builder.ErrorHandler, nameof(Builder.ErrorHandler));
|
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
|
||||||
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 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)) {
|
if (!FileUtils.CheckFolderWritePermission(StoragePath)) {
|
||||||
throw new AppException("Permission Error", "TweetDuck does not have write permissions to the storage folder: " + 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
|
// Setup
|
||||||
@ -46,19 +108,19 @@ private static T Validate<T>(T? obj, string name) where T : class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AppBuilder {
|
public sealed class AppBuilder {
|
||||||
public AppStartup? Startup { get; set; }
|
public IAppSetup? Setup { get; set; }
|
||||||
public IAppLogger? Logger { get; set; }
|
|
||||||
public IAppErrorHandler? ErrorHandler { get; set; }
|
public IAppErrorHandler? ErrorHandler { get; set; }
|
||||||
public IAppSystemHandler? SystemHandler { get; set; }
|
public IAppSystemHandler? SystemHandler { get; set; }
|
||||||
public IAppDialogHandler? DialogHandler { get; set; }
|
public IAppMessageDialogs? MessageDialogs { get; set; }
|
||||||
public IAppUserConfiguration? UserConfiguration { get; set; }
|
public IAppFileDialogs? FileDialogs { get; set; }
|
||||||
|
|
||||||
internal static AppBuilder? Instance { get; private set; }
|
internal static AppBuilder? Instance { get; private set; }
|
||||||
|
|
||||||
internal void Build() {
|
internal Lib.AppLauncher Build() {
|
||||||
Instance = this;
|
Instance = this;
|
||||||
App.Initialize();
|
App.Initialize();
|
||||||
Instance = null;
|
Instance = null;
|
||||||
|
return App.Launch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,6 @@ namespace TweetLib.Core.Application {
|
|||||||
public sealed class AppException : Exception {
|
public sealed class AppException : Exception {
|
||||||
public string Title { get; }
|
public string Title { get; }
|
||||||
|
|
||||||
internal AppException(string title) {
|
|
||||||
this.Title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal AppException(string title, string message) : base(message) {
|
internal AppException(string title, string message) : base(message) {
|
||||||
this.Title = title;
|
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 {
|
namespace TweetLib.Core.Application {
|
||||||
public interface IAppSystemHandler {
|
public interface IAppSystemHandler {
|
||||||
void OpenAssociatedProgram(string path);
|
|
||||||
void OpenBrowser(string? url);
|
void OpenBrowser(string? url);
|
||||||
void OpenFileExplorer(string path);
|
void OpenFileExplorer(string path);
|
||||||
void CopyImageFromFile(string path);
|
|
||||||
void CopyText(string text);
|
OpenAssociatedProgramFunc? OpenAssociatedProgram { get; }
|
||||||
void SearchText(string text);
|
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 {
|
namespace TweetLib.Core.Application {
|
||||||
public interface IAppUserConfiguration {
|
public interface IAppUserConfiguration {
|
||||||
string CustomBrowserCSS { get; }
|
string? CustomBrowserCSS { get; }
|
||||||
string CustomNotificationCSS { get; }
|
string? CustomNotificationCSS { get; }
|
||||||
string? DismissedUpdate { get; }
|
string? DismissedUpdate { get; }
|
||||||
bool ExpandLinksOnHover { get; }
|
bool ExpandLinksOnHover { get; }
|
||||||
bool FirstRun { get; }
|
bool FirstRun { get; }
|
||||||
@ -20,6 +20,7 @@ public interface IAppUserConfiguration {
|
|||||||
int CalendarFirstDay { get; }
|
int CalendarFirstDay { get; }
|
||||||
string TranslationTarget { get; }
|
string TranslationTarget { get; }
|
||||||
ImageQuality TwitterImageQuality { get; }
|
ImageQuality TwitterImageQuality { get; }
|
||||||
|
bool UseSystemProxyForAllConnections { get; }
|
||||||
|
|
||||||
event EventHandler MuteToggled;
|
event EventHandler MuteToggled;
|
||||||
event EventHandler OptionsDialogClosed;
|
event EventHandler OptionsDialogClosed;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
using System;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using TweetLib.Browser.Contexts;
|
using TweetLib.Browser.Contexts;
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
using TweetLib.Core.Systems.Dialogs;
|
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features {
|
namespace TweetLib.Core.Features {
|
||||||
@ -13,14 +11,14 @@ internal class BaseContextMenu : IContextMenuHandler {
|
|||||||
|
|
||||||
public BaseContextMenu(IBrowserComponent browser) {
|
public BaseContextMenu(IBrowserComponent browser) {
|
||||||
this.browser = browser;
|
this.browser = browser;
|
||||||
this.fileDownloadManager = new FileDownloadManager(browser);
|
this.fileDownloadManager = new FileDownloadManager(browser.FileDownloader);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Show(IContextMenuBuilder menu, Context context) {
|
public virtual void Show(IContextMenuBuilder menu, Context context) {
|
||||||
if (context.Selection is { Editable: false } selection) {
|
if (context.Selection is { Editable: false } selection) {
|
||||||
AddSearchSelectionItems(menu, selection.Text);
|
AddSearchSelectionItems(menu, selection.Text);
|
||||||
menu.AddSeparator();
|
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();
|
menu.AddSeparator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,13 +30,13 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
|
|||||||
Match match = TwitterUrls.RegexAccount.Match(link.CopyUrl);
|
Match match = TwitterUrls.RegexAccount.Match(link.CopyUrl);
|
||||||
|
|
||||||
if (match.Success) {
|
if (match.Success) {
|
||||||
menu.AddAction(TextOpen("account"), OpenLink(link.Url));
|
AddOpenAction(menu, TextOpen("account"), link.Url);
|
||||||
menu.AddAction(TextCopy("account"), CopyText(link.CopyUrl));
|
AddCopyAction(menu, TextCopy("account"), link.CopyUrl);
|
||||||
menu.AddAction("Copy account username", CopyText(match.Groups[1].Value));
|
AddCopyAction(menu, "Copy account username", match.Groups[1].Value);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.AddAction(TextOpen("link"), OpenLink(link.Url));
|
AddOpenAction(menu, TextOpen("link"), link.Url);
|
||||||
menu.AddAction(TextCopy("link"), CopyText(link.CopyUrl));
|
AddCopyAction(menu, TextCopy("link"), link.CopyUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.AddSeparator();
|
menu.AddSeparator();
|
||||||
@ -49,24 +47,37 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
|
|||||||
|
|
||||||
switch (media.MediaType) {
|
switch (media.MediaType) {
|
||||||
case Media.Type.Image:
|
case Media.Type.Image:
|
||||||
menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(media.Url));
|
if (fileDownloadManager.SupportsViewingImage) {
|
||||||
menu.AddAction(TextOpen("image"), OpenLink(media.Url));
|
menu.AddAction("View image in photo viewer", () => fileDownloadManager.ViewImage(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));
|
|
||||||
|
|
||||||
var imageUrls = tweet?.ImageUrls;
|
AddOpenAction(menu, TextOpen("image"), media.Url);
|
||||||
if (imageUrls?.Length > 1) {
|
AddCopyAction(menu, TextCopy("image"), media.Url);
|
||||||
menu.AddAction(TextSave("all images"), () => fileDownloadManager.SaveImages(imageUrls, tweet?.MediaAuthor));
|
|
||||||
|
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();
|
menu.AddSeparator();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Media.Type.Video:
|
case Media.Type.Video:
|
||||||
menu.AddAction(TextOpen("video"), OpenLink(media.Url));
|
AddOpenAction(menu, TextOpen("video"), media.Url);
|
||||||
menu.AddAction(TextCopy("video"), CopyText(media.Url));
|
AddCopyAction(menu, TextCopy("video"), media.Url);
|
||||||
menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor));
|
|
||||||
|
if (fileDownloadManager.SupportsFileSaving) {
|
||||||
|
menu.AddAction(TextSave("video"), () => fileDownloadManager.SaveVideo(media.Url, tweet?.MediaAuthor));
|
||||||
|
}
|
||||||
|
|
||||||
menu.AddSeparator();
|
menu.AddSeparator();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -74,22 +85,26 @@ public virtual void Show(IContextMenuBuilder menu, Context context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void AddSearchSelectionItems(IContextMenuBuilder menu, string selectedText) {
|
protected virtual void AddSearchSelectionItems(IContextMenuBuilder menu, string selectedText) {
|
||||||
menu.AddAction("Search in browser", () => {
|
if (App.SystemHandler.SearchText is {} searchText) {
|
||||||
App.SystemHandler.SearchText(selectedText);
|
menu.AddAction("Search in browser", () => {
|
||||||
DeselectAll();
|
searchText(selectedText);
|
||||||
});
|
DeselectAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void DeselectAll() {
|
protected void DeselectAll() {
|
||||||
browser.RunScript("gen:deselect", "window.getSelection().removeAllRanges()");
|
browser.RunScript("gen:deselect", "window.getSelection().removeAllRanges()");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Action OpenLink(string url) {
|
protected static void AddOpenAction(IContextMenuBuilder menu, string title, string url) {
|
||||||
return () => App.SystemHandler.OpenBrowser(url);
|
menu.AddAction(title, () => App.SystemHandler.OpenBrowser(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Action CopyText(string text) {
|
protected static void AddCopyAction(IContextMenuBuilder menu, string title, string textToCopy) {
|
||||||
return () => App.SystemHandler.CopyText(text);
|
if (App.SystemHandler.CopyText is {} copyText) {
|
||||||
|
menu.AddAction(title, () => copyText(textToCopy));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features {
|
namespace TweetLib.Core.Features {
|
||||||
public class BaseResourceRequestHandler : IResourceRequestHandler {
|
internal class BaseResourceRequestHandler : IResourceRequestHandler {
|
||||||
private static readonly Regex TweetDeckResourceUrl = new (@"/dist/(.*?)\.(.*?)\.(css|js)$");
|
private static readonly Regex TweetDeckResourceUrl = new (@"/dist/(.*?)\.(.*?)\.(css|js)$");
|
||||||
private static readonly SortedList<string, string> TweetDeckHashes = new (4);
|
private static readonly SortedList<string, string> TweetDeckHashes = new (4);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
@ -7,16 +8,21 @@
|
|||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features {
|
namespace TweetLib.Core.Features {
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
public sealed class FileDownloadManager {
|
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) {
|
private readonly IFileDownloader fileDownloader;
|
||||||
this.browser = browser;
|
|
||||||
|
internal FileDownloadManager(IFileDownloader fileDownloader) {
|
||||||
|
this.fileDownloader = fileDownloader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadTempImage(string url, Action<string> process) {
|
private void DownloadTempImage(string url, Action<string> process) {
|
||||||
string? staticFileName = TwitterUrls.GetImageFileName(url);
|
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)) {
|
if (staticFileName != null && FileUtils.FileExistsAndNotEmpty(file)) {
|
||||||
process(file);
|
process(file);
|
||||||
@ -27,14 +33,18 @@ void OnSuccess() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void OnFailure(Exception ex) {
|
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) {
|
public void ViewImage(string url) {
|
||||||
|
if (App.SystemHandler.OpenAssociatedProgram == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DownloadTempImage(url, static path => {
|
DownloadTempImage(url, static path => {
|
||||||
string ext = Path.GetExtension(path);
|
string ext = Path.GetExtension(path);
|
||||||
|
|
||||||
@ -42,16 +52,24 @@ public void ViewImage(string url) {
|
|||||||
App.SystemHandler.OpenAssociatedProgram(path);
|
App.SystemHandler.OpenAssociatedProgram(path);
|
||||||
}
|
}
|
||||||
else {
|
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) {
|
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) {
|
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) {
|
if (urls.Length == 0) {
|
||||||
return;
|
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 }) }
|
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) {
|
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) {
|
if (oneImage) {
|
||||||
browser.FileDownloader.DownloadFile(firstImageLink, path, null, OnFailure);
|
fileDownloader.DownloadFile(firstImageLink, path, null, OnFailure);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
string pathBase = Path.ChangeExtension(path, null);
|
string pathBase = Path.ChangeExtension(path, null);
|
||||||
string pathExt = Path.GetExtension(path);
|
string pathExt = Path.GetExtension(path);
|
||||||
|
|
||||||
for (int index = 0; index < urls.Length; index++) {
|
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) {
|
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? filename = TwitterUrls.GetFileNameFromUrl(url);
|
||||||
string? ext = Path.GetExtension(filename);
|
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 }) }
|
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) {
|
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;
|
private readonly PluginManager pluginManager;
|
||||||
|
|
||||||
protected Tweet(IBrowserComponent browserComponent, INotificationInterface notificationInterface, ICommonInterface commonInterface, PluginManager pluginManager, Func<NotificationBrowser, BrowserSetup> setup) : base(browserComponent, setup) {
|
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.PageLoadEnd += BrowserComponentOnPageLoadEnd;
|
||||||
|
this.browserComponent.AttachBridgeObject("$TD", new NotificationBridgeObject(notificationInterface, commonInterface));
|
||||||
|
|
||||||
this.notificationInterface = notificationInterface;
|
this.notificationInterface = notificationInterface;
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
@ -56,10 +56,10 @@ public override void Show(IContextMenuBuilder menu, Context context) {
|
|||||||
menu.AddSeparator();
|
menu.AddSeparator();
|
||||||
|
|
||||||
if (context.Notification is {} notification) {
|
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)) {
|
if (!string.IsNullOrEmpty(notification.QuoteUrl)) {
|
||||||
menu.AddAction("Copy quoted tweet address", CopyText(notification.QuoteUrl!));
|
AddCopyAction(menu, "Copy quoted tweet address", notification.QuoteUrl!);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.AddSeparator();
|
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;
|
using TweetLib.Core.Systems.Configuration;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins.Config {
|
namespace TweetLib.Core.Features.Plugins.Config {
|
||||||
public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig {
|
internal sealed class PluginConfigInstance : IConfigInstance {
|
||||||
public T Instance { get; }
|
public PluginConfig Instance { get; }
|
||||||
|
|
||||||
private readonly string filename;
|
private readonly string filename;
|
||||||
|
|
||||||
public PluginConfigInstance(string filename, T instance) {
|
public PluginConfigInstance(string filename, PluginConfig instance) {
|
||||||
this.filename = filename;
|
|
||||||
this.Instance = instance;
|
this.Instance = instance;
|
||||||
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load() {
|
public void Load() {
|
||||||
@ -58,7 +58,7 @@ public void Reload() {
|
|||||||
public void Reset() {
|
public void Reset() {
|
||||||
try {
|
try {
|
||||||
File.Delete(filename);
|
File.Delete(filename);
|
||||||
Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins);
|
Instance.ResetToDefault();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
OnException("Could not delete the plugin configuration file.", e);
|
OnException("Could not delete the plugin configuration file.", e);
|
||||||
return;
|
return;
|
||||||
|
@ -11,12 +11,11 @@
|
|||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins {
|
namespace TweetLib.Core.Features.Plugins {
|
||||||
public sealed class PluginManager {
|
public sealed class PluginManager {
|
||||||
public string CustomPluginFolder => Path.Combine(App.PluginPath, PluginGroup.Custom.GetSubFolder());
|
|
||||||
|
|
||||||
public IEnumerable<Plugin> Plugins => plugins;
|
public IEnumerable<Plugin> Plugins => plugins;
|
||||||
public IEnumerable<InjectedString> NotificationInjections => bridge.NotificationInjections;
|
public IEnumerable<InjectedString> NotificationInjections => bridge.NotificationInjections;
|
||||||
|
|
||||||
public IPluginConfig Config { get; }
|
public PluginConfig Config { get; }
|
||||||
|
public string PluginFolder { get; }
|
||||||
public string PluginDataFolder { get; }
|
public string PluginDataFolder { get; }
|
||||||
|
|
||||||
public event EventHandler<PluginErrorEventArgs>? Reloaded;
|
public event EventHandler<PluginErrorEventArgs>? Reloaded;
|
||||||
@ -27,13 +26,18 @@ public sealed class PluginManager {
|
|||||||
|
|
||||||
private readonly HashSet<Plugin> plugins = new ();
|
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 = config;
|
||||||
this.Config.PluginChangedState += Config_PluginChangedState;
|
this.Config.PluginChangedState += Config_PluginChangedState;
|
||||||
|
this.PluginFolder = pluginFolder;
|
||||||
this.PluginDataFolder = pluginDataFolder;
|
this.PluginDataFolder = pluginDataFolder;
|
||||||
this.bridge = new PluginBridge(this);
|
this.bridge = new PluginBridge(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetPluginFolder(PluginGroup group) {
|
||||||
|
return Path.Combine(PluginFolder, group.GetSubFolder());
|
||||||
|
}
|
||||||
|
|
||||||
internal void Register(PluginEnvironment environment, IBrowserComponent browserComponent) {
|
internal void Register(PluginEnvironment environment, IBrowserComponent browserComponent) {
|
||||||
browserComponent.AttachBridgeObject("$TDP", bridge);
|
browserComponent.AttachBridgeObject("$TDP", bridge);
|
||||||
|
|
||||||
@ -47,7 +51,7 @@ public void Reload() {
|
|||||||
|
|
||||||
var errors = new List<string>(1);
|
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) {
|
if (result.HasValue) {
|
||||||
plugins.Add(result.Value);
|
plugins.Add(result.Value);
|
||||||
}
|
}
|
||||||
@ -111,7 +115,7 @@ public void ConfigurePlugin(Plugin plugin) {
|
|||||||
browserExecutor.RunFunction("TDPF_configurePlugin", plugin);
|
browserExecutor.RunFunction("TDPF_configurePlugin", plugin);
|
||||||
}
|
}
|
||||||
else if (plugin.HasConfig) {
|
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.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
|
using TweetLib.Browser.Request;
|
||||||
using TweetLib.Core.Features.Plugins.Enums;
|
using TweetLib.Core.Features.Plugins.Enums;
|
||||||
using TweetLib.Core.Resources;
|
using TweetLib.Core.Resources;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.Plugins {
|
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";
|
public string Protocol => "tdp";
|
||||||
|
|
||||||
private readonly CachingResourceProvider<T> resourceProvider;
|
private readonly ResourceCache resourceCache;
|
||||||
private readonly PluginBridge bridge;
|
private readonly PluginBridge bridge;
|
||||||
|
|
||||||
public PluginSchemeHandler(CachingResourceProvider<T> resourceProvider, PluginManager pluginManager) {
|
public PluginSchemeHandler(ResourceCache resourceCache, PluginManager pluginManager) {
|
||||||
this.resourceProvider = resourceProvider;
|
this.resourceCache = resourceCache;
|
||||||
this.bridge = pluginManager.bridge;
|
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)) {
|
if (!uri.IsAbsoluteUri || uri.Scheme != Protocol || !int.TryParse(uri.Authority, out var identifier)) {
|
||||||
return null;
|
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);
|
string path = string.Join("/", segments, 1, segments.Length - 1);
|
||||||
|
|
||||||
Plugin? plugin = bridge.GetPluginFromToken(identifier);
|
Plugin? plugin = bridge.GetPluginFromToken(identifier);
|
||||||
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(PluginFolder.Root, path);
|
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 {
|
namespace TweetLib.Core.Features.Plugins {
|
||||||
internal static class PluginScriptGenerator {
|
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 + '"')) + "]";
|
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.Plugins.Events;
|
||||||
using TweetLib.Core.Features.Twitter;
|
using TweetLib.Core.Features.Twitter;
|
||||||
using TweetLib.Core.Resources;
|
using TweetLib.Core.Resources;
|
||||||
using TweetLib.Core.Systems.Dialogs;
|
|
||||||
using TweetLib.Core.Systems.Updates;
|
using TweetLib.Core.Systems.Updates;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
using Version = TweetDuck.Version;
|
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)";
|
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 TweetDeckFunctions Functions { get; }
|
||||||
public FileDownloadManager FileDownloadManager => new (browserComponent);
|
public FileDownloadManager FileDownloadManager => new (browserComponent.FileDownloader);
|
||||||
|
|
||||||
private readonly ISoundNotificationHandler soundNotificationHandler;
|
private readonly ISoundNotificationHandler soundNotificationHandler;
|
||||||
private readonly PluginManager pluginManager;
|
private readonly PluginManager pluginManager;
|
||||||
private readonly UpdateChecker updateChecker;
|
|
||||||
|
|
||||||
private bool isBrowserReady;
|
private bool isBrowserReady;
|
||||||
private bool ignoreUpdateCheckError;
|
private bool ignoreUpdateCheckError;
|
||||||
private string? prevSoundNotificationPath = null;
|
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.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.soundNotificationHandler = soundNotificationHandler;
|
||||||
|
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
@ -45,13 +40,17 @@ public TweetDeckBrowser(IBrowserComponent browserComponent, ITweetDeckInterface
|
|||||||
this.pluginManager.Executed += pluginManager_Executed;
|
this.pluginManager.Executed += pluginManager_Executed;
|
||||||
this.pluginManager.Reload();
|
this.pluginManager.Reload();
|
||||||
|
|
||||||
this.updateChecker = updateChecker;
|
|
||||||
this.updateChecker.CheckFinished += updateChecker_CheckFinished;
|
|
||||||
|
|
||||||
this.browserComponent.BrowserLoaded += browserComponent_BrowserLoaded;
|
this.browserComponent.BrowserLoaded += browserComponent_BrowserLoaded;
|
||||||
this.browserComponent.PageLoadStart += browserComponent_PageLoadStart;
|
this.browserComponent.PageLoadStart += browserComponent_PageLoadStart;
|
||||||
this.browserComponent.PageLoadEnd += browserComponent_PageLoadEnd;
|
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.MuteToggled += UserConfiguration_GeneralEventHandler;
|
||||||
App.UserConfiguration.OptionsDialogClosed += UserConfiguration_GeneralEventHandler;
|
App.UserConfiguration.OptionsDialogClosed += UserConfiguration_GeneralEventHandler;
|
||||||
App.UserConfiguration.SoundNotificationChanged += UserConfiguration_SoundNotificationChanged;
|
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) {
|
private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
|
||||||
if (e.HasErrors) {
|
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) {
|
if (isBrowserReady) {
|
||||||
@ -115,11 +114,13 @@ private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
|
|||||||
|
|
||||||
private void pluginManager_Executed(object sender, PluginErrorEventArgs e) {
|
private void pluginManager_Executed(object sender, PluginErrorEventArgs e) {
|
||||||
if (e.HasErrors) {
|
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) {
|
private void updateChecker_CheckFinished(object sender, UpdateCheckEventArgs e) {
|
||||||
|
var updateChecker = (UpdateChecker) sender;
|
||||||
|
|
||||||
e.Result.Handle(update => {
|
e.Result.Handle(update => {
|
||||||
string tag = update.VersionTag;
|
string tag = update.VersionTag;
|
||||||
|
|
||||||
@ -201,14 +202,14 @@ public override void Show(IContextMenuBuilder menu, Context context) {
|
|||||||
base.Show(menu, context);
|
base.Show(menu, context);
|
||||||
|
|
||||||
if (context.Selection == null && context.Tweet is {} tweet) {
|
if (context.Selection == null && context.Tweet is {} tweet) {
|
||||||
menu.AddAction("Open tweet in browser", OpenLink(tweet.Url));
|
AddOpenAction(menu, "Open tweet in browser", tweet.Url);
|
||||||
menu.AddAction("Copy tweet address", CopyText(tweet.Url));
|
AddCopyAction(menu, "Copy tweet address", tweet.Url);
|
||||||
menu.AddAction("Screenshot tweet to clipboard", () => owner.Functions.TriggerTweetScreenshot(tweet.ColumnId, tweet.ChirpId));
|
menu.AddAction("Screenshot tweet to clipboard", () => owner.Functions.TriggerTweetScreenshot(tweet.ColumnId, tweet.ChirpId));
|
||||||
menu.AddSeparator();
|
menu.AddSeparator();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(tweet.QuoteUrl)) {
|
if (!string.IsNullOrEmpty(tweet.QuoteUrl)) {
|
||||||
menu.AddAction("Open quoted tweet in browser", OpenLink(tweet.QuoteUrl!));
|
AddOpenAction(menu, "Open quoted tweet in browser", tweet.QuoteUrl!);
|
||||||
menu.AddAction("Copy quoted tweet address", CopyText(tweet.QuoteUrl!));
|
AddCopyAction(menu, "Copy quoted tweet address", tweet.QuoteUrl!);
|
||||||
menu.AddSeparator();
|
menu.AddSeparator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using TweetLib.Browser.Interfaces;
|
using TweetLib.Browser.Interfaces;
|
||||||
|
using TweetLib.Browser.Request;
|
||||||
using TweetLib.Core.Resources;
|
using TweetLib.Core.Resources;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
|
|
||||||
namespace TweetLib.Core.Features.TweetDeck {
|
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";
|
public string Protocol => "td";
|
||||||
|
|
||||||
private readonly CachingResourceProvider<T> resourceProvider;
|
private readonly ResourceCache resourceCache;
|
||||||
|
|
||||||
public TweetDuckSchemeHandler(CachingResourceProvider<T> resourceProvider) {
|
public TweetDuckSchemeHandler(ResourceCache resourceCache) {
|
||||||
this.resourceProvider = resourceProvider;
|
this.resourceCache = resourceCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T Resolve(Uri uri) {
|
public SchemeResource Resolve(Uri uri) {
|
||||||
string? rootPath = uri.Authority switch {
|
string? rootPath = uri.Authority switch {
|
||||||
"resources" => App.ResourcesPath,
|
"resources" => App.ResourcesPath,
|
||||||
"guide" => App.GuidePath,
|
"guide" => App.GuidePath,
|
||||||
@ -22,11 +26,11 @@ public T Resolve(Uri uri) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (rootPath == null) {
|
if (rootPath == null) {
|
||||||
return resourceProvider.Status(HttpStatusCode.NotFound, "Invalid URL.");
|
return InvalidUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
string filePath = FileUtils.ResolveRelativePathSafely(rootPath, uri.AbsolutePath.TrimStart('/'));
|
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 {
|
namespace TweetLib.Core {
|
||||||
public static class Lib {
|
public static class Lib {
|
||||||
public const string BrandName = "TweetDuck";
|
public const string BrandName = "TweetDuck";
|
||||||
|
public const string IssueTrackerUrl = "https://github.com/chylex/TweetDuck/issues";
|
||||||
|
|
||||||
public static CultureInfo Culture { get; } = CultureInfo.CurrentCulture;
|
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;
|
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||||
CultureInfo.DefaultThreadCurrentCulture = 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
|
CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
|
||||||
#endif
|
#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;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace TweetLib.Core.Systems.Configuration {
|
namespace TweetLib.Core.Systems.Configuration {
|
||||||
public abstract class BaseConfig {
|
public abstract class BaseConfig<T> : IConfigObject<T> where T : BaseConfig<T> {
|
||||||
internal T ConstructWithDefaults<T>() where T : BaseConfig {
|
public abstract T ConstructWithDefaults();
|
||||||
return (T) ConstructWithDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract BaseConfig ConstructWithDefaults();
|
protected void UpdatePropertyWithEvent<V>(ref V field, V value, EventHandler? eventHandler) {
|
||||||
|
if (!EqualityComparer<V>.Default.Equals(field, value)) {
|
||||||
protected void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler? eventHandler) {
|
|
||||||
if (!EqualityComparer<T>.Default.Equals(field, value)) {
|
|
||||||
field = value;
|
field = value;
|
||||||
eventHandler?.Invoke(this, EventArgs.Empty);
|
eventHandler?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void UpdatePropertyWithCallback<T>(ref T field, T value, Action action) {
|
protected void UpdatePropertyWithCallback<V>(ref V field, V value, Action action) {
|
||||||
if (!EqualityComparer<T>.Default.Equals(field, value)) {
|
if (!EqualityComparer<V>.Default.Equals(field, value)) {
|
||||||
field = value;
|
field = value;
|
||||||
action();
|
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;
|
using TweetLib.Utils.Serialization;
|
||||||
|
|
||||||
namespace TweetLib.Core.Systems.Configuration {
|
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 T Instance { get; }
|
||||||
public SimpleObjectSerializer<T> Serializer { get; }
|
|
||||||
|
private readonly SimpleObjectSerializer<T> serializer;
|
||||||
|
|
||||||
private readonly string filenameMain;
|
private readonly string filenameMain;
|
||||||
private readonly string filenameBackup;
|
private readonly string filenameBackup;
|
||||||
private readonly string identifier;
|
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.filenameMain = filename ?? throw new ArgumentNullException(nameof(filename), "Config file name must not be null!");
|
||||||
this.filenameBackup = filename + ".bak";
|
this.filenameBackup = filename + ".bak";
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
|
|
||||||
this.Instance = instance;
|
|
||||||
this.Serializer = new SimpleObjectSerializer<T>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadInternal(bool backup) {
|
private void LoadInternal(bool backup) {
|
||||||
Serializer.Read(backup ? filenameBackup : filenameMain, Instance);
|
serializer.Read(backup ? filenameBackup : filenameMain, Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load() {
|
public void Load() {
|
||||||
@ -63,7 +64,7 @@ public void Save() {
|
|||||||
File.Move(filenameMain, filenameBackup);
|
File.Move(filenameMain, filenameBackup);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serializer.Write(filenameMain, Instance);
|
serializer.Write(filenameMain, Instance);
|
||||||
} catch (SerializationSoftException e) {
|
} catch (SerializationSoftException e) {
|
||||||
OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", 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) {
|
} catch (Exception e) {
|
||||||
@ -76,7 +77,7 @@ public void Reload() {
|
|||||||
LoadInternal(false);
|
LoadInternal(false);
|
||||||
} catch (FileNotFoundException) {
|
} catch (FileNotFoundException) {
|
||||||
try {
|
try {
|
||||||
Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>());
|
serializer.Write(filenameMain, Instance.ConstructWithDefaults());
|
||||||
LoadInternal(false);
|
LoadInternal(false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
OnException($"Could not regenerate the configuration file for {identifier}.", e);
|
OnException($"Could not regenerate the configuration file for {identifier}.", e);
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
namespace TweetLib.Core.Systems.Configuration {
|
namespace TweetLib.Core.Systems.Configuration {
|
||||||
public interface IConfigInstance<out T> {
|
public interface IConfigInstance {
|
||||||
T Instance { get; }
|
|
||||||
|
|
||||||
void Save();
|
void Save();
|
||||||
void Reload();
|
|
||||||
void Reset();
|
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.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
using Version = TweetDuck.Version;
|
|
||||||
|
|
||||||
namespace TweetLib.Core.Systems.Updates {
|
namespace TweetLib.Core.Systems.Updates {
|
||||||
public sealed class UpdateInfo {
|
public sealed class UpdateInfo {
|
||||||
@ -49,7 +48,7 @@ internal void BeginSilentDownload() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WebClient client = WebUtils.NewClient($"{Lib.BrandName} {Version.Tag}");
|
WebClient client = WebUtils.NewClient();
|
||||||
|
|
||||||
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
|
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
|
||||||
DownloadStatus = UpdateDownloadStatus.Done;
|
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();
|
return build.Append(data.Substring(index)).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly TypeConverterRegistry converterRegistry;
|
||||||
private readonly Dictionary<string, PropertyInfo> props;
|
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.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) {
|
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))) {
|
using (StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))) {
|
||||||
foreach (KeyValuePair<string, PropertyInfo> prop in props) {
|
foreach (KeyValuePair<string, PropertyInfo> prop in props) {
|
||||||
Type type = prop.Value.PropertyType;
|
var type = prop.Value.PropertyType;
|
||||||
object value = prop.Value.GetValue(obj);
|
var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
|
||||||
|
|
||||||
if (!converters.TryGetValue(type, out ITypeConverter serializer)) {
|
if (converter.TryWriteType(type, prop.Value.GetValue(obj), out string? converted)) {
|
||||||
serializer = ClrTypeConverter.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serializer.TryWriteType(type, value, out string? converted)) {
|
|
||||||
if (converted != null) {
|
if (converted != null) {
|
||||||
writer.Write(prop.Key);
|
writer.Write(prop.Key);
|
||||||
writer.Write(' ');
|
writer.Write(' ');
|
||||||
@ -140,11 +132,10 @@ public void Read(string file, T obj) {
|
|||||||
string value = UnescapeLine(line.Substring(space + 1));
|
string value = UnescapeLine(line.Substring(space + 1));
|
||||||
|
|
||||||
if (props.TryGetValue(property, out PropertyInfo info)) {
|
if (props.TryGetValue(property, out PropertyInfo info)) {
|
||||||
if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)) {
|
var type = info.PropertyType;
|
||||||
serializer = ClrTypeConverter.Instance;
|
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);
|
info.SetValue(obj, converted);
|
||||||
}
|
}
|
||||||
else {
|
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