diff --git a/.devops/build-nuget.yaml b/.devops/build-nuget.yaml
index d28e7267..b5591865 100644
--- a/.devops/build-nuget.yaml
+++ b/.devops/build-nuget.yaml
@@ -1,5 +1,5 @@
variables:
- PackageVersion: 13.5.1.$(Build.BuildId)
+ PackageVersion: 19.0.9.$(Build.BuildId)
projectAPI: './ElectronNET.API/ElectronNET.API.csproj'
projectCLI: './ElectronNET.CLI/ElectronNET.CLI.csproj'
@@ -20,7 +20,7 @@ steps:
displayName: 'Use .NET Core sdk'
inputs:
packageType: sdk
- version: 5.0.203
+ version: 6.0.100
installationPath: $(Agent.ToolsDirectory)/dotnet
- task: DotNetCoreCLI@2
@@ -55,6 +55,7 @@ steps:
configuration: 'Release'
versioningScheme: 'off'
buildProperties: 'Version=$(PackageVersion)'
+ arguments: -IncludeReferencedProjects
- task: DotNetCoreCLI@2
inputs:
diff --git a/ElectronNET.API/App.cs b/ElectronNET.API/App.cs
index 274786c5..4aa0568d 100644
--- a/ElectronNET.API/App.cs
+++ b/ElectronNET.API/App.cs
@@ -7,6 +7,11 @@
using System.Threading;
using System.Threading.Tasks;
using ElectronNET.API.Extensions;
+using System.Runtime.Versioning;
+
+//TODO: Implement app.showEmojiPanel and app.isEmojiPanelSupported: https://www.electronjs.org/docs/api/app#appshowemojipanel-macos-windows
+//TODO: Implement app.moveToApplicationsFolder: https://www.electronjs.org/docs/api/app#appmovetoapplicationsfolderoptions-macos
+//TODO: Implement apprunningUnderRosettaTranslation: https://www.electronjs.org/docs/api/app#apprunningunderrosettatranslation-macos-readonly
namespace ElectronNET.API
{
@@ -20,6 +25,90 @@ public sealed class App
///
public static bool SocketDebug { get; set; }
+ ///
+ /// Handle hard fails of connecting to the socket. The application must exit when this event is raised.
+ /// The default behavior is to exit with code 0xDEAD
+ ///
+ public static event Action OnSocketConnectFail;
+
+ internal static bool TryRaiseOnSocketConnectFail()
+ {
+ if (OnSocketConnectFail is object)
+ {
+ OnSocketConnectFail();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Emitted when the user clicks on the dock on Mac
+ ///
+ ///
+ [SupportedOSPlatform("macos")]
+ public event Action Activate
+ {
+ add
+ {
+ if (_appActivate == null)
+ {
+ BridgeConnector.On("app-activate", () =>
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ _appActivate();
+ }
+ });
+ }
+ _appActivate += value;
+ }
+ remove
+ {
+ _appActivate -= value;
+
+ if (_appActivate == null)
+ {
+ BridgeConnector.Off("app-activate");
+ }
+ }
+ }
+
+ private event Action _appActivate;
+
+ ///
+ /// Emitted on the first instance when the user opens a second instance of the app, and the app is single instance
+ ///
+ ///
+ public event Action ActivateFromSecondInstance
+ {
+ add
+ {
+ if (_appActivateFromSecondInstance == null)
+ {
+ BridgeConnector.On("app-activate-from-second-instance", (args) =>
+ {
+ _appActivateFromSecondInstance(args);
+ });
+ }
+ _appActivateFromSecondInstance += value;
+ }
+ remove
+ {
+ _appActivateFromSecondInstance -= value;
+
+ if (_appActivateFromSecondInstance == null)
+ {
+ BridgeConnector.Off("app-activate-from-second-instance");
+ }
+ }
+ }
+
+ private event Action _appActivateFromSecondInstance;
+
+
///
/// Emitted when all windows have been closed.
///
@@ -337,6 +426,8 @@ public event Action WebContentsCreated
/// screen readers, are enabled or disabled. See https://www.chromium.org/developers/design-documents/accessibility for more details.
///
/// when Chrome's accessibility support is enabled, otherwise.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public event Action AccessibilitySupportChanged
{
add
@@ -411,6 +502,7 @@ internal set
///
/// On Windows, you have to parse the arguments using App.CommandLine to get the filepath.
///
+ [SupportedOSPlatform("macos")]
public event Action OpenFile
{
add
@@ -439,8 +531,7 @@ public event Action OpenFile
///
- /// Emitted when a MacOS user wants to open a URL with the application. Your application's Info.plist file must
- /// define the URL scheme within the CFBundleURLTypes key, and set NSPrincipalClass to AtomApplication.
+ /// Emitted when a user wants to open a URL with the application. See https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app for more information.
///
public event Action OpenUrl
{
@@ -495,9 +586,13 @@ public string Name
public Task GetNameAsync() => BridgeConnector.OnResult("appGetName", "appGetNameCompleted");
- internal App()
+ private App()
{
- CommandLine = new CommandLine();
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
+ {
+ AppContext.SetSwitch("System.Drawing.EnableUnixSupport", true);
+ }
+ CommandLine = CommandLine.Instance;
}
internal static App Instance
@@ -528,7 +623,7 @@ public static void ManuallySetIsReady()
}
private static App _app;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
///
/// Try to close all windows. The event will be emitted first. If all windows are successfully
@@ -599,6 +694,7 @@ public void Focus()
///
/// You should seek to use the option as sparingly as possible.
///
+ [SupportedOSPlatform("macos")]
public void Focus(FocusOptions focusOptions)
{
BridgeConnector.Emit("appFocus", JObject.FromObject(focusOptions, _jsonSerializer));
@@ -607,6 +703,7 @@ public void Focus(FocusOptions focusOptions)
///
/// Hides all application windows without minimizing them.
///
+ [SupportedOSPlatform("macos")]
public void Hide()
{
BridgeConnector.Emit("appHide");
@@ -615,6 +712,7 @@ public void Hide()
///
/// Shows application windows after they were hidden. Does not automatically focus them.
///
+ [SupportedOSPlatform("macos")]
public void Show()
{
BridgeConnector.Emit("appShow");
@@ -688,6 +786,8 @@ public void SetPath(PathName name, string path)
/// list from the task bar, and on macOS you can visit it from dock menu.
///
/// Path to add.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public void AddRecentDocument(string path)
{
BridgeConnector.Emit("appAddRecentDocument", path);
@@ -696,6 +796,8 @@ public void AddRecentDocument(string path)
///
/// Clears the recent documents list.
///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public void ClearRecentDocuments()
{
BridgeConnector.Emit("appClearRecentDocuments");
@@ -726,6 +828,8 @@ public void ClearRecentDocuments()
/// call this method with electron as the parameter.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public async Task SetAsDefaultProtocolClientAsync(string protocol, CancellationToken cancellationToken = default)
{
return await SetAsDefaultProtocolClientAsync(protocol, null, null, cancellationToken);
@@ -757,6 +861,8 @@ public async Task SetAsDefaultProtocolClientAsync(string protocol, Cancell
/// The path to the Electron executable. Defaults to process.execPath
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public async Task SetAsDefaultProtocolClientAsync(string protocol, string path, CancellationToken cancellationToken = default)
{
return await SetAsDefaultProtocolClientAsync(protocol, path, null, cancellationToken);
@@ -789,6 +895,8 @@ public async Task SetAsDefaultProtocolClientAsync(string protocol, string
/// Arguments passed to the executable. Defaults to an empty array.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public Task SetAsDefaultProtocolClientAsync(string protocol, string path, string[] args, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appSetAsDefaultProtocolClient", "appSetAsDefaultProtocolClientCompleted", cancellationToken, protocol, path, args);
///
@@ -798,6 +906,8 @@ public async Task SetAsDefaultProtocolClientAsync(string protocol, string
/// The name of your protocol, without ://.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public async Task RemoveAsDefaultProtocolClientAsync(string protocol, CancellationToken cancellationToken = default)
{
return await RemoveAsDefaultProtocolClientAsync(protocol, null, null, cancellationToken);
@@ -811,6 +921,8 @@ public async Task RemoveAsDefaultProtocolClientAsync(string protocol, Canc
/// Defaults to process.execPath.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public async Task RemoveAsDefaultProtocolClientAsync(string protocol, string path, CancellationToken cancellationToken = default)
{
return await RemoveAsDefaultProtocolClientAsync(protocol, path, null, cancellationToken);
@@ -825,6 +937,8 @@ public async Task RemoveAsDefaultProtocolClientAsync(string protocol, stri
/// Defaults to an empty array.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public Task RemoveAsDefaultProtocolClientAsync(string protocol, string path, string[] args, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appRemoveAsDefaultProtocolClient", "appRemoveAsDefaultProtocolClientCompleted", cancellationToken, protocol, path, args);
@@ -841,6 +955,8 @@ public async Task RemoveAsDefaultProtocolClientAsync(string protocol, stri
/// The name of your protocol, without ://.
/// The cancellation token.
/// Whether the current executable is the default handler for a protocol (aka URI scheme).
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public async Task IsDefaultProtocolClientAsync(string protocol, CancellationToken cancellationToken = default)
{
return await IsDefaultProtocolClientAsync(protocol, null, null, cancellationToken);
@@ -860,6 +976,8 @@ public async Task IsDefaultProtocolClientAsync(string protocol, Cancellati
/// Defaults to process.execPath.
/// The cancellation token.
/// Whether the current executable is the default handler for a protocol (aka URI scheme).
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public async Task IsDefaultProtocolClientAsync(string protocol, string path, CancellationToken cancellationToken = default)
{
return await IsDefaultProtocolClientAsync(protocol, path, null, cancellationToken);
@@ -880,6 +998,8 @@ public async Task IsDefaultProtocolClientAsync(string protocol, string pat
/// Defaults to an empty array.
/// The cancellation token.
/// Whether the current executable is the default handler for a protocol (aka URI scheme).
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public Task IsDefaultProtocolClientAsync(string protocol, string path, string[] args, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appIsDefaultProtocolClient", "appIsDefaultProtocolClientCompleted", cancellationToken, protocol, path, args);
@@ -891,6 +1011,7 @@ public async Task IsDefaultProtocolClientAsync(string protocol, string pat
/// Array of objects.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("windows")]
public Task SetUserTasksAsync(UserTask[] userTasks, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appSetUserTasks", "appSetUserTasksCompleted", cancellationToken, JArray.FromObject(userTasks, _jsonSerializer));
///
@@ -898,6 +1019,7 @@ public async Task IsDefaultProtocolClientAsync(string protocol, string pat
///
/// The cancellation token.
/// Jump List settings.
+ [SupportedOSPlatform("windows")]
public Task GetJumpListSettingsAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appGetJumpListSettings", "appGetJumpListSettingsCompleted", cancellationToken);
///
@@ -916,6 +1038,7 @@ public async Task IsDefaultProtocolClientAsync(string protocol, string pat
/// omitted from the Jump List. The list of removed items can be obtained using .
///
/// Array of objects.
+ [SupportedOSPlatform("windows")]
public void SetJumpList(JumpListCategory[] categories)
{
BridgeConnector.Emit("appSetJumpList", JArray.FromObject(categories, _jsonSerializer));
@@ -991,6 +1114,7 @@ public void ReleaseSingleInstanceLock()
///
/// Uniquely identifies the activity. Maps to NSUserActivity.activityType .
/// App-specific state to store for use by another device.
+ [SupportedOSPlatform("macos")]
public void SetUserActivity(string type, object userInfo)
{
SetUserActivity(type, userInfo, null);
@@ -1008,6 +1132,7 @@ public void SetUserActivity(string type, object userInfo)
///
/// The webpage to load in a browser if no suitable app is installed on the resuming device. The scheme must be http or https.
///
+ [SupportedOSPlatform("macos")]
public void SetUserActivity(string type, object userInfo, string webpageUrl)
{
BridgeConnector.Emit("appSetUserActivity", type, userInfo, webpageUrl);
@@ -1017,12 +1142,14 @@ public void SetUserActivity(string type, object userInfo, string webpageUrl)
/// The type of the currently running activity.
///
/// The cancellation token.
+ [SupportedOSPlatform("macos")]
public Task GetCurrentActivityTypeAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appGetCurrentActivityType", "appGetCurrentActivityTypeCompleted", cancellationToken);
///
/// Invalidates the current Handoff user activity.
///
+ [SupportedOSPlatform("macos")]
public void InvalidateCurrentActivity()
{
BridgeConnector.Emit("appInvalidateCurrentActivity");
@@ -1031,6 +1158,7 @@ public void InvalidateCurrentActivity()
///
/// Marks the current Handoff user activity as inactive without invalidating it.
///
+ [SupportedOSPlatform("macos")]
public void ResignCurrentActivity()
{
BridgeConnector.Emit("appResignCurrentActivity");
@@ -1040,6 +1168,7 @@ public void ResignCurrentActivity()
/// Changes the Application User Model ID to id.
///
/// Model Id.
+ [SupportedOSPlatform("windows")]
public void SetAppUserModelId(string id)
{
BridgeConnector.Emit("appSetAppUserModelId", id);
@@ -1054,6 +1183,7 @@ public void SetAppUserModelId(string id)
///
/// The cancellation token.
/// Result of import. Value of 0 indicates success.
+ [SupportedOSPlatform("linux")]
public Task ImportCertificateAsync(ImportCertificateOptions options, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appImportCertificate", "appImportCertificateCompleted", cancellationToken, JObject.FromObject(options, _jsonSerializer));
///
@@ -1084,12 +1214,16 @@ public void SetAppUserModelId(string id)
/// Counter badge.
/// The cancellation token.
/// Whether the call succeeded.
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
public Task SetBadgeCountAsync(int count, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appSetBadgeCount", "appSetBadgeCountCompleted", cancellationToken, count);
///
/// The current value displayed in the counter badge.
///
/// The cancellation token.
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
public Task GetBadgeCountAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appGetBadgeCount", "appGetBadgeCountCompleted", cancellationToken);
///
@@ -1101,12 +1235,15 @@ public void SetAppUserModelId(string id)
/// Whether the current desktop environment is Unity launcher.
///
/// The cancellation token.
+ [SupportedOSPlatform("linux")]
public Task IsUnityRunningAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appIsUnityRunning", "appIsUnityRunningCompleted", cancellationToken);
///
/// If you provided path and args options to then you need to pass the same
/// arguments here for to be set correctly.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public async Task GetLoginItemSettingsAsync(CancellationToken cancellationToken = default)
{
return await GetLoginItemSettingsAsync(null, cancellationToken);
@@ -1118,6 +1255,8 @@ public async Task GetLoginItemSettingsAsync(CancellationToken
///
///
/// The cancellation token.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task GetLoginItemSettingsAsync(LoginItemSettingsOptions options, CancellationToken cancellationToken = default) =>
options is null ? BridgeConnector.OnResult("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken)
: BridgeConnector.OnResult("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken, JObject.FromObject(options, _jsonSerializer));
@@ -1128,6 +1267,8 @@ public Task GetLoginItemSettingsAsync(LoginItemSettingsOption
/// you'll want to set the launch path to Update.exe, and pass arguments that specify your application name.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetLoginItemSettings(LoginSettings loginSettings)
{
BridgeConnector.Emit("appSetLoginItemSettings", JObject.FromObject(loginSettings, _jsonSerializer));
@@ -1139,6 +1280,8 @@ public void SetLoginItemSettings(LoginSettings loginSettings)
/// See Chromium's accessibility docs for more details.
///
/// if Chrome’s accessibility support is enabled, otherwise.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task IsAccessibilitySupportEnabledAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appIsAccessibilitySupportEnabled", "appIsAccessibilitySupportEnabledCompleted", cancellationToken);
@@ -1152,6 +1295,8 @@ public void SetLoginItemSettings(LoginSettings loginSettings)
/// Note: Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default.
///
/// Enable or disable accessibility tree rendering.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetAccessibilitySupportEnabled(bool enabled)
{
BridgeConnector.Emit("appSetAboutPanelOptions", enabled);
@@ -1182,6 +1327,13 @@ public void SetAboutPanelOptions(AboutPanelOptions options)
BridgeConnector.Emit("appSetAboutPanelOptions", JObject.FromObject(options, _jsonSerializer));
}
+ ///
+ /// Fetches a path's associated icon.
+ ///
+ ///
+ ///
+ public Task GetFileIcon(string path) => BridgeConnector.OnResult("appGetFileIcon", "appGetFileIconCompleted", path);
+
///
/// A which is the user agent string Electron will use as a global fallback.
///
@@ -1244,7 +1396,14 @@ internal void PreventQuit()
/// The handler
public void Once(string eventName, Action fn) => Events.Instance.Once(ModuleName, eventName, fn);
- private readonly JsonSerializer _jsonSerializer = new JsonSerializer()
+
+ ///
+ /// If you're using a splashscreen in the electron.manifest.json, the window will ony be fully destroyed once you call this method once.
+ /// You should only do this after creating another window, to avoid a bug where the Electron renderer process frezees till any window interaction.
+ ///
+ public void DestroySplashScreen() => BridgeConnector.Emit("splashscreen-destroy");
+
+ private readonly JsonSerializer _jsonSerializer = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
diff --git a/ElectronNET.API/AutoUpdater.cs b/ElectronNET.API/AutoUpdater.cs
index d21ed041..2053d8d0 100644
--- a/ElectronNET.API/AutoUpdater.cs
+++ b/ElectronNET.API/AutoUpdater.cs
@@ -313,7 +313,7 @@ public event Action OnUpdateDownloaded
private event Action _updateDownloaded;
private static AutoUpdater _autoUpdater;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal AutoUpdater() { }
@@ -426,6 +426,7 @@ public Task CheckForUpdatesAndNotifyAsync()
/// Run the app after finish even on silent install. Not applicable for macOS. Ignored if `isSilent` is set to `false`.
public void QuitAndInstall(bool isSilent = false, bool isForceRunAfter = false)
{
+ BridgeConnector.EmitSync("prepare-for-update");
BridgeConnector.EmitSync("autoUpdaterQuitAndInstall", isSilent, isForceRunAfter);
}
diff --git a/ElectronNET.API/BridgeConnector.cs b/ElectronNET.API/BridgeConnector.cs
index 1088f9e1..66df3cde 100644
--- a/ElectronNET.API/BridgeConnector.cs
+++ b/ElectronNET.API/BridgeConnector.cs
@@ -4,8 +4,10 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
+using Nito.AsyncEx;
using SocketIOClient;
using SocketIOClient.JsonSerializer;
using SocketIOClient.Newtonsoft.Json;
@@ -85,26 +87,63 @@ internal static void DoneWith(string key, string eventKey, TaskCompletionSource<
private static SocketIO _socket;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
+
+ private static readonly SemaphoreSlim _socketSemaphoreEmit = new(1, 1);
+ private static readonly SemaphoreSlim _socketSemaphoreHandlers = new(1, 1);
+
+ private static AsyncManualResetEvent _connectedSocketEvent = new AsyncManualResetEvent();
+
+ private static Dictionary> _eventHandlers = new ();
+
+ private static Task _waitForConnection
+ {
+ get
+ {
+ EnsureSocketTaskIsCreated();
+ return GetSocket();
+ }
+ }
+
+ private static async Task GetSocket()
+ {
+ await _connectedSocketEvent.WaitAsync();
+ return _socket;
+ }
+
+ public static bool IsConnected => _waitForConnection is Task task && task.IsCompletedSuccessfully;
public static void Emit(string eventString, params object[] args)
{
//We don't care about waiting for the event to be emitted, so this doesn't need to be async
+
+ Task.Run(() => EmitAsync(eventString, args));
+ }
- Task.Run(async () =>
+ private static async Task EmitAsync(string eventString, object[] args)
+ {
+ if (App.SocketDebug)
{
- if (App.SocketDebug)
- {
- Console.WriteLine($"Sending event {eventString}");
- }
+ Log("Sending event {0}", eventString);
+ }
- await Socket.EmitAsync(eventString, args);
+ var socket = await _waitForConnection;
- if (App.SocketDebug)
- {
- Console.WriteLine($"Sent event {eventString}");
- }
- });
+ await _socketSemaphoreEmit.WaitAsync();
+
+ try
+ {
+ await socket.EmitAsync(eventString, args);
+ }
+ finally
+ {
+ _socketSemaphoreEmit.Release();
+ }
+
+ if (App.SocketDebug)
+ {
+ Log($"Sent event {eventString}");
+ }
}
///
@@ -116,30 +155,128 @@ internal static void EmitSync(string eventString, params object[] args)
{
if (App.SocketDebug)
{
- Console.WriteLine($"Sending event {eventString}");
+ Log("Sending event {0}", eventString);
}
- Socket.EmitAsync(eventString, args).Wait();
+ Task.Run(async () =>
+ {
+ var socket = await _waitForConnection;
+ try
+ {
+ await _socketSemaphoreEmit.WaitAsync();
+ await socket.EmitAsync(eventString, args);
+ }
+ finally
+ {
+ _socketSemaphoreEmit.Release();
+ }
+ }).Wait();
+
if (App.SocketDebug)
{
- Console.WriteLine($"Sent event {eventString}");
+ Log("Sent event {0}", eventString);
}
}
public static void Off(string eventString)
{
- Socket.Off(eventString);
+ EnsureSocketTaskIsCreated();
+
+ _socketSemaphoreHandlers.Wait();
+ try
+ {
+ if (_eventHandlers.ContainsKey(eventString))
+ {
+ _eventHandlers.Remove(eventString);
+ }
+
+ _socket.Off(eventString);
+ }
+ finally
+ {
+ _socketSemaphoreHandlers.Release();
+ }
}
public static void On(string eventString, Action fn)
{
- Socket.On(eventString, _ => fn());
+ EnsureSocketTaskIsCreated();
+
+ _socketSemaphoreHandlers.Wait();
+ try
+ {
+ if (_eventHandlers.ContainsKey(eventString))
+ {
+ _eventHandlers.Remove(eventString);
+ }
+
+ _eventHandlers.Add(eventString, _ =>
+ {
+ try
+ {
+ fn();
+ }
+ catch (Exception E)
+ {
+ LogError(E, "Error running handler for event {0}", eventString);
+ }
+ });
+
+ _socket.On(eventString, _eventHandlers[eventString]);
+ }
+ finally
+ {
+ _socketSemaphoreHandlers.Release();
+ }
}
public static void On(string eventString, Action fn)
{
- Socket.On(eventString, (o) => fn(o.GetValue(0)));
+ EnsureSocketTaskIsCreated();
+
+ _socketSemaphoreHandlers.Wait();
+ try
+ {
+ if (_eventHandlers.ContainsKey(eventString))
+ {
+ _eventHandlers.Remove(eventString);
+ }
+
+ _eventHandlers.Add(eventString, o =>
+ {
+ try
+ {
+ fn(o.GetValue(0));
+ }
+ catch (Exception E)
+ {
+ LogError(E, "Error running handler for event {0}", eventString);
+ }
+ });
+
+ _socket.On(eventString, _eventHandlers[eventString]);
+ }
+ finally
+ {
+ _socketSemaphoreHandlers.Release();
+ }
+ }
+
+ private static void RehookHandlers(SocketIO newSocket)
+ {
+ _socketSemaphoreHandlers.Wait();
+ try
+ {
+ foreach (var kv in _eventHandlers)
+ {
+ newSocket.On(kv.Key, kv.Value);
+ }
+ }
+ finally
+ {
+ _socketSemaphoreHandlers.Release();
+ }
}
public static void Once(string eventString, Action fn)
@@ -195,7 +332,7 @@ public static async Task OnResult(string triggerEvent, string completedEve
EventTasks.DoneWith(completedEvent, eventKey, taskCompletionSource);
});
- Emit(triggerEvent, args);
+ await EmitAsync(triggerEvent, args);
}
}
@@ -256,61 +393,160 @@ public static async Task OnResult(string triggerEvent, string completedEve
return await taskCompletionSource.Task;
}
- private static SocketIO Socket
+
+ internal static void Log(string formatString, params object[] args)
{
- get
+ if (Logger is object)
{
- if (_socket is null)
+ Logger.LogInformation(formatString, args);
+ }
+ else
+ {
+ Console.WriteLine(formatString, args);
+ }
+ }
+
+ internal static void LogError(Exception E, string formatString, params object[] args)
+ {
+ if (Logger is object)
+ {
+ Logger.LogError(E, formatString, args);
+ }
+ else
+ {
+ Console.WriteLine(formatString, args);
+ Console.WriteLine(E.ToString());
+ }
+ }
+
+ private static Thread _backgroundMonitorThread;
+
+ private static void EnsureSocketTaskIsCreated()
+ {
+ if (_socket is null)
+ {
+ if(string.IsNullOrWhiteSpace(AuthKey))
{
- if (HybridSupport.IsElectronActive)
- {
+ throw new Exception("You must call Electron.ReadAuth() first thing on your main entry point.");
+ }
- lock (_syncRoot)
+ if (HybridSupport.IsElectronActive)
+ {
+ lock (_syncRoot)
+ {
+ if (_socket is null)
{
- if (_socket is null && HybridSupport.IsElectronActive)
+ if (HybridSupport.IsElectronActive)
{
var socket = new SocketIO($"http://localhost:{BridgeSettings.SocketPort}", new SocketIOOptions()
{
- EIO = 3
+ EIO = 4,
+ Reconnection = true,
+ ReconnectionAttempts = int.MaxValue,
+ ReconnectionDelay = 500,
+ ReconnectionDelayMax = 2000,
+ RandomizationFactor = 0.5,
+ ConnectionTimeout = TimeSpan.FromSeconds(10),
+ Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
- socket.JsonSerializer = new CamelCaseNewtonsoftJsonSerializer(socket.Options.EIO);
+ socket.JsonSerializer = new CamelCaseNewtonsoftJsonSerializer();
+ _connectedSocketEvent.Reset();
socket.OnConnected += (_, __) =>
{
- Console.WriteLine("BridgeConnector connected!");
+ Task.Run(async () =>
+ {
+ await socket.EmitAsync("auth", AuthKey);
+ _connectedSocketEvent.Set();
+ Log("ElectronNET socket {1} connected on port {0}!", BridgeSettings.SocketPort, socket.Id);
+ });
+ };
+
+ socket.OnReconnectAttempt += (_, __) =>
+ {
+ _connectedSocketEvent.Reset();
+ Log("ElectronNET socket {1} is trying to reconnect on port {0}...", BridgeSettings.SocketPort, socket.Id);
+ };
+
+ socket.OnReconnectError += (_, ex) =>
+ {
+ _connectedSocketEvent.Reset();
+ Log("ElectronNET socket {1} failed to connect {0}", ex, socket.Id);
+ };
+
+
+ socket.OnReconnectFailed += (_, ex) =>
+ {
+ _connectedSocketEvent.Reset();
+ Log("ElectronNET socket {1} failed to reconnect {0}", ex, socket.Id);
};
- socket.ConnectAsync().Wait();
+ socket.OnReconnected += (_, __) =>
+ {
+ _connectedSocketEvent.Set();
+ Log("ElectronNET socket {1} reconnected on port {0}...", BridgeSettings.SocketPort, socket.Id);
+ };
+
+ socket.OnDisconnected += (_, reason) =>
+ {
+ _connectedSocketEvent.Reset();
+ Log("ElectronNET socket {2} disconnected with reason {0}, trying to reconnect on port {1}!", reason, BridgeSettings.SocketPort, socket.Id);
+ };
+
+ socket.OnError += (_, msg) =>
+ {
+ //_connectedSocketEvent.Reset();
+ Log("ElectronNET socket {1} error: {0}...", msg, socket.Id);
+ };
_socket = socket;
+
+ Task.Run(async () =>
+ {
+ try
+ {
+ await socket.ConnectAsync();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+
+ if (!App.TryRaiseOnSocketConnectFail())
+ {
+ Environment.Exit(0xDEAD);
+ }
+ }
+ });
+
+ RehookHandlers(socket);
+ }
+ else
+ {
+ throw new Exception("Missing Socket Port");
}
}
}
- else
- {
- throw new Exception("Missing Socket Port");
- }
}
-
- return _socket;
+ else
+ {
+ throw new Exception("Missing Socket Port");
+ }
}
}
+ internal static ILogger Logger { private get; set; }
+ internal static string AuthKey { get; set; } = null;
+
private class CamelCaseNewtonsoftJsonSerializer : NewtonsoftJsonSerializer
{
- public CamelCaseNewtonsoftJsonSerializer(int eio) : base(eio)
- {
- }
-
- public override JsonSerializerSettings CreateOptions()
+ public CamelCaseNewtonsoftJsonSerializer() : base()
{
- return new JsonSerializerSettings()
+ OptionsProvider = () => new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
- DefaultValueHandling = DefaultValueHandling.Ignore
};
}
}
diff --git a/ElectronNET.API/BrowserView.cs b/ElectronNET.API/BrowserView.cs
index 09a44cbd..cd29dfe3 100644
--- a/ElectronNET.API/BrowserView.cs
+++ b/ElectronNET.API/BrowserView.cs
@@ -33,6 +33,10 @@ public class BrowserView
///
public Task GetBoundsAsync() => BridgeConnector.OnResult("browserView-getBounds", "browserView-getBounds-reply" + Id, Id);
+ ///
+ /// Set the bounds of the current view inside the window
+ ///
+ ///
public void SetBounds(Rectangle value)
{
BridgeConnector.Emit("browserView-setBounds", Id, value);
diff --git a/ElectronNET.API/BrowserWindow.cs b/ElectronNET.API/BrowserWindow.cs
index 0b8222c5..a39e26bd 100644
--- a/ElectronNET.API/BrowserWindow.cs
+++ b/ElectronNET.API/BrowserWindow.cs
@@ -7,8 +7,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
+//TODO: Add setTrafficLightPosition and getTrafficLightPosition: https://www.electronjs.org/docs/api/browser-window#winsettrafficlightpositionposition-macos
+
namespace ElectronNET.API
{
///
@@ -146,6 +149,7 @@ public event Action OnClosed
///
/// Emitted when window session is going to end due to force shutdown or machine restart or session log off.
///
+ [SupportedOSPlatform("windows")]
public event Action OnSessionEnd
{
add
@@ -465,6 +469,8 @@ public event Action OnRestore
///
/// Emitted when the window is being resized.
///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public event Action OnResize
{
add
@@ -496,6 +502,8 @@ public event Action OnResize
///
/// Note: On macOS this event is just an alias of moved.
///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public event Action OnMove
{
add
@@ -523,8 +531,10 @@ public event Action OnMove
private event Action _move;
///
- /// macOS: Emitted once when the window is moved to a new position.
+ /// Emitted once when the window is moved to a new position.
///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public event Action OnMoved
{
add
@@ -676,6 +686,8 @@ public event Action OnLeaveHtmlFullScreen
/// and the APPCOMMAND_ prefix is stripped off.e.g.APPCOMMAND_BROWSER_BACKWARD
/// is emitted as browser-backward.
///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("windows")]
public event Action OnAppCommand
{
add
@@ -705,6 +717,7 @@ public event Action OnAppCommand
///
/// Emitted when scroll wheel event phase has begun.
///
+ [SupportedOSPlatform("macos")]
public event Action OnScrollTouchBegin
{
add
@@ -734,6 +747,7 @@ public event Action OnScrollTouchBegin
///
/// Emitted when scroll wheel event phase has ended.
///
+ [SupportedOSPlatform("macos")]
public event Action OnScrollTouchEnd
{
add
@@ -763,6 +777,7 @@ public event Action OnScrollTouchEnd
///
/// Emitted when scroll wheel event phase filed upon reaching the edge of element.
///
+ [SupportedOSPlatform("macos")]
public event Action OnScrollTouchEdge
{
add
@@ -792,6 +807,7 @@ public event Action OnScrollTouchEdge
///
/// Emitted on 3-finger swipe. Possible directions are up, right, down, left.
///
+ [SupportedOSPlatform("macos")]
public event Action OnSwipe
{
add
@@ -821,6 +837,7 @@ public event Action OnSwipe
///
/// Emitted when the window opens a sheet.
///
+ [SupportedOSPlatform("macos")]
public event Action OnSheetBegin
{
add
@@ -850,6 +867,7 @@ public event Action OnSheetBegin
///
/// Emitted when the window has closed a sheet.
///
+ [SupportedOSPlatform("macos")]
public event Action OnSheetEnd
{
add
@@ -879,6 +897,7 @@ public event Action OnSheetEnd
///
/// Emitted when the native new tab button is clicked.
///
+ [SupportedOSPlatform("macos")]
public event Action OnNewWindowForTab
{
add
@@ -1056,6 +1075,14 @@ public void SetFullScreen(bool flag)
{
BridgeConnector.Emit("browserWindowSetFullScreen", Id, flag);
}
+
+ ///
+ /// Sets whether the background color of the window
+ ///
+ public void SetBackgroundColor(string color)
+ {
+ BridgeConnector.Emit("browserWindowSetBackgroundColor", Id, color);
+ }
///
/// Whether the window is in fullscreen mode.
@@ -1066,6 +1093,24 @@ public Task IsFullScreenAsync()
return BridgeConnector.OnResult("browserWindowIsFullScreen", "browserWindow-isFullScreen-completed" + Id, Id);
}
+ ///
+ /// This will make a window maintain an aspect ratio. The extra size allows a developer to have space,
+ /// specified in pixels, not included within the aspect ratio calculations. This API already takes into
+ /// account the difference between a window’s size and its content size.
+ ///
+ /// Consider a normal window with an HD video player and associated controls.Perhaps there are 15 pixels
+ /// of controls on the left edge, 25 pixels of controls on the right edge and 50 pixels of controls below
+ /// the player. In order to maintain a 16:9 aspect ratio (standard aspect ratio for HD @1920x1080) within
+ /// the player itself we would call this function with arguments of 16/9 and[40, 50]. The second argument
+ /// doesn’t care where the extra width and height are within the content view–only that they exist. Just
+ /// sum any extra width and height areas you have within the overall content view.
+ ///
+ /// The aspect ratio to maintain for some portion of the content view.
+ public void SetAspectRatio(int aspectRatio)
+ {
+ BridgeConnector.Emit("browserWindowSetAspectRatio", Id, aspectRatio, new Size() { Height = 0, Width = 0 });
+ }
+
///
/// This will make a window maintain an aspect ratio. The extra size allows a developer to have space,
/// specified in pixels, not included within the aspect ratio calculations. This API already takes into
@@ -1080,17 +1125,22 @@ public Task IsFullScreenAsync()
///
/// The aspect ratio to maintain for some portion of the content view.
/// The extra size not to be included while maintaining the aspect ratio.
+ [SupportedOSPlatform("macos")]
public void SetAspectRatio(int aspectRatio, Size extraSize)
{
BridgeConnector.Emit("browserWindowSetAspectRatio", Id, aspectRatio, extraSize);
}
+
+
+
///
/// Uses Quick Look to preview a file at a given path.
///
/// The absolute path to the file to preview with QuickLook. This is important as
/// Quick Look uses the file name and file extension on the path to determine the content type of the
/// file to open.
+ [SupportedOSPlatform("macos")]
public void PreviewFile(string path)
{
BridgeConnector.Emit("browserWindowPreviewFile", Id, path);
@@ -1104,6 +1154,7 @@ public void PreviewFile(string path)
/// file to open.
/// The name of the file to display on the Quick Look modal view. This is
/// purely visual and does not affect the content type of the file. Defaults to path.
+ [SupportedOSPlatform("macos")]
public void PreviewFile(string path, string displayname)
{
BridgeConnector.Emit("browserWindowPreviewFile", Id, path, displayname);
@@ -1112,6 +1163,7 @@ public void PreviewFile(string path, string displayname)
///
/// Closes the currently open Quick Look panel.
///
+ [SupportedOSPlatform("macos")]
public void CloseFilePreview()
{
BridgeConnector.Emit("browserWindowCloseFilePreview", Id);
@@ -1131,6 +1183,7 @@ public void SetBounds(Rectangle bounds)
///
///
///
+ [SupportedOSPlatform("macos")]
public void SetBounds(Rectangle bounds, bool animate)
{
BridgeConnector.Emit("browserWindowSetBounds", Id, bounds, animate);
@@ -1159,6 +1212,7 @@ public void SetContentBounds(Rectangle bounds)
///
///
///
+ [SupportedOSPlatform("macos")]
public void SetContentBounds(Rectangle bounds, bool animate)
{
BridgeConnector.Emit("browserWindowSetContentBounds", Id, bounds, animate);
@@ -1189,6 +1243,7 @@ public void SetSize(int width, int height)
///
///
///
+ [SupportedOSPlatform("macos")]
public void SetSize(int width, int height, bool animate)
{
BridgeConnector.Emit("browserWindowSetSize", Id, width, height, animate);
@@ -1219,6 +1274,7 @@ public void SetContentSize(int width, int height)
///
///
///
+ [SupportedOSPlatform("macos")]
public void SetContentSize(int width, int height, bool animate)
{
BridgeConnector.Emit("browserWindowSetContentSize", Id, width, height, animate);
@@ -1293,6 +1349,8 @@ public Task IsResizableAsync()
/// Sets whether the window can be moved by user. On Linux does nothing.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetMovable(bool movable)
{
BridgeConnector.Emit("browserWindowSetMovable", Id, movable);
@@ -1300,10 +1358,10 @@ public void SetMovable(bool movable)
///
/// Whether the window can be moved by user.
- ///
- /// On Linux always returns true.
///
/// On Linux always returns true.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task IsMovableAsync()
{
return BridgeConnector.OnResult("browserWindowIsMovable", "browserWindow-isMovable-completed" + Id, Id);
@@ -1313,6 +1371,8 @@ public Task IsMovableAsync()
/// Sets whether the window can be manually minimized by user. On Linux does nothing.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetMinimizable(bool minimizable)
{
BridgeConnector.Emit("browserWindowSetMinimizable", Id, minimizable);
@@ -1320,10 +1380,10 @@ public void SetMinimizable(bool minimizable)
///
/// Whether the window can be manually minimized by user.
- ///
- /// On Linux always returns true.
///
/// On Linux always returns true.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task IsMinimizableAsync()
{
return BridgeConnector.OnResult("browserWindowIsMinimizable", "browserWindow-isMinimizable-completed" + Id, Id);
@@ -1333,6 +1393,8 @@ public Task IsMinimizableAsync()
/// Sets whether the window can be manually maximized by user. On Linux does nothing.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetMaximizable(bool maximizable)
{
BridgeConnector.Emit("browserWindowSetMaximizable", Id, maximizable);
@@ -1340,10 +1402,10 @@ public void SetMaximizable(bool maximizable)
///
/// Whether the window can be manually maximized by user.
- ///
- /// On Linux always returns true.
///
/// On Linux always returns true.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task IsMaximizableAsync()
{
return BridgeConnector.OnResult("browserWindowIsMaximizable", "browserWindow-isMaximizable-completed" + Id, Id);
@@ -1371,6 +1433,8 @@ public Task IsFullScreenableAsync()
/// Sets whether the window can be manually closed by user. On Linux does nothing.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetClosable(bool closable)
{
BridgeConnector.Emit("browserWindowSetClosable", Id, closable);
@@ -1378,10 +1442,10 @@ public void SetClosable(bool closable)
///
/// Whether the window can be manually closed by user.
- ///
- /// On Linux always returns true.
///
/// On Linux always returns true.
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task IsClosableAsync()
{
return BridgeConnector.OnResult("browserWindowIsClosable", "browserWindow-isClosable-completed" + Id, Id);
@@ -1407,6 +1471,8 @@ public void SetAlwaysOnTop(bool flag)
/// Values include normal, floating, torn-off-menu, modal-panel, main-menu,
/// status, pop-up-menu and screen-saver. The default is floating.
/// See the macOS docs
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetAlwaysOnTop(bool flag, OnTopLevel level)
{
BridgeConnector.Emit("browserWindowSetAlwaysOnTop", Id, flag, level.GetDescription());
@@ -1423,6 +1489,7 @@ public void SetAlwaysOnTop(bool flag, OnTopLevel level)
/// See the macOS docs
/// The number of layers higher to set this window relative to the given level.
/// The default is 0. Note that Apple discourages setting levels higher than 1 above screen-saver.
+ [SupportedOSPlatform("macos")]
public void SetAlwaysOnTop(bool flag, OnTopLevel level, int relativeLevel)
{
BridgeConnector.Emit("browserWindowSetAlwaysOnTop", Id, flag, level.GetDescription(), relativeLevel);
@@ -1456,7 +1523,7 @@ public void SetPosition(int x, int y)
// https://github.com/electron/electron/issues/4045
if (isWindows10())
{
- x = x - 7;
+ x -= 7;
}
BridgeConnector.Emit("browserWindowSetPosition", Id, x, y);
@@ -1468,13 +1535,14 @@ public void SetPosition(int x, int y)
///
///
///
+ [SupportedOSPlatform("macos")]
public void SetPosition(int x, int y, bool animate)
{
// Workaround Windows 10 / Electron Bug
// https://github.com/electron/electron/issues/4045
if (isWindows10())
{
- x = x - 7;
+ x -= 7;
}
BridgeConnector.Emit("browserWindowSetPosition", Id, x, y, animate);
@@ -1482,7 +1550,7 @@ public void SetPosition(int x, int y, bool animate)
private bool isWindows10()
{
- return RuntimeInformation.OSDescription.Contains("Windows 10");
+ return OperatingSystem.IsWindowsVersionAtLeast(10);
}
///
@@ -1520,6 +1588,7 @@ public Task GetTitleAsync()
/// but you may want to display them beneath a HTML-rendered toolbar.
///
///
+ [SupportedOSPlatform("macos")]
public void SetSheetOffset(float offsetY)
{
BridgeConnector.Emit("browserWindowSetSheetOffset", Id, offsetY);
@@ -1532,6 +1601,7 @@ public void SetSheetOffset(float offsetY)
///
///
///
+ [SupportedOSPlatform("macos")]
public void SetSheetOffset(float offsetY, float offsetX)
{
BridgeConnector.Emit("browserWindowSetSheetOffset", Id, offsetY, offsetX);
@@ -1587,6 +1657,7 @@ public Task GetNativeWindowHandle()
/// and the icon of the file will show in window’s title bar.
///
///
+ [SupportedOSPlatform("macos")]
public void SetRepresentedFilename(string filename)
{
BridgeConnector.Emit("browserWindowSetRepresentedFilename", Id, filename);
@@ -1596,6 +1667,7 @@ public void SetRepresentedFilename(string filename)
/// The pathname of the file the window represents.
///
///
+ [SupportedOSPlatform("macos")]
public Task GetRepresentedFilenameAsync()
{
return BridgeConnector.OnResult("browserWindowGetRepresentedFilename", "browserWindow-getRepresentedFilename-completed" + Id, Id);
@@ -1606,6 +1678,7 @@ public Task GetRepresentedFilenameAsync()
/// and the icon in title bar will become gray when set to true.
///
///
+ [SupportedOSPlatform("macos")]
public void SetDocumentEdited(bool edited)
{
BridgeConnector.Emit("browserWindowSetDocumentEdited", Id, edited);
@@ -1615,6 +1688,7 @@ public void SetDocumentEdited(bool edited)
/// Whether the window’s document has been edited.
///
///
+ [SupportedOSPlatform("macos")]
public Task IsDocumentEditedAsync()
{
return BridgeConnector.OnResult("browserWindowIsDocumentEdited", "browserWindow-isDocumentEdited-completed" + Id, Id);
@@ -1672,13 +1746,15 @@ public void Reload()
/// The menu items.
///
public IReadOnlyCollection MenuItems { get { return _items.AsReadOnly(); } }
- private List _items = new List();
+ private readonly List _items = new();
///
/// Sets the menu as the window’s menu bar,
/// setting it to null will remove the menu bar.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("linux")]
public void SetMenu(MenuItem[] menuItems)
{
menuItems.AddMenuItemsId();
@@ -1696,6 +1772,8 @@ public void SetMenu(MenuItem[] menuItems)
///
/// Remove the window's menu bar.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("linux")]
public void RemoveMenu()
{
BridgeConnector.Emit("browserWindowRemoveMenu", Id);
@@ -1729,6 +1807,7 @@ public void SetProgressBar(double progress)
///
///
///
+ [SupportedOSPlatform("windows")]
public void SetProgressBar(double progress, ProgressBarOptions progressBarOptions)
{
BridgeConnector.Emit("browserWindowSetProgressBar", Id, progress, progressBarOptions);
@@ -1738,6 +1817,8 @@ public void SetProgressBar(double progress, ProgressBarOptions progressBarOption
/// Sets whether the window should have a shadow. On Windows and Linux does nothing.
///
///
+
+ [SupportedOSPlatform("macos")]
public void SetHasShadow(bool hasShadow)
{
BridgeConnector.Emit("browserWindowSetHasShadow", Id, hasShadow);
@@ -1762,7 +1843,7 @@ public Task HasShadowAsync()
///
public IReadOnlyCollection ThumbarButtons { get { return _thumbarButtons.AsReadOnly(); } }
- private List _thumbarButtons = new List();
+ private readonly List _thumbarButtons = new();
///
/// Add a thumbnail toolbar with a specified set of buttons to the thumbnail
@@ -1776,6 +1857,7 @@ public Task HasShadowAsync()
///
///
/// Whether the buttons were added successfully.
+ [SupportedOSPlatform("windows")]
public Task SetThumbarButtonsAsync(ThumbarButton[] thumbarButtons)
{
var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -1808,6 +1890,7 @@ public Task SetThumbarButtonsAsync(ThumbarButton[] thumbarButtons)
/// an empty region: {x: 0, y: 0, width: 0, height: 0}.
///
///
+ [SupportedOSPlatform("windows")]
public void SetThumbnailClip(Rectangle rectangle)
{
BridgeConnector.Emit("browserWindowSetThumbnailClip", Id, rectangle);
@@ -1817,6 +1900,7 @@ public void SetThumbnailClip(Rectangle rectangle)
/// Sets the toolTip that is displayed when hovering over the window thumbnail in the taskbar.
///
///
+ [SupportedOSPlatform("windows")]
public void SetThumbnailToolTip(string tooltip)
{
BridgeConnector.Emit("browserWindowSetThumbnailToolTip", Id, tooltip);
@@ -1829,14 +1913,29 @@ public void SetThumbnailToolTip(string tooltip)
/// If one of those properties is not set, then neither will be used.
///
///
+ [SupportedOSPlatform("windows")]
public void SetAppDetails(AppDetailsOptions options)
{
BridgeConnector.Emit("browserWindowSetAppDetails", Id, options);
}
+ ///
+ ///On a Window with Window Controls Overlay already enabled, this method updates
+ /// the style of the title bar overlay. It should not be called unless you enabled WCO
+ /// when creating the window.
+ ///
+ ///
+ [SupportedOSPlatform("win")]
+ [SupportedOSPlatform("macos")]
+ public void SetTitleBarOverlay(TitleBarOverlayConfig options)
+ {
+ BridgeConnector.Emit("browserWindowSetTitleBarOverlay", Id, options);
+ }
+
///
/// Same as webContents.showDefinitionForSelection().
///
+ [SupportedOSPlatform("macos")]
public void ShowDefinitionForSelection()
{
BridgeConnector.Emit("browserWindowShowDefinitionForSelection", Id);
@@ -1868,6 +1967,8 @@ public Task IsMenuBarAutoHideAsync()
/// users can still bring up the menu bar by pressing the single Alt key.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("linux")]
public void SetMenuBarVisibility(bool visible)
{
BridgeConnector.Emit("browserWindowSetMenuBarVisibility", Id, visible);
@@ -1877,6 +1978,8 @@ public void SetMenuBarVisibility(bool visible)
/// Whether the menu bar is visible.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("linux")]
public Task IsMenuBarVisibleAsync()
{
return BridgeConnector.OnResult("browserWindowIsMenuBarVisible", "browserWindow-isMenuBarVisible-completed" + Id, Id);
@@ -1923,6 +2026,8 @@ public void SetIgnoreMouseEvents(bool ignore)
/// On Windows it calls SetWindowDisplayAffinity with WDA_MONITOR.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetContentProtection(bool enable)
{
BridgeConnector.Emit("browserWindowSetContentProtection", Id, enable);
@@ -1932,6 +2037,8 @@ public void SetContentProtection(bool enable)
/// Changes whether the window can be focused.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void SetFocusable(bool focusable)
{
BridgeConnector.Emit("browserWindowSetFocusable", Id, focusable);
@@ -1971,6 +2078,7 @@ public async Task> GetChildWindowsAsync()
/// Controls whether to hide cursor when typing.
///
///
+ [SupportedOSPlatform("macos")]
public void SetAutoHideCursor(bool autoHide)
{
BridgeConnector.Emit("browserWindowSetAutoHideCursor", Id, autoHide);
@@ -1983,11 +2091,25 @@ public void SetAutoHideCursor(bool autoHide)
/// Can be appearance-based, light, dark, titlebar, selection,
/// menu, popover, sidebar, medium-light or ultra-dark.
/// See the macOS documentation for more details.
+ [SupportedOSPlatform("macos")]
public void SetVibrancy(Vibrancy type)
{
BridgeConnector.Emit("browserWindowSetVibrancy", Id, type.GetDescription());
}
+ ///
+ /// Adds a vibrancy effect to the browser window.
+ /// Passing null or an empty string will remove the vibrancy effect on the window.
+ ///
+ /// Can be appearance-based, light, dark, titlebar, selection,
+ /// menu, popover, sidebar, medium-light or ultra-dark.
+ /// See the macOS documentation for more details.
+ [SupportedOSPlatform("macos")]
+ public void ExcludeFromShownWindowsMenu()
+ {
+ BridgeConnector.Emit("browserWindowSetExcludedFromShownWindowsMenu", Id);
+ }
+
///
/// Render and control web pages.
///
@@ -2004,7 +2126,7 @@ public void SetBrowserView(BrowserView browserView)
BridgeConnector.Emit("browserWindow-setBrowserView", Id, browserView.Id);
}
- private static readonly JsonSerializer _jsonSerializer = new JsonSerializer()
+ private static readonly JsonSerializer _jsonSerializer = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
diff --git a/ElectronNET.API/Clipboard.cs b/ElectronNET.API/Clipboard.cs
index b757bb0d..3583c33e 100644
--- a/ElectronNET.API/Clipboard.cs
+++ b/ElectronNET.API/Clipboard.cs
@@ -2,6 +2,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
namespace ElectronNET.API
@@ -12,7 +13,7 @@ namespace ElectronNET.API
public sealed class Clipboard
{
private static Clipboard _clipboard;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Clipboard() { }
@@ -93,6 +94,8 @@ public void WriteRTF(string text, string type = "")
/// be empty strings when the bookmark is unavailable.
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task ReadBookmarkAsync() => BridgeConnector.OnResult("clipboard-readBookmark", "clipboard-readBookmark-Completed");
///
@@ -105,6 +108,8 @@ public void WriteRTF(string text, string type = "")
///
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public void WriteBookmark(string title, string url, string type = "")
{
BridgeConnector.Emit("clipboard-writeBookmark", title, url, type);
@@ -116,6 +121,7 @@ public void WriteBookmark(string title, string url, string type = "")
/// find pasteboard whenever the application is activated.
///
///
+ [SupportedOSPlatform("macos")]
public Task ReadFindTextAsync() => BridgeConnector.OnResult("clipboard-readFindText", "clipboard-readFindText-Completed");
///
@@ -123,6 +129,7 @@ public void WriteBookmark(string title, string url, string type = "")
/// synchronous IPC when called from the renderer process.
///
///
+ [SupportedOSPlatform("macos")]
public void WriteFindText(string text)
{
BridgeConnector.Emit("clipboard-writeFindText", text);
@@ -151,7 +158,7 @@ public void Clear(string type = "")
///
public void Write(Data data, string type = "")
{
- BridgeConnector.Emit("clipboard-write", data, type);
+ BridgeConnector.Emit("clipboard-write", JObject.FromObject(data, _jsonSerializer), type);
}
///
@@ -170,5 +177,12 @@ public void WriteImage(NativeImage image, string type = "")
{
BridgeConnector.Emit("clipboard-writeImage", JsonConvert.SerializeObject(image), type);
}
+
+ private static readonly JsonSerializer _jsonSerializer = new()
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ NullValueHandling = NullValueHandling.Ignore,
+ DefaultValueHandling = DefaultValueHandling.Ignore
+ };
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/CommandLine.cs b/ElectronNET.API/CommandLine.cs
index 26a116cd..6aa1e502 100644
--- a/ElectronNET.API/CommandLine.cs
+++ b/ElectronNET.API/CommandLine.cs
@@ -8,7 +8,7 @@ namespace ElectronNET.API
///
public sealed class CommandLine
{
- internal CommandLine() { }
+ private CommandLine() { }
internal static CommandLine Instance
{
@@ -31,7 +31,7 @@ internal static CommandLine Instance
private static CommandLine _commandLine;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
///
/// Append a switch (with optional value) to Chromium's command line.
diff --git a/ElectronNET.API/Cookies.cs b/ElectronNET.API/Cookies.cs
index 67f16b0c..f5de8700 100644
--- a/ElectronNET.API/Cookies.cs
+++ b/ElectronNET.API/Cookies.cs
@@ -71,7 +71,7 @@ public Task GetAsync(CookieFilter filter)
taskCompletionSource.SetResult(cookies);
});
- BridgeConnector.Emit("webContents-session-cookies-get", Id, filter, guid);
+ BridgeConnector.Emit("webContents-session-cookies-get", Id, JObject.FromObject(filter, _jsonSerializer), guid);
return taskCompletionSource.Task;
}
@@ -92,7 +92,7 @@ public Task SetAsync(CookieDetails details)
taskCompletionSource.SetResult(null);
});
- BridgeConnector.Emit("webContents-session-cookies-set", Id, details, guid);
+ BridgeConnector.Emit("webContents-session-cookies-set", Id, JObject.FromObject(details, _jsonSerializer), guid);
return taskCompletionSource.Task;
}
@@ -138,5 +138,13 @@ public Task FlushStoreAsync()
return taskCompletionSource.Task;
}
+
+ private static readonly JsonSerializer _jsonSerializer = new()
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ NullValueHandling = NullValueHandling.Ignore,
+ DefaultValueHandling = DefaultValueHandling.Ignore
+ };
+
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/DesktopCapturer.cs b/ElectronNET.API/DesktopCapturer.cs
new file mode 100644
index 00000000..358b513d
--- /dev/null
+++ b/ElectronNET.API/DesktopCapturer.cs
@@ -0,0 +1,38 @@
+using System.Threading.Tasks;
+using ElectronNET.API.Entities;
+using Newtonsoft.Json;
+
+namespace ElectronNET.API
+{
+ public sealed class DesktopCapturer
+ {
+ private static readonly object _syncRoot = new();
+ private static DesktopCapturer _desktopCapturer;
+
+ internal DesktopCapturer() { }
+
+ internal static DesktopCapturer Instance
+ {
+ get
+ {
+ if (_desktopCapturer == null)
+ {
+ lock (_syncRoot)
+ {
+ if (_desktopCapturer == null)
+ {
+ _desktopCapturer = new DesktopCapturer();
+ }
+ }
+ }
+
+ return _desktopCapturer;
+ }
+ }
+
+ public async Task GetSourcesAsync(SourcesOption option)
+ {
+ return await BridgeConnector.OnResult("desktop-capturer-get-sources", "desktop-capturer-get-sources-result", option);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ElectronNET.API/Dialog.cs b/ElectronNET.API/Dialog.cs
index a8ecb824..02d22aa2 100644
--- a/ElectronNET.API/Dialog.cs
+++ b/ElectronNET.API/Dialog.cs
@@ -4,6 +4,7 @@
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using System.Web;
@@ -15,7 +16,7 @@ namespace ElectronNET.API
public sealed class Dialog
{
private static Dialog _dialog;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Dialog() { }
@@ -158,12 +159,12 @@ public Task ShowMessageBoxAsync(BrowserWindow browserWindow, M
});
- if (browserWindow == null)
+ if (browserWindow is null)
{
- BridgeConnector.Emit("showMessageBox", messageBoxOptions, guid);
+ BridgeConnector.Emit("showMessageBox", JObject.FromObject(messageBoxOptions, _jsonSerializer), guid);
} else
{
- BridgeConnector.Emit("showMessageBox", browserWindow , messageBoxOptions, guid);
+ BridgeConnector.Emit("showMessageBox", JObject.FromObject(messageBoxOptions, _jsonSerializer), JObject.FromObject(messageBoxOptions, _jsonSerializer), guid);
}
return taskCompletionSource.Task;
@@ -191,6 +192,8 @@ public void ShowErrorBox(string title, string content)
///
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task ShowCertificateTrustDialogAsync(CertificateTrustDialogOptions options)
{
return ShowCertificateTrustDialogAsync(null, options);
@@ -204,6 +207,8 @@ public Task ShowCertificateTrustDialogAsync(CertificateTrustDialogOptions option
///
///
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task ShowCertificateTrustDialogAsync(BrowserWindow browserWindow, CertificateTrustDialogOptions options)
{
var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -219,5 +224,12 @@ public Task ShowCertificateTrustDialogAsync(BrowserWindow browserWindow, Certifi
return taskCompletionSource.Task;
}
+
+ private static readonly JsonSerializer _jsonSerializer = new()
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ NullValueHandling = NullValueHandling.Ignore,
+ DefaultValueHandling = DefaultValueHandling.Ignore
+ };
}
}
diff --git a/ElectronNET.API/Dock.cs b/ElectronNET.API/Dock.cs
index 9c00b2d3..b5ea5d49 100644
--- a/ElectronNET.API/Dock.cs
+++ b/ElectronNET.API/Dock.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using ElectronNET.API.Entities;
@@ -12,10 +13,11 @@ namespace ElectronNET.API
///
/// Control your app in the macOS dock.
///
+ [SupportedOSPlatform("macos")]
public sealed class Dock
{
private static Dock _dock;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Dock()
{
@@ -130,7 +132,7 @@ public Task IsVisibleAsync(CancellationToken cancellationToken = default)
/// The menu items.
///
public IReadOnlyCollection MenuItems { get { return _items.AsReadOnly(); } }
- private List _items = new List();
+ private readonly List _items = new();
///
/// Sets the application's dock menu.
@@ -163,7 +165,7 @@ public void SetIcon(string image)
BridgeConnector.Emit("dock-setIcon", image);
}
- private static readonly JsonSerializer _jsonSerializer = new JsonSerializer()
+ private static readonly JsonSerializer _jsonSerializer = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
diff --git a/ElectronNET.API/Electron.Experimental.cs b/ElectronNET.API/Electron.Experimental.cs
new file mode 100644
index 00000000..75d110ee
--- /dev/null
+++ b/ElectronNET.API/Electron.Experimental.cs
@@ -0,0 +1,144 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Net.Sockets;
+using System.Net;
+using System.Threading.Tasks;
+using System.Diagnostics;
+
+namespace ElectronNET.API
+{
+ public static partial class Electron
+ {
+ ///
+ /// Experimental code, use with care
+ ///
+ public static class Experimental
+ {
+ ///
+ /// Starts electron from C#, use during development to avoid having to fully publish / build your app on every compile cycle
+ /// You will need to run the CLI at least once (and once per update) to bootstrap all required files
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async Task StartElectronForDevelopment(int webPort, string projectPath = null, string[] extraElectronArguments = null, bool clearCache = false)
+ {
+ string aspCoreProjectPath;
+
+ if (!string.IsNullOrEmpty(projectPath))
+ {
+ if (Directory.Exists(projectPath))
+ {
+ aspCoreProjectPath = projectPath;
+ }
+ else
+ {
+ throw new DirectoryNotFoundException(projectPath);
+ }
+ }
+ else
+ {
+ aspCoreProjectPath = Directory.GetCurrentDirectory();
+ }
+
+ string tempPath = Path.Combine(aspCoreProjectPath, "obj", "Host");
+
+ if (!Directory.Exists(tempPath))
+ {
+ Directory.CreateDirectory(tempPath);
+ }
+
+ var mainFileJs = Path.Combine(tempPath, "main.js");
+ if (!File.Exists(mainFileJs))
+ {
+ throw new Exception("You need to run once the electronize-h5 start command to bootstrap the necessary files");
+ }
+
+ var nodeModulesDirPath = Path.Combine(tempPath, "node_modules");
+
+ bool runNpmInstall = false;
+
+ if (!Directory.Exists(nodeModulesDirPath))
+ {
+ runNpmInstall = true;
+ }
+
+ var packagesJson = Path.Combine(tempPath, "package.json");
+
+ var packagesPrevious = Path.Combine(tempPath, "package.json.previous");
+
+ if (!runNpmInstall)
+ {
+ if (File.Exists(packagesPrevious))
+ {
+ if (File.ReadAllText(packagesPrevious) != File.ReadAllText(packagesJson))
+ {
+ runNpmInstall = true;
+ }
+ }
+ else
+ {
+ runNpmInstall = true;
+ }
+ }
+
+ if (runNpmInstall)
+ {
+ throw new Exception("You need to run once the electronize-h5 start command to bootstrap the necessary files");
+ }
+
+ string arguments = "";
+
+ if (extraElectronArguments is object)
+ {
+ arguments = string.Join(' ', extraElectronArguments);
+ }
+
+ if (clearCache)
+ {
+ arguments += " --clear-cache=true";
+ }
+
+ BridgeConnector.AuthKey = Guid.NewGuid().ToString().Replace("-", "");
+
+ var socketPort = FreeTcpPort();
+
+ arguments += $" --development=true --devauth={BridgeConnector.AuthKey} --devport={socketPort}";
+
+ string path = Path.Combine(tempPath, "node_modules", ".bin");
+ bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+
+ if (isWindows)
+ {
+ ProcessHelper.Execute(@"electron.cmd ""..\..\main.js"" " + arguments, path);
+ }
+ else
+ {
+ ProcessHelper.Execute(@"./electron ""../../main.js"" " + arguments, path);
+ }
+
+ BridgeSettings.InitializePorts(socketPort, webPort);
+ await Task.Delay(500);
+
+ return socketPort;
+ }
+
+ ///
+ /// Return a free local TCP port
+ ///
+ ///
+ public static int FreeTcpPort()
+ {
+ TcpListener l = new TcpListener(IPAddress.Loopback, 0);
+ l.Start();
+ int port = ((IPEndPoint)l.LocalEndpoint).Port;
+ l.Stop();
+ return port;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ElectronNET.API/Electron.cs b/ElectronNET.API/Electron.cs
index 23f9902d..34faf336 100644
--- a/ElectronNET.API/Electron.cs
+++ b/ElectronNET.API/Electron.cs
@@ -1,10 +1,51 @@
-namespace ElectronNET.API
+using Microsoft.Extensions.Logging;
+using System.Runtime.Versioning;
+using System;
+using System.Collections.Generic;
+
+namespace ElectronNET.API
{
///
/// The Electron.NET API
///
- public static class Electron
+ public static partial class Electron
{
+ private static ILoggerFactory loggerFactory;
+
+ ///
+ /// Reads the auth key from the command line. This method must be called first thing.
+ ///
+ ///
+ public static void ReadAuth()
+ {
+ if (!string.IsNullOrEmpty(BridgeConnector.AuthKey))
+ {
+ throw new Exception($"Don't call ReadAuth twice or from with {nameof(Experimental)}.{nameof(Experimental.StartElectronForDevelopment)}");
+ }
+
+ var line = Console.ReadLine();
+
+ if(line.StartsWith("Auth="))
+ {
+ BridgeConnector.AuthKey = line.Substring("Auth=".Length);
+ }
+ else
+ {
+ throw new Exception("The call to Electron.ReadAuth must be the first thing your app entry point does");
+ }
+ }
+
+ ///
+ /// Sets the logger factory to be used by Electron, if any
+ ///
+ public static ILoggerFactory LoggerFactory
+ {
+ private get => loggerFactory; set
+ {
+ loggerFactory = value;
+ BridgeConnector.Logger = value.CreateLogger();
+ }
+ }
///
/// Communicate asynchronously from the main process to renderer processes.
///
@@ -60,6 +101,11 @@ public static class Electron
///
public static Screen Screen { get { return Screen.Instance; } }
+ ///
+ /// Access information about media sources that can be used to capture audio and video from the desktop using the navigator.mediaDevices.getUserMedia API.
+ ///
+ public static DesktopCapturer DesktopCapturer { get { return DesktopCapturer.Instance; } }
+
///
/// Perform copy and paste operations on the system clipboard.
///
@@ -87,6 +133,7 @@ public static class Electron
///
/// Control your app in the macOS dock.
///
+ [SupportedOSPlatform("macos")]
public static Dock Dock { get { return Dock.Instance; } }
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/ElectronNET.API.csproj b/ElectronNET.API/ElectronNET.API.csproj
index 0400251e..2cb3799d 100644
--- a/ElectronNET.API/ElectronNET.API.csproj
+++ b/ElectronNET.API/ElectronNET.API.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
true
..\artifacts
h5.ElectronNET.API
@@ -36,14 +36,18 @@ This package contains the API to access the "native" electron API.
-
+
+
all
runtime; build; native; contentfiles; analyzers
-
-
-
+
+
+
+
+
diff --git a/ElectronNET.API/Entities/BrowserWindowOptions.cs b/ElectronNET.API/Entities/BrowserWindowOptions.cs
index ba920e10..2a860d24 100644
--- a/ElectronNET.API/Entities/BrowserWindowOptions.cs
+++ b/ElectronNET.API/Entities/BrowserWindowOptions.cs
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.ComponentModel;
+using System.Runtime.Versioning;
namespace ElectronNET.API.Entities
{
@@ -36,62 +37,62 @@ public class BrowserWindowOptions
/// window's size will include window frame's size and be slightly larger. Default
/// is false.
///
- public bool UseContentSize { get; set; }
+ public bool? UseContentSize { get; set; }
///
/// Show window in the center of the screen.
///
- public bool Center { get; set; }
+ public bool? Center { get; set; }
///
/// Window's minimum width. Default is 0.
///
- public int MinWidth { get; set; }
+ public int? MinWidth { get; set; }
///
/// Window's minimum height. Default is 0.
///
- public int MinHeight { get; set; }
+ public int? MinHeight { get; set; }
///
/// Window's maximum width. Default is no limit.
///
- public int MaxWidth { get; set; }
+ public int? MaxWidth { get; set; }
///
/// Window's maximum height. Default is no limit.
///
- public int MaxHeight { get; set; }
+ public int? MaxHeight { get; set; }
///
/// Whether window is resizable. Default is true.
///
[DefaultValue(true)]
- public bool Resizable { get; set; } = true;
+ public bool? Resizable { get; set; } = true;
///
/// Whether window is movable. This is not implemented on Linux. Default is true.
///
[DefaultValue(true)]
- public bool Movable { get; set; } = true;
+ public bool? Movable { get; set; } = true;
///
/// Whether window is minimizable. This is not implemented on Linux. Default is true.
///
[DefaultValue(true)]
- public bool Minimizable { get; set; } = true;
+ public bool? Minimizable { get; set; } = true;
///
/// Whether window is maximizable. This is not implemented on Linux. Default is true.
///
[DefaultValue(true)]
- public bool Maximizable { get; set; } = true;
+ public bool? Maximizable { get; set; } = true;
///
/// Whether window is closable. This is not implemented on Linux. Default is true.
///
[DefaultValue(true)]
- public bool Closable { get; set; } = true;
+ public bool? Closable { get; set; } = true;
///
/// Whether the window can be focused. Default is true. On Windows setting
@@ -100,35 +101,35 @@ public class BrowserWindowOptions
/// always stay on top in all workspaces.
///
[DefaultValue(true)]
- public bool Focusable { get; set; } = true;
+ public bool? Focusable { get; set; } = true;
///
/// Whether the window should always stay on top of other windows. Default is false.
///
- public bool AlwaysOnTop { get; set; }
+ public bool? AlwaysOnTop { get; set; }
///
/// Whether the window should show in fullscreen. When explicitly set to false the
/// fullscreen button will be hidden or disabled on macOS.Default is false.
///
- public bool Fullscreen { get; set; }
+ public bool? Fullscreen { get; set; }
///
/// Whether the window can be put into fullscreen mode. On macOS, also whether the
/// maximize/zoom button should toggle full screen mode or maximize window.Default
/// is true.
///
- public bool Fullscreenable { get; set; }
+ public bool? Fullscreenable { get; set; }
///
/// Whether to show the window in taskbar. Default is false.
///
- public bool SkipTaskbar { get; set; }
+ public bool? SkipTaskbar { get; set; }
///
/// The kiosk mode. Default is false.
///
- public bool Kiosk { get; set; }
+ public bool? Kiosk { get; set; }
///
/// Default window title. Default is "Electron.NET".
@@ -145,40 +146,40 @@ public class BrowserWindowOptions
/// Whether window should be shown when created. Default is true.
///
[DefaultValue(true)]
- public bool Show { get; set; } = true;
+ public bool? Show { get; set; } = true;
///
/// Specify false to create a . Default is true.
///
[DefaultValue(true)]
- public bool Frame { get; set; } = true;
+ public bool? Frame { get; set; } = true;
///
- /// Whether this is a modal window. This only works when the window is a child
- /// window.Default is false.
+ /// Whether this is a modal window. This only works when is
+ /// also specified. Default is false.
///
- public bool Modal { get; set; }
+ public bool? Modal { get; set; }
///
/// Whether the web view accepts a single mouse-down event that simultaneously
/// activates the window.Default is false.
///
- public bool AcceptFirstMouse { get; set; }
+ public bool? AcceptFirstMouse { get; set; }
///
/// Whether to hide cursor when typing. Default is false.
///
- public bool DisableAutoHideCursor { get; set; }
+ public bool? DisableAutoHideCursor { get; set; }
///
/// Auto hide the menu bar unless the Alt key is pressed. Default is false.
///
- public bool AutoHideMenuBar { get; set; }
+ public bool? AutoHideMenuBar { get; set; }
///
/// Enable the window to be resized larger than screen. Default is false.
///
- public bool EnableLargerThanScreen { get; set; }
+ public bool? EnableLargerThanScreen { get; set; }
///
/// Window's background color as Hexadecimal value, like #66CD00 or #FFF or
@@ -190,18 +191,18 @@ public class BrowserWindowOptions
/// Whether window should have a shadow. This is only implemented on macOS. Default
/// is true.
///
- public bool HasShadow { get; set; }
+ public bool? HasShadow { get; set; }
///
/// Forces using dark theme for the window, only works on some GTK+3 desktop
/// environments.Default is false.
///
- public bool DarkTheme { get; set; }
+ public bool? DarkTheme { get; set; }
///
/// Makes the window . Default is false.
///
- public bool Transparent { get; set; }
+ public bool? Transparent { get; set; }
///
/// The type of window, default is normal window.
@@ -213,13 +214,21 @@ public class BrowserWindowOptions
/// 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover'
///
[JsonConverter(typeof(StringEnumConverter))]
- public TitleBarStyle TitleBarStyle { get; set; }
+ public TitleBarStyle? TitleBarStyle { get; set; }
///
/// Shows the title in the tile bar in full screen mode on macOS for all
/// titleBarStyle options.Default is false.
///
- public bool FullscreenWindowTitle { get; set; }
+ public bool? FullscreenWindowTitle { get; set; }
+
+ ///
+ /// Activate the Window Controls Overlay on Windows, when combined with =
+ ///
+ [SupportedOSPlatform("win")]
+ [SupportedOSPlatform("macos")]
+ [DefaultValue(null)]
+ public TitleBarOverlayConfig TitleBarOverlay { get; set; }
///
/// Use WS_THICKFRAME style for frameless windows on Windows, which adds standard
@@ -227,7 +236,7 @@ public class BrowserWindowOptions
/// animations. Default is true.
///
[DefaultValue(true)]
- public bool ThickFrame { get; set; } = true;
+ public bool? ThickFrame { get; set; } = true;
///
/// Add a type of vibrancy effect to the window, only on macOS. Can be
@@ -235,7 +244,7 @@ public class BrowserWindowOptions
/// medium-light or ultra-dark.
///
[JsonConverter(typeof(StringEnumConverter))]
- public Vibrancy Vibrancy { get; set; }
+ public Vibrancy? Vibrancy { get; set; }
///
/// Controls the behavior on macOS when option-clicking the green stoplight button
@@ -244,7 +253,7 @@ public class BrowserWindowOptions
/// it to zoom to the width of the screen.This will also affect the behavior when
/// calling maximize() directly.Default is false.
///
- public bool ZoomToPageWidth { get; set; }
+ public bool? ZoomToPageWidth { get; set; }
///
/// Tab group name, allows opening the window as a native tab on macOS 10.12+.
@@ -270,5 +279,20 @@ public class BrowserWindowOptions
/// These will only be used if the Proxy field is also set.
///
public string ProxyCredentials { get; set; }
+
+
+ ///
+ /// The window to use as the created window's parent.
+ ///
+ [DefaultValue(null)]
+ public BrowserWindow Parent { get; set; }
+
+ ///
+ /// Set a custom position for the traffic light buttons in frameless windows.
+ ///
+
+ [DefaultValue(null)]
+ [SupportedOSPlatform("macos")]
+ public Point TrafficLightPosition { get; set; }
}
}
diff --git a/ElectronNET.API/Entities/Data.cs b/ElectronNET.API/Entities/Data.cs
index 2dcca894..893c55c1 100644
--- a/ElectronNET.API/Entities/Data.cs
+++ b/ElectronNET.API/Entities/Data.cs
@@ -34,5 +34,7 @@ public class Data
/// The title of the url at text.
///
public string Bookmark { get; set; }
+
+ public NativeImage? Image { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/ElectronNET.API/Entities/DesktopCapturerSource.cs b/ElectronNET.API/Entities/DesktopCapturerSource.cs
new file mode 100644
index 00000000..c08dbb90
--- /dev/null
+++ b/ElectronNET.API/Entities/DesktopCapturerSource.cs
@@ -0,0 +1,15 @@
+using Newtonsoft.Json;
+
+namespace ElectronNET.API.Entities
+{
+ public sealed class DesktopCapturerSource
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public NativeImage Thumbnail { get; set; }
+
+ [JsonProperty("display_id")]
+ public string DisplayId { get; set; }
+ public NativeImage AppIcon { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/Display.cs b/ElectronNET.API/Entities/Display.cs
index 4d44f32e..4d8cceb4 100644
--- a/ElectronNET.API/Entities/Display.cs
+++ b/ElectronNET.API/Entities/Display.cs
@@ -26,12 +26,12 @@ public class Display
///
/// Can be 0, 90, 180, 270, represents screen rotation in clock-wise degrees.
///
- public int Rotation { get; set; }
+ public float Rotation { get; set; }
///
/// Output device's pixel scale factor.
///
- public int ScaleFactor { get; set; }
+ public float ScaleFactor { get; set; }
///
/// Gets or sets the size.
diff --git a/ElectronNET.API/Entities/JumpListSettings.cs b/ElectronNET.API/Entities/JumpListSettings.cs
index 150d0b78..dbd657a1 100644
--- a/ElectronNET.API/Entities/JumpListSettings.cs
+++ b/ElectronNET.API/Entities/JumpListSettings.cs
@@ -1,4 +1,6 @@
-namespace ElectronNET.API.Entities
+using System;
+
+namespace ElectronNET.API.Entities
{
public class JumpListSettings
{
@@ -13,6 +15,6 @@ public class JumpListSettings
/// in the Jump List. These items must not be re-added to the Jump List in the next call to , Windows will
/// not display any custom category that contains any of the removed items.
///
- public JumpListItem[] RemovedItems { get; set; } = new JumpListItem[0];
+ public JumpListItem[] RemovedItems { get; set; } = Array.Empty();
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/NativeImage.cs b/ElectronNET.API/Entities/NativeImage.cs
index e1066bed..f6e6395d 100644
--- a/ElectronNET.API/Entities/NativeImage.cs
+++ b/ElectronNET.API/Entities/NativeImage.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.Drawing.Imaging;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Processing;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@@ -16,10 +16,10 @@ namespace ElectronNET.API.Entities
[JsonConverter(typeof(NativeImageJsonConverter))]
public class NativeImage
{
- private readonly Dictionary _images = new Dictionary();
+ private readonly Dictionary _images = new();
private bool _isTemplateImage;
- private static readonly Dictionary ScaleFactorPairs = new Dictionary
+ private static readonly Dictionary ScaleFactorPairs = new()
{
{"@2x", 2.0f}, {"@3x", 3.0f}, {"@1x", 1.0f}, {"@4x", 4.0f},
{"@5x", 5.0f}, {"@1.25x", 1.25f}, {"@1.33x", 1.33f}, {"@1.4x", 1.4f},
@@ -36,8 +36,7 @@ public class NativeImage
}
private static Image BytesToImage(byte[] bytes)
{
- var ms = new MemoryStream(bytes);
- return Image.FromStream(ms);
+ return Image.Load(new MemoryStream(bytes));
}
///
@@ -51,14 +50,14 @@ public static NativeImage CreateEmpty()
///
///
///
- public static NativeImage CreateFromBitmap(Bitmap bitmap, CreateFromBitmapOptions options = null)
+ public static NativeImage CreateFromImage(Image image, CreateFromBitmapOptions options = null)
{
if (options is null)
{
options = new CreateFromBitmapOptions();
}
- return new NativeImage(bitmap, options.ScaleFactor);
+ return new NativeImage(image, options.ScaleFactor);
}
///
@@ -71,8 +70,7 @@ public static NativeImage CreateFromBuffer(byte[] buffer, CreateFromBufferOption
options = new CreateFromBufferOptions();
}
- var ms = new MemoryStream(buffer);
- var image = Image.FromStream(ms);
+ var image = Image.Load(new MemoryStream(buffer));
return new NativeImage(image, options.ScaleFactor);
}
@@ -110,14 +108,14 @@ public static NativeImage CreateFromPath(string path)
throw new Exception($"Invalid scaling factor for '{path}'.");
}
- images[dpi.Value] = Image.FromFile(path);
+ images[dpi.Value] = Image.Load(path);
}
else
{
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
var extension = Path.GetExtension(path);
// Load as 1x dpi
- images[1.0f] = Image.FromFile(path);
+ images[1.0f] = Image.Load(path);
foreach (var scale in ScaleFactorPairs)
{
@@ -127,7 +125,7 @@ public static NativeImage CreateFromPath(string path)
var dpi = ExtractDpiFromFilePath(fileName);
if (dpi != null)
{
- images[dpi.Value] = Image.FromFile(fileName);
+ images[dpi.Value] = Image.Load(fileName);
}
}
}
@@ -164,7 +162,7 @@ public NativeImage(Dictionary imageDictionary)
///
public NativeImage Crop(Rectangle rect)
{
- var images = new Dictionary();
+ var images = new Dictionary();
foreach (var image in _images)
{
images.Add(image.Key, Crop(rect.X, rect.Y, rect.Width, rect.Height, image.Key));
@@ -214,28 +212,12 @@ public float GetAspectRatio(float scaleFactor = 1.0f)
var image = GetScale(scaleFactor);
if (image != null)
{
- return image.Width / image.Height;
+ return (float)image.Width / image.Height;
}
return 0f;
}
-
- ///
- /// Returns a byte array that contains the image's raw bitmap pixel data.
- ///
- public byte[] GetBitmap(BitmapOptions options)
- {
- return ToBitmap(new ToBitmapOptions{ ScaleFactor = options.ScaleFactor });
- }
-
- ///
- /// Returns a byte array that contains the image's raw bitmap pixel data.
- ///
- public byte[] GetNativeHandle()
- {
- return ToBitmap(new ToBitmapOptions());
- }
-
+
///
/// Gets the size of the specified image based on scale factor
///
@@ -278,148 +260,91 @@ public void SetTemplateImage(bool option)
///
/// Outputs a bitmap based on the scale factor
///
- public byte[] ToBitmap(ToBitmapOptions options)
+ public MemoryStream ToBitmap(float scaleFactor = 1.0f)
{
- return ImageToBytes(ImageFormat.Bmp, options.ScaleFactor);
+ var ms = new MemoryStream();
+ _images[scaleFactor].SaveAsBmp(ms);
+ return ms;
}
///
- /// Outputs a data URL based on the scale factor
+ /// Outputs a PNG based on the scale factor
///
- public string ToDataURL(ToDataUrlOptions options)
+ public MemoryStream ToPng(float scaleFactor = 1.0f)
{
- if (!_images.ContainsKey(options.ScaleFactor))
- {
- return null;
- }
-
- var image = _images[options.ScaleFactor];
- var mimeType = ImageCodecInfo.GetImageEncoders().FirstOrDefault(x => x.FormatID == image.RawFormat.Guid)?.MimeType;
- if (mimeType is null)
- {
- mimeType = "image/png";
- }
-
- var bytes = ImageToBytes(image.RawFormat, options.ScaleFactor);
- var base64 = Convert.ToBase64String(bytes);
-
- return $"data:{mimeType};base64,{base64}";
+ var ms = new MemoryStream();
+ _images[scaleFactor].SaveAsPng(ms);
+ return ms;
}
///
/// Outputs a JPEG for the default scale factor
///
- public byte[] ToJPEG(int quality)
+ public MemoryStream ToJpeg(int quality, float scaleFactor = 1.0f)
{
- return ImageToBytes(ImageFormat.Jpeg, 1.0f, quality);
+ var ms = new MemoryStream();
+ _images[scaleFactor].SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder() { Quality = quality });
+ return ms;
}
///
- /// Outputs a PNG for the specified scale factor
+ /// Outputs a data URL based on the scale factor
///
- public byte[] ToPNG(ToPNGOptions options)
- {
- return ImageToBytes(ImageFormat.Png, options.ScaleFactor);
- }
-
- private byte[] ImageToBytes(ImageFormat imageFormat = null, float scaleFactor = 1.0f, int quality = 100)
+ public string ToDataURL(float scaleFactor = 1.0f)
{
- using var ms = new MemoryStream();
-
- if (_images.ContainsKey(scaleFactor))
+ if (!_images.TryGetValue(scaleFactor, out var image))
{
- var image = _images[scaleFactor];
- var encoderCodecInfo = GetEncoder(imageFormat ?? image.RawFormat);
- var encoder = Encoder.Quality;
-
- var encoderParameters = new EncoderParameters(1)
- {
- Param = new[]
- {
- new EncoderParameter(encoder, quality)
- }
- };
-
- image.Save(ms, encoderCodecInfo, encoderParameters);
-
- return ms.ToArray();
+ throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}");
}
- return null;
+ return image.ToBase64String(PngFormat.Instance);
}
+
private Image Resize(int? width, int? height, float scaleFactor = 1.0f)
{
- if (!_images.ContainsKey(scaleFactor) || (width is null && height is null))
+ if (!_images.TryGetValue(scaleFactor, out var image))
{
- return null;
+ throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}");
}
-
- var image = _images[scaleFactor];
- using (var g = Graphics.FromImage(image))
+ if (width is null && height is null)
{
- g.CompositingQuality = CompositingQuality.HighQuality;
-
- var aspect = GetAspectRatio(scaleFactor);
-
- width ??= Convert.ToInt32(image.Width * aspect);
- height ??= Convert.ToInt32(image.Height * aspect);
-
- width = Convert.ToInt32(width * scaleFactor);
- height = Convert.ToInt32(height * scaleFactor);
+ throw new ArgumentNullException("Missing width or height");
+ }
- var bmp = new Bitmap(width.Value, height.Value);
- g.DrawImage(bmp,
- new System.Drawing.Rectangle(0, 0, image.Width, image.Height),
- new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height),
- GraphicsUnit.Pixel);
+ var aspect = GetAspectRatio(scaleFactor);
+ width ??= Convert.ToInt32(image.Width * aspect);
+ height ??= Convert.ToInt32(image.Height * aspect);
+ width = Convert.ToInt32(width * scaleFactor);
+ height = Convert.ToInt32(height * scaleFactor);
- return bmp;
- }
+ return image.Clone(c => c.Resize(new SixLabors.ImageSharp.Processing.ResizeOptions
+ {
+ Size = new(width.Value, height.Value),
+ Sampler = KnownResamplers.Triangle,
+ }));
}
private Image Crop(int? x, int? y, int? width, int? height, float scaleFactor = 1.0f)
{
- if (!_images.ContainsKey(scaleFactor))
+ if (!_images.TryGetValue(scaleFactor, out var image))
{
- return null;
+ throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}");
}
- var image = _images[scaleFactor];
- using (var g = Graphics.FromImage(image))
- {
- g.CompositingQuality = CompositingQuality.HighQuality;
-
- x ??= 0;
- y ??= 0;
-
- x = Convert.ToInt32(x * scaleFactor);
- y = Convert.ToInt32(y * scaleFactor);
+ x ??= 0;
+ y ??= 0;
- width ??= image.Width;
- height ??= image.Height;
+ x = Convert.ToInt32(x * scaleFactor);
+ y = Convert.ToInt32(y * scaleFactor);
- width = Convert.ToInt32(width * scaleFactor);
- height = Convert.ToInt32(height * scaleFactor);
+ width ??= image.Width;
+ height ??= image.Height;
- var bmp = new Bitmap(width.Value, height.Value);
- g.DrawImage(bmp, new System.Drawing.Rectangle(0, 0, image.Width, image.Height), new System.Drawing.Rectangle(x.Value, y.Value, width.Value, height.Value), GraphicsUnit.Pixel);
-
- return bmp;
- }
- }
+ width = Convert.ToInt32(width * scaleFactor);
+ height = Convert.ToInt32(height * scaleFactor);
- private ImageCodecInfo GetEncoder(ImageFormat format)
- {
- var codecs = ImageCodecInfo.GetImageDecoders();
- foreach (ImageCodecInfo codec in codecs)
- {
- if (codec.FormatID == format.Guid)
- {
- return codec;
- }
- }
- return null;
+ return image.Clone(c => c.Crop(new SixLabors.ImageSharp.Rectangle(x.Value, y.Value, width.Value, height.Value)));
}
internal Dictionary GetAllScaledImages()
@@ -429,12 +354,12 @@ internal Dictionary GetAllScaledImages()
{
foreach (var (scale, image) in _images)
{
- dict.Add(scale, Convert.ToBase64String(ImageToBytes(null, scale)));
+ dict.Add(scale, image.ToBase64String(PngFormat.Instance));
}
}
catch (Exception ex)
{
- Console.WriteLine(ex);
+ BridgeConnector.LogError(ex, "Error getting scaled images");
}
return dict;
@@ -442,9 +367,9 @@ internal Dictionary GetAllScaledImages()
internal Image GetScale(float scaleFactor)
{
- if (_images.ContainsKey(scaleFactor))
+ if (_images.TryGetValue(scaleFactor, out var image))
{
- return _images[scaleFactor];
+ return image;
}
return null;
diff --git a/ElectronNET.API/Entities/NativeImageJsonConverter.cs b/ElectronNET.API/Entities/NativeImageJsonConverter.cs
index a82c38ec..c15210d7 100644
--- a/ElectronNET.API/Entities/NativeImageJsonConverter.cs
+++ b/ElectronNET.API/Entities/NativeImageJsonConverter.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Drawing;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Processing;
using System.IO;
using Newtonsoft.Json;
@@ -19,12 +21,15 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
- var dict = serializer.Deserialize>(reader);
+ var dict = serializer.Deserialize>(reader);
var newDictionary = new Dictionary();
foreach (var item in dict)
{
- var bytes = Convert.FromBase64String(item.Value);
- newDictionary.Add(item.Key, Image.FromStream(new MemoryStream(bytes)));
+ if (float.TryParse(item.Key, out var size))
+ {
+ var bytes = Convert.FromBase64String(item.Value);
+ newDictionary.Add(size, Image.Load(new MemoryStream(bytes)));
+ }
}
return new NativeImage(newDictionary);
}
diff --git a/ElectronNET.API/Entities/NotificationAction.cs b/ElectronNET.API/Entities/NotificationAction.cs
index c7194cd0..6dd4fa97 100644
--- a/ElectronNET.API/Entities/NotificationAction.cs
+++ b/ElectronNET.API/Entities/NotificationAction.cs
@@ -1,8 +1,11 @@
-namespace ElectronNET.API.Entities
+using System.Runtime.Versioning;
+
+namespace ElectronNET.API.Entities
{
///
///
///
+ [SupportedOSPlatform("macos")]
public class NotificationAction
{
///
diff --git a/ElectronNET.API/Entities/NotificationOptions.cs b/ElectronNET.API/Entities/NotificationOptions.cs
index d1913798..53478220 100644
--- a/ElectronNET.API/Entities/NotificationOptions.cs
+++ b/ElectronNET.API/Entities/NotificationOptions.cs
@@ -1,5 +1,6 @@
using Newtonsoft.Json;
using System;
+using System.Runtime.Versioning;
namespace ElectronNET.API.Entities
{
@@ -17,6 +18,8 @@ public class NotificationOptions
///
/// A subtitle for the notification, which will be displayed below the title.
///
+ [SupportedOSPlatform("macos")]
+
public string SubTitle { get; set; }
///
@@ -38,38 +41,46 @@ public class NotificationOptions
///
/// Whether or not to add an inline reply option to the notification.
///
+ [SupportedOSPlatform("macos")]
public bool HasReply { get; set; }
///
/// The timeout duration of the notification. Can be 'default' or 'never'.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("linux")]
public string TimeoutType { get; set; }
///
/// The placeholder to write in the inline reply input field.
///
+ [SupportedOSPlatform("macos")]
public string ReplyPlaceholder { get; set; }
///
/// The name of the sound file to play when the notification is shown.
///
+ [SupportedOSPlatform("macos")]
public string Sound { get; set; }
///
/// The urgency level of the notification. Can be 'normal', 'critical', or 'low'.
///
+ [SupportedOSPlatform("linux")]
public string Urgency { get; set; }
///
/// Actions to add to the notification. Please read the available actions and
/// limitations in the NotificationAction documentation.
///
+ [SupportedOSPlatform("macos")]
public NotificationAction Actions { get; set; }
///
/// A custom title for the close button of an alert. An empty string will cause the
/// default localized text to be used.
///
+ [SupportedOSPlatform("macos")]
public string CloseButtonText { get; set; }
///
@@ -127,6 +138,7 @@ public class NotificationOptions
/// The string the user entered into the inline reply field
///
[JsonIgnore]
+ [SupportedOSPlatform("macos")]
public Action OnReply { get; set; }
///
@@ -142,6 +154,7 @@ public class NotificationOptions
/// macOS only - The index of the action that was activated
///
[JsonIgnore]
+ [SupportedOSPlatform("macos")]
public Action OnAction { get; set; }
///
diff --git a/ElectronNET.API/Entities/OpenDialogOptions.cs b/ElectronNET.API/Entities/OpenDialogOptions.cs
index 3f18dffd..76b58a81 100644
--- a/ElectronNET.API/Entities/OpenDialogOptions.cs
+++ b/ElectronNET.API/Entities/OpenDialogOptions.cs
@@ -1,5 +1,6 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
+using System.Runtime.Versioning;
namespace ElectronNET.API.Entities
{
@@ -39,6 +40,7 @@ public class OpenDialogOptions
///
/// Message to display above input boxes.
///
+ [SupportedOSPlatform("macos")]
public string Message { get; set; }
///
diff --git a/ElectronNET.API/Entities/OpenDialogProperty.cs b/ElectronNET.API/Entities/OpenDialogProperty.cs
index 44a16b15..ec47dad0 100644
--- a/ElectronNET.API/Entities/OpenDialogProperty.cs
+++ b/ElectronNET.API/Entities/OpenDialogProperty.cs
@@ -1,4 +1,6 @@
-namespace ElectronNET.API.Entities
+using System.Runtime.Versioning;
+
+namespace ElectronNET.API.Entities
{
///
///
@@ -28,21 +30,66 @@ public enum OpenDialogProperty
///
/// The create directory
///
+ [SupportedOSPlatform("macos")]
createDirectory,
///
/// The prompt to create
///
+ [SupportedOSPlatform("windows")]
promptToCreate,
///
/// The no resolve aliases
///
+ [SupportedOSPlatform("macos")]
noResolveAliases,
///
- /// The treat package as directory
+ /// Treat packages, such as .app folders, as a directory instead of a file.
+ ///
+ [SupportedOSPlatform("macos")]
+ treatPackageAsDirectory,
+
+ ///
+ /// Don't add the item being opened to recent documents list
+ ///
+ [SupportedOSPlatform("windows")]
+ dontAddToRecent
+ }
+
+ ///
+ ///
+ ///
+ public enum SaveDialogProperty
+ {
+ ///
+ /// The show hidden files
+ ///
+ showHiddenFiles,
+
+ ///
+ /// The create directory
+ ///
+ [SupportedOSPlatform("macos")]
+ createDirectory,
+
+ ///
+ /// Treat packages, such as .app folders, as a directory instead of a file.
+ ///
+ [SupportedOSPlatform("macos")]
+ treatPackageAsDirectory,
+
+ ///
+ /// Sets whether the user will be presented a confirmation dialog if the user types a file name that already exists.
+ ///
+ [SupportedOSPlatform("linux")]
+ showOverwriteConfirmation,
+
+ ///
+ /// Don't add the item being opened to recent documents list
///
- treatPackageAsDirectory
+ [SupportedOSPlatform("windows")]
+ dontAddToRecent
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/OpenExternalOptions.cs b/ElectronNET.API/Entities/OpenExternalOptions.cs
index dba51ca4..d913593f 100644
--- a/ElectronNET.API/Entities/OpenExternalOptions.cs
+++ b/ElectronNET.API/Entities/OpenExternalOptions.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.Runtime.Versioning;
namespace ElectronNET.API.Entities
{
@@ -12,11 +13,13 @@ public class OpenExternalOptions
/// to bring the opened application to the foreground. The default is .
///
[DefaultValue(true)]
+ [SupportedOSPlatform("macos")]
public bool Activate { get; set; } = true;
///
/// The working directory.
///
+ [SupportedOSPlatform("windows")]
public string WorkingDirectory { get; set; }
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/SaveDialogOptions.cs b/ElectronNET.API/Entities/SaveDialogOptions.cs
index 16811d70..2e7ed4ea 100644
--- a/ElectronNET.API/Entities/SaveDialogOptions.cs
+++ b/ElectronNET.API/Entities/SaveDialogOptions.cs
@@ -1,4 +1,7 @@
using ElectronNET.API.Entities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using System.Runtime.Versioning;
namespace ElectronNET.API
{
@@ -46,16 +49,26 @@ public class SaveDialogOptions
///
/// Message to display above text fields.
///
+ [SupportedOSPlatform("macos")]
public string Message { get; set; }
///
/// Custom label for the text displayed in front of the filename text field.
///
+ [SupportedOSPlatform("macos")]
public string NameFieldLabel { get; set; }
///
/// Show the tags input box, defaults to true.
///
+ [SupportedOSPlatform("macos")]
public bool ShowsTagField { get; set; }
+
+ ///
+ /// Contains which features the dialog should use. The following values are supported:
+ /// 'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory'
+ ///
+ [JsonProperty("properties", ItemConverterType = typeof(StringEnumConverter))]
+ public SaveDialogProperty[] Properties { get; set; }
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/SourcesOption.cs b/ElectronNET.API/Entities/SourcesOption.cs
new file mode 100644
index 00000000..f6bcac4f
--- /dev/null
+++ b/ElectronNET.API/Entities/SourcesOption.cs
@@ -0,0 +1,9 @@
+namespace ElectronNET.API.Entities
+{
+ public sealed class SourcesOption
+ {
+ public string[] Types { get; set; }
+ public Size ThumbnailSize { get; set; }
+ public bool FetchWindowIcons { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/TitleBarStyle.cs b/ElectronNET.API/Entities/TitleBarStyle.cs
index 8fb108ec..a166b994 100644
--- a/ElectronNET.API/Entities/TitleBarStyle.cs
+++ b/ElectronNET.API/Entities/TitleBarStyle.cs
@@ -28,4 +28,11 @@ public enum TitleBarStyle
///
customButtonsOnHover
}
+
+ public class TitleBarOverlayConfig
+ {
+ public string color { get; set; }
+ public string symbolColor { get; set; }
+ public int height { get; set; }
+ }
}
\ No newline at end of file
diff --git a/ElectronNET.API/Entities/ToBitmapOptions.cs b/ElectronNET.API/Entities/ToBitmapOptions.cs
deleted file mode 100644
index 1a08c3bf..00000000
--- a/ElectronNET.API/Entities/ToBitmapOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace ElectronNET.API.Entities
-{
- ///
- ///
- ///
- public class ToBitmapOptions
- {
- ///
- /// Gets or sets the scalefactor
- ///
- public float ScaleFactor { get; set; } = 1.0f;
- }
-}
diff --git a/ElectronNET.API/Entities/ToDataUrlOptions.cs b/ElectronNET.API/Entities/ToDataUrlOptions.cs
deleted file mode 100644
index 0df4aa99..00000000
--- a/ElectronNET.API/Entities/ToDataUrlOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace ElectronNET.API.Entities
-{
- ///
- ///
- ///
- public class ToDataUrlOptions
- {
- ///
- /// Gets or sets the scalefactor
- ///
- public float ScaleFactor { get; set; } = 1.0f;
- }
-}
diff --git a/ElectronNET.API/Entities/ToPNGOptions.cs b/ElectronNET.API/Entities/ToPNGOptions.cs
deleted file mode 100644
index f4e36b10..00000000
--- a/ElectronNET.API/Entities/ToPNGOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace ElectronNET.API.Entities
-{
- ///
- ///
- ///
- public class ToPNGOptions
- {
- ///
- /// Gets or sets the scalefactor
- ///
- public float ScaleFactor { get; set; } = 1.0f;
- }
-}
diff --git a/ElectronNET.API/Entities/UpdateInfo.cs b/ElectronNET.API/Entities/UpdateInfo.cs
index 01dc54bc..8acc0bc0 100644
--- a/ElectronNET.API/Entities/UpdateInfo.cs
+++ b/ElectronNET.API/Entities/UpdateInfo.cs
@@ -1,4 +1,6 @@
-namespace ElectronNET.API.Entities
+using System;
+
+namespace ElectronNET.API.Entities
{
///
///
@@ -13,7 +15,7 @@ public class UpdateInfo
///
///
///
- public UpdateFileInfo[] Files { get; set; } = new UpdateFileInfo[0];
+ public UpdateFileInfo[] Files { get; set; } = Array.Empty();
///
/// The release name.
diff --git a/ElectronNET.API/Entities/Vibrancy.cs b/ElectronNET.API/Entities/Vibrancy.cs
index 2e8bec85..b496abc8 100644
--- a/ElectronNET.API/Entities/Vibrancy.cs
+++ b/ElectronNET.API/Entities/Vibrancy.cs
@@ -1,4 +1,5 @@
-using System.Runtime.Serialization;
+using System.Runtime.Serialization;
+using System;
namespace ElectronNET.API.Entities
{
@@ -11,16 +12,19 @@ public enum Vibrancy
/// The appearance based
///
[EnumMember(Value = "appearance-based")]
+ [Obsolete("Removed in macOS Catalina (10.15).")]
appearanceBased,
///
/// The light
///
+ [Obsolete("Removed in macOS Catalina (10.15).")]
light,
///
/// The dark
///
+ [Obsolete("Removed in macOS Catalina (10.15).")]
dark,
///
@@ -52,12 +56,38 @@ public enum Vibrancy
/// The medium light
///
[EnumMember(Value = "medium-light")]
+ [Obsolete("Removed in macOS Catalina (10.15).")]
mediumLight,
///
/// The ultra dark
///
[EnumMember(Value = "ultra-dark")]
- ultraDark
+ [Obsolete("Removed in macOS Catalina (10.15).")]
+ ultraDark,
+
+ header,
+
+ sheet,
+
+ window,
+
+ hud,
+
+ [EnumMember(Value = "fullscreen-ui")]
+ fullscreenUI,
+
+ tooltip,
+
+ content,
+
+ [EnumMember(Value = "under-window")]
+ underWindow,
+
+ [EnumMember(Value = "under-page")]
+ underPage
+
+
+
}
-}
\ No newline at end of file
+}
diff --git a/ElectronNET.API/Entities/WebPreferences.cs b/ElectronNET.API/Entities/WebPreferences.cs
index ca5e24e0..1ecee13a 100644
--- a/ElectronNET.API/Entities/WebPreferences.cs
+++ b/ElectronNET.API/Entities/WebPreferences.cs
@@ -171,6 +171,12 @@ public class WebPreferences
///
public bool Offscreen { get; set; }
+ ///
+ /// Whether to enable built-in spellcheck
+ ///
+ [DefaultValue(true)]
+ public bool Spellcheck { get; set; } = true;
+
///
/// Whether to run Electron APIs and the specified preload script in a separate
/// JavaScript context. Defaults to false. The context that the preload script runs
@@ -189,11 +195,6 @@ public class WebPreferences
[DefaultValue(true)]
public bool ContextIsolation { get; set; } = true;
- ///
- /// Whether to use native window.open(). Defaults to false. This option is currently experimental.
- ///
- public bool NativeWindowOpen { get; set; }
-
///
/// Whether to enable the Webview. Defaults to the value of the nodeIntegration option. The
/// preload script configured for the Webview will have node integration enabled
@@ -209,9 +210,9 @@ public class WebPreferences
public bool WebviewTag { get; set; } = false;
///
- /// Whether to enable the remote module. Defaults to false.
+ /// Make the web view transparent
///
[DefaultValue(false)]
- public bool EnableRemoteModule { get; set; } = false;
+ public bool Transparent { get; set; } = false;
}
}
\ No newline at end of file
diff --git a/ElectronNET.API/Events.cs b/ElectronNET.API/Events.cs
index 6c01cdc2..2eb65d8b 100644
--- a/ElectronNET.API/Events.cs
+++ b/ElectronNET.API/Events.cs
@@ -9,8 +9,8 @@ namespace ElectronNET.API
internal class Events
{
private static Events _events;
- private static object _syncRoot = new object();
- private TextInfo _ti = new CultureInfo("en-US", false).TextInfo;
+ private static readonly object _syncRoot = new();
+ private readonly TextInfo _ti = new CultureInfo("en-US", false).TextInfo;
private Events()
{
diff --git a/ElectronNET.API/Extensions/EnumExtensions.cs b/ElectronNET.API/Extensions/EnumExtensions.cs
index 4424310e..949c1fcd 100644
--- a/ElectronNET.API/Extensions/EnumExtensions.cs
+++ b/ElectronNET.API/Extensions/EnumExtensions.cs
@@ -11,7 +11,7 @@ public static string GetDescription(this T enumerationValue) where T : struct
Type type = enumerationValue.GetType();
if (!type.IsEnum)
{
- throw new ArgumentException("EnumerationValue must be of Enum type", "enumerationValue");
+ throw new ArgumentException("EnumerationValue must be of Enum type", nameof(enumerationValue));
}
//Tries to find a DescriptionAttribute for a potential friendly name
diff --git a/ElectronNET.API/Extensions/MenuItemExtensions.cs b/ElectronNET.API/Extensions/MenuItemExtensions.cs
index 37a5b9cb..44678a29 100644
--- a/ElectronNET.API/Extensions/MenuItemExtensions.cs
+++ b/ElectronNET.API/Extensions/MenuItemExtensions.cs
@@ -28,7 +28,7 @@ public static MenuItem[] AddMenuItemsId(this MenuItem[] menuItems)
public static MenuItem GetMenuItem(this List menuItems, string id)
{
- MenuItem result = new MenuItem();
+ MenuItem result = new();
foreach (var item in menuItems)
{
diff --git a/ElectronNET.API/Extensions/ThumbarButtonExtensions.cs b/ElectronNET.API/Extensions/ThumbarButtonExtensions.cs
index 3b155546..4d510053 100644
--- a/ElectronNET.API/Extensions/ThumbarButtonExtensions.cs
+++ b/ElectronNET.API/Extensions/ThumbarButtonExtensions.cs
@@ -23,7 +23,7 @@ public static ThumbarButton[] AddThumbarButtonsId(this ThumbarButton[] thumbarBu
public static ThumbarButton GetThumbarButton(this List thumbarButtons, string id)
{
- ThumbarButton result = new ThumbarButton("");
+ ThumbarButton result = new("");
foreach (var item in thumbarButtons)
{
diff --git a/ElectronNET.API/GlobalShortcut.cs b/ElectronNET.API/GlobalShortcut.cs
index 9efbdf27..9d356b28 100644
--- a/ElectronNET.API/GlobalShortcut.cs
+++ b/ElectronNET.API/GlobalShortcut.cs
@@ -10,7 +10,7 @@ namespace ElectronNET.API
public sealed class GlobalShortcut
{
private static GlobalShortcut _globalShortcut;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal GlobalShortcut() { }
@@ -33,7 +33,7 @@ internal static GlobalShortcut Instance
}
}
- private Dictionary _shortcuts = new Dictionary();
+ private readonly Dictionary _shortcuts = new();
///
/// Registers a global shortcut of accelerator.
diff --git a/ElectronNET.API/HostHook.cs b/ElectronNET.API/HostHook.cs
index a5c4e9a9..3fcd7910 100644
--- a/ElectronNET.API/HostHook.cs
+++ b/ElectronNET.API/HostHook.cs
@@ -16,8 +16,8 @@ namespace ElectronNET.API
public sealed class HostHook
{
private static HostHook _electronHostHook;
- private static object _syncRoot = new object();
- string oneCallguid = Guid.NewGuid().ToString();
+ private static readonly object _syncRoot = new();
+ readonly string oneCallguid = Guid.NewGuid().ToString();
internal HostHook() { }
diff --git a/ElectronNET.API/IpcMain.cs b/ElectronNET.API/IpcMain.cs
index eae3f3cf..6efb051e 100644
--- a/ElectronNET.API/IpcMain.cs
+++ b/ElectronNET.API/IpcMain.cs
@@ -14,7 +14,7 @@ namespace ElectronNET.API
public sealed class IpcMain
{
private static IpcMain _ipcMain;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal IpcMain() { }
@@ -37,6 +37,8 @@ internal static IpcMain Instance
}
}
+ public static bool IsConnected => BridgeConnector.IsConnected;
+
///
/// Listens to channel, when a new message arrives listener would be called with
/// listener(event, args...).
@@ -47,11 +49,11 @@ public void On(string channel, Action listener)
{
BridgeConnector.Emit("registerIpcMainChannel", channel);
BridgeConnector.Off(channel);
- BridgeConnector.On(channel, (args) =>
+ BridgeConnector.On(channel, (args) =>
{
var objectArray = FormatArguments(args);
- if(objectArray.Count == 1)
+ if (objectArray.Count == 1)
{
listener(objectArray.First());
}
@@ -62,6 +64,38 @@ public void On(string channel, Action listener)
});
}
+ ///
+ /// Listens to channel, when a new message arrives listener would be called with
+ /// listener(event, args...). This listner will keep the window event sender id
+ ///
+ /// Channelname.
+ /// Callback Method.
+ public void OnWithId(string channel, Action<(int browserId, int webContentId, object arguments)> listener)
+ {
+ BridgeConnector.Emit("registerIpcMainChannelWithId", channel);
+ BridgeConnector.Off(channel);
+ BridgeConnector.On(channel, (data) =>
+ {
+ var objectArray = FormatArguments(data.args);
+
+ if (objectArray.Count == 1)
+ {
+ listener((data.id, data.wcId, objectArray.First()));
+ }
+ else
+ {
+ listener((data.id, data.wcId, objectArray));
+ }
+ });
+ }
+
+ private class ArgsAndIds
+ {
+ public int id { get; set; }
+ public int wcId { get; set; }
+ public object[] args { get; set; }
+ }
+
private List FormatArguments(object[] objectArray)
{
return objectArray.Where(o => o is object).ToList();
@@ -105,7 +139,7 @@ public void OnSync(string channel, Func listener)
public void Once(string channel, Action listener)
{
BridgeConnector.Emit("registerOnceIpcMainChannel", channel);
- BridgeConnector.On(channel, (args) =>
+ BridgeConnector.Once(channel, (args) =>
{
var objectArray = FormatArguments(args);
@@ -140,32 +174,29 @@ public void RemoveAllListeners(string channel)
/// Arguments data.
public void Send(BrowserWindow browserWindow, string channel, params object[] data)
{
- List jobjects = new List();
- List jarrays = new List();
- List objects = new List();
+ var objectsWithCorrectSerialization = new List
+ {
+ browserWindow.Id,
+ channel
+ };
foreach (var parameterObject in data)
{
if(parameterObject.GetType().IsArray || parameterObject.GetType().IsGenericType && parameterObject is IEnumerable)
{
- jarrays.Add(JArray.FromObject(parameterObject, _jsonSerializer));
- } else if(parameterObject.GetType().IsClass && !parameterObject.GetType().IsPrimitive && !(parameterObject is string))
+ objectsWithCorrectSerialization.Add(JArray.FromObject(parameterObject, _jsonSerializer));
+ }
+ else if(parameterObject.GetType().IsClass && !parameterObject.GetType().IsPrimitive && !(parameterObject is string))
{
- jobjects.Add(JObject.FromObject(parameterObject, _jsonSerializer));
- } else if(parameterObject.GetType().IsPrimitive || (parameterObject is string))
+ objectsWithCorrectSerialization.Add(JObject.FromObject(parameterObject, _jsonSerializer));
+ }
+ else if(parameterObject.GetType().IsPrimitive || (parameterObject is string))
{
- objects.Add(parameterObject);
+ objectsWithCorrectSerialization.Add(parameterObject);
}
}
- if(jobjects.Count > 0 || jarrays.Count > 0)
- {
- BridgeConnector.Emit("sendToIpcRenderer", JObject.FromObject(browserWindow, _jsonSerializer), channel, jarrays.ToArray(), jobjects.ToArray(), objects.ToArray());
- }
- else
- {
- BridgeConnector.Emit("sendToIpcRenderer", JObject.FromObject(browserWindow, _jsonSerializer), channel, data);
- }
+ BridgeConnector.Emit("sendToIpcRenderer", objectsWithCorrectSerialization.ToArray());
}
///
@@ -179,9 +210,9 @@ public void Send(BrowserWindow browserWindow, string channel, params object[] da
/// Arguments data.
public void Send(BrowserView browserView, string channel, params object[] data)
{
- List jobjects = new List();
- List jarrays = new List();
- List objects = new List();
+ List jobjects = new();
+ List jarrays = new();
+ List objects = new();
foreach (var parameterObject in data)
{
@@ -228,7 +259,7 @@ public static void ConsoleError(string text)
BridgeConnector.Emit("console-stderr", text);
}
- private JsonSerializer _jsonSerializer = new JsonSerializer()
+ private readonly JsonSerializer _jsonSerializer = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
diff --git a/ElectronNET.API/LifetimeServiceHost.cs b/ElectronNET.API/LifetimeServiceHost.cs
index cb58eb1b..d6685c2c 100644
--- a/ElectronNET.API/LifetimeServiceHost.cs
+++ b/ElectronNET.API/LifetimeServiceHost.cs
@@ -16,7 +16,7 @@ public LifetimeServiceHost(IHostApplicationLifetime lifetime)
{
App.Instance.IsReady = true;
- Console.WriteLine("ASP.NET Core host has fully started.");
+ BridgeConnector.Log("ASP.NET Core host has fully started.");
});
}
diff --git a/ElectronNET.API/Menu.cs b/ElectronNET.API/Menu.cs
index aa98613a..b5b68d24 100644
--- a/ElectronNET.API/Menu.cs
+++ b/ElectronNET.API/Menu.cs
@@ -15,7 +15,7 @@ namespace ElectronNET.API
public sealed class Menu
{
private static Menu _menu;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Menu() { }
@@ -45,7 +45,7 @@ internal static Menu Instance
/// The menu items.
///
public IReadOnlyCollection MenuItems { get { return _menuItems.AsReadOnly(); } }
- private List _menuItems = new List();
+ private readonly List _menuItems = new();
///
/// Sets the application menu.
@@ -75,7 +75,7 @@ public void SetApplicationMenu(MenuItem[] menuItems)
/// The context menu items.
///
public IReadOnlyDictionary> ContextMenuItems { get; internal set; }
- private Dictionary> _contextMenuItems = new Dictionary>();
+ private readonly Dictionary> _contextMenuItems = new();
///
/// Sets the context menu.
@@ -113,7 +113,7 @@ public void ContextMenuPopup(BrowserWindow browserWindow)
BridgeConnector.Emit("menu-contextMenuPopup", browserWindow.Id);
}
- private JsonSerializer _jsonSerializer = new JsonSerializer()
+ private readonly JsonSerializer _jsonSerializer = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
diff --git a/ElectronNET.API/NativeTheme.cs b/ElectronNET.API/NativeTheme.cs
index 12bce45a..421db083 100644
--- a/ElectronNET.API/NativeTheme.cs
+++ b/ElectronNET.API/NativeTheme.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using ElectronNET.API.Entities;
using ElectronNET.API.Extensions;
@@ -11,7 +12,7 @@ namespace ElectronNET.API
public sealed class NativeTheme
{
private static NativeTheme _nativeTheme;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal NativeTheme() { }
@@ -114,12 +115,16 @@ public void SetThemeSource(ThemeSourceMode themeSourceMode)
/// A for if the OS / Chromium currently has high-contrast mode enabled or is
/// being instructed to show a high-contrast UI.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task ShouldUseHighContrastColorsAsync() => BridgeConnector.OnResult("nativeTheme-shouldUseHighContrastColors", "nativeTheme-shouldUseHighContrastColors-completed");
///
/// A for if the OS / Chromium currently has an inverted color scheme or is
/// being instructed to use an inverted color scheme.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public Task ShouldUseInvertedColorSchemeAsync() => BridgeConnector.OnResult("nativeTheme-shouldUseInvertedColorScheme", "nativeTheme-shouldUseInvertedColorScheme-completed");
///
diff --git a/ElectronNET.API/Notification.cs b/ElectronNET.API/Notification.cs
index 7f93f96f..54172ca0 100644
--- a/ElectronNET.API/Notification.cs
+++ b/ElectronNET.API/Notification.cs
@@ -15,7 +15,7 @@ namespace ElectronNET.API
public sealed class Notification
{
private static Notification _notification;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Notification() { }
@@ -38,7 +38,7 @@ internal static Notification Instance
}
}
- private static List _notificationOptions = new List();
+ private static readonly List _notificationOptions = new();
///
/// Create OS desktop notifications
@@ -48,7 +48,7 @@ public void Show(NotificationOptions notificationOptions)
{
GenerateIDsForDefinedActions(notificationOptions);
- BridgeConnector.Emit("createNotification", notificationOptions);
+ BridgeConnector.Emit("createNotification", JObject.FromObject(notificationOptions, _jsonSerializer));
}
private static void GenerateIDsForDefinedActions(NotificationOptions notificationOptions)
@@ -134,5 +134,12 @@ public Task IsSupportedAsync()
return taskCompletionSource.Task;
}
+
+ private static readonly JsonSerializer _jsonSerializer = new()
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ NullValueHandling = NullValueHandling.Ignore,
+ DefaultValueHandling = DefaultValueHandling.Ignore
+ };
}
}
diff --git a/ElectronNET.API/PowerMonitor.cs b/ElectronNET.API/PowerMonitor.cs
index 8a7ab809..8ddde896 100644
--- a/ElectronNET.API/PowerMonitor.cs
+++ b/ElectronNET.API/PowerMonitor.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
namespace ElectronNET.API
@@ -11,6 +12,8 @@ public sealed class PowerMonitor
///
/// Emitted when the system is about to lock the screen.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public event Action OnLockScreen
{
add
@@ -40,6 +43,8 @@ public event Action OnLockScreen
///
/// Emitted when the system is about to unlock the screen.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public event Action OnUnLockScreen
{
add
@@ -69,6 +74,8 @@ public event Action OnUnLockScreen
///
/// Emitted when the system is suspending.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public event Action OnSuspend
{
add
@@ -98,6 +105,8 @@ public event Action OnSuspend
///
/// Emitted when system is resuming.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public event Action OnResume
{
add
@@ -127,6 +136,8 @@ public event Action OnResume
///
/// Emitted when the system changes to AC power.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public event Action OnAC
{
add
@@ -156,6 +167,8 @@ public event Action OnAC
///
/// Emitted when system changes to battery power.
///
+ [SupportedOSPlatform("windows")]
+ [SupportedOSPlatform("macos")]
public event Action OnBattery
{
add
@@ -189,6 +202,9 @@ public event Action OnBattery
/// order for the app to exit cleanly.If `e.preventDefault()` is called, the app
/// should exit as soon as possible by calling something like `app.quit()`.
///
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+
public event Action OnShutdown
{
add
@@ -216,7 +232,7 @@ public event Action OnShutdown
private event Action _shutdown;
private static PowerMonitor _powerMonitor;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal PowerMonitor() { }
diff --git a/ElectronNET.API/ProcessHelper.cs b/ElectronNET.API/ProcessHelper.cs
new file mode 100644
index 00000000..08fedc27
--- /dev/null
+++ b/ElectronNET.API/ProcessHelper.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ElectronNET.API
+{
+ internal class ProcessHelper
+ {
+ public static void Execute(string command, string workingDirectoryPath)
+ {
+ using (Process cmd = new Process())
+ {
+ bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+
+ if (isWindows)
+ {
+ cmd.StartInfo = new ProcessStartInfo("cmd.exe", "/c " + command);
+ }
+ else
+ {
+ // works for OSX and Linux (at least on Ubuntu)
+ var escapedArgs = command.Replace("\"", "\\\"");
+ cmd.StartInfo = new ProcessStartInfo("bash", $"-c \"{escapedArgs}\"");
+ }
+
+ cmd.StartInfo.RedirectStandardInput = false;
+ cmd.StartInfo.RedirectStandardOutput = false;
+ cmd.StartInfo.RedirectStandardError = false;
+ cmd.StartInfo.CreateNoWindow = true;
+ cmd.StartInfo.UseShellExecute = false;
+ cmd.StartInfo.WorkingDirectory = workingDirectoryPath;
+ cmd.Start();
+ }
+ }
+ }
+}
diff --git a/ElectronNET.API/Screen.cs b/ElectronNET.API/Screen.cs
index 48bf392a..a8e91689 100644
--- a/ElectronNET.API/Screen.cs
+++ b/ElectronNET.API/Screen.cs
@@ -102,7 +102,7 @@ public event Action OnDisplayMetricsChanged
private event Action _onDisplayMetricsChanged;
private static Screen _screen;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Screen() { }
diff --git a/ElectronNET.API/Shell.cs b/ElectronNET.API/Shell.cs
index 5bf82c4a..d51c142f 100644
--- a/ElectronNET.API/Shell.cs
+++ b/ElectronNET.API/Shell.cs
@@ -4,6 +4,7 @@
using Newtonsoft.Json.Serialization;
using System.Threading.Tasks;
using ElectronNET.API.Extensions;
+using System.Runtime.Versioning;
namespace ElectronNET.API
{
@@ -13,7 +14,7 @@ namespace ElectronNET.API
public sealed class Shell
{
private static Shell _shell;
- private static object _syncRoot = new object();
+ private static readonly object _syncRoot = new();
internal Shell() { }
@@ -93,6 +94,7 @@ public Task OpenExternalAsync(string url)
/// Max 2081 characters on windows.
/// Controls the behavior of OpenExternal.
/// The error message corresponding to the failure if a failure occurred, otherwise .
+
public Task OpenExternalAsync(string url, OpenExternalOptions options)
{
var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -141,6 +143,7 @@ public void Beep()
/// Default is
/// Structure of a shortcut.
/// Whether the shortcut was created successfully.
+ [SupportedOSPlatform("windows")]
public Task WriteShortcutLinkAsync(string shortcutPath, ShortcutLinkOperation operation, ShortcutDetails options)
{
return BridgeConnector.OnResult("shell-writeShortcutLink", "shell-writeShortcutLinkCompleted", shortcutPath, operation.GetDescription(), options);
@@ -152,6 +155,7 @@ public Task WriteShortcutLinkAsync(string shortcutPath, ShortcutLinkOperat
///
/// The path tot the shortcut.
/// of the shortcut.
+ [SupportedOSPlatform("windows")]
public Task ReadShortcutLinkAsync(string shortcutPath)
{
return BridgeConnector.OnResult("shell-readShortcutLink", "shell-readShortcutLinkCompleted", shortcutPath);
diff --git a/ElectronNET.API/SocketIO/ByteArrayConverter.cs b/ElectronNET.API/SocketIO/ByteArrayConverter.cs
new file mode 100644
index 00000000..206b2664
--- /dev/null
+++ b/ElectronNET.API/SocketIO/ByteArrayConverter.cs
@@ -0,0 +1,64 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SocketIOClient.Newtonsoft.Json
+{
+ class ByteArrayConverter : JsonConverter
+ {
+ public ByteArrayConverter()
+ {
+ Bytes = new List();
+ }
+
+ internal List Bytes { get; }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(byte[]);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ byte[] bytes = null;
+ if (reader.TokenType == JsonToken.StartObject)
+ {
+ reader.Read();
+ if (reader.TokenType == JsonToken.PropertyName && reader.Value?.ToString() == "_placeholder")
+ {
+ reader.Read();
+ if (reader.TokenType == JsonToken.Boolean && (bool)reader.Value)
+ {
+ reader.Read();
+ if (reader.TokenType == JsonToken.PropertyName && reader.Value?.ToString() == "num")
+ {
+ reader.Read();
+ if (reader.Value != null)
+ {
+ if (int.TryParse(reader.Value.ToString(), out int num))
+ {
+ bytes = Bytes[num];
+ reader.Read();
+ }
+ }
+ }
+ }
+ }
+ }
+ return bytes;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, global::Newtonsoft.Json.JsonSerializer serializer)
+ {
+ var source = value as byte[];
+ Bytes.Add(source.ToArray());
+ writer.WriteStartObject();
+ writer.WritePropertyName("_placeholder");
+ writer.WriteValue(true);
+ writer.WritePropertyName("num");
+ writer.WriteValue(Bytes.Count - 1);
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/DisconnectReason.cs b/ElectronNET.API/SocketIO/DisconnectReason.cs
new file mode 100644
index 00000000..ed66d955
--- /dev/null
+++ b/ElectronNET.API/SocketIO/DisconnectReason.cs
@@ -0,0 +1,11 @@
+namespace SocketIOClient
+{
+ public class DisconnectReason
+ {
+ public static string IOServerDisconnect = "io server disconnect";
+ public static string IOClientDisconnect = "io client disconnect";
+ public static string PingTimeout = "ping timeout";
+ public static string TransportClose = "transport close";
+ public static string TransportError = "transport error";
+ }
+}
diff --git a/ElectronNET.API/SocketIO/EventHandlers.cs b/ElectronNET.API/SocketIO/EventHandlers.cs
new file mode 100644
index 00000000..48d5f020
--- /dev/null
+++ b/ElectronNET.API/SocketIO/EventHandlers.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace SocketIOClient
+{
+ public delegate void OnAnyHandler(string eventName, SocketIOResponse response);
+ public delegate void OnOpenedHandler(string sid, int pingInterval, int pingTimeout);
+ //public delegate void OnDisconnectedHandler(string sid, int pingInterval, int pingTimeout);
+ public delegate void OnAck(int packetId, List array);
+ public delegate void OnBinaryAck(int packetId, int totalCount, List array);
+ public delegate void OnBinaryReceived(int packetId, int totalCount, string eventName, List array);
+ public delegate void OnDisconnected();
+ public delegate void OnError(string error);
+ public delegate void OnEventReceived(int packetId, string eventName, List array);
+ public delegate void OnOpened(string sid, int pingInterval, int pingTimeout);
+ public delegate void OnPing();
+ public delegate void OnPong();
+}
diff --git a/ElectronNET.API/SocketIO/Extensions/CancellationTokenSourceExtensions.cs b/ElectronNET.API/SocketIO/Extensions/CancellationTokenSourceExtensions.cs
new file mode 100644
index 00000000..ff5ae99d
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Extensions/CancellationTokenSourceExtensions.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Threading;
+
+namespace SocketIOClient.Extensions
+{
+ internal static class CancellationTokenSourceExtensions
+ {
+ public static void TryDispose(this CancellationTokenSource cts)
+ {
+ cts?.Dispose();
+ }
+
+ public static void TryCancel(this CancellationTokenSource cts)
+ {
+ cts?.Cancel();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Extensions/DisposableExtensions.cs b/ElectronNET.API/SocketIO/Extensions/DisposableExtensions.cs
new file mode 100644
index 00000000..a0f8722d
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Extensions/DisposableExtensions.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace SocketIOClient.Extensions
+{
+ internal static class DisposableExtensions
+ {
+ public static void TryDispose(this IDisposable disposable)
+ {
+ disposable?.Dispose();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Extensions/EventHandlerExtensions.cs b/ElectronNET.API/SocketIO/Extensions/EventHandlerExtensions.cs
new file mode 100644
index 00000000..b05b8f38
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Extensions/EventHandlerExtensions.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace SocketIOClient.Extensions
+{
+ internal static class EventHandlerExtensions
+ {
+ public static void TryInvoke(this EventHandler handler, object sender, T args)
+ {
+ handler?.Invoke(sender, args);
+ }
+
+ public static void TryInvoke(this EventHandler handler, object sender, EventArgs args)
+ {
+ handler?.Invoke(sender, args);
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Extensions/SocketIOEventExtensions.cs b/ElectronNET.API/SocketIO/Extensions/SocketIOEventExtensions.cs
new file mode 100644
index 00000000..a03fadf4
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Extensions/SocketIOEventExtensions.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace SocketIOClient.Extensions
+{
+ internal static class SocketIOEventExtensions
+ {
+ public static void TryInvoke(this OnAnyHandler handler, string eventName, SocketIOResponse response)
+ {
+ try
+ {
+ handler(eventName, response);
+ }
+ catch
+ {
+ // The exception is thrown by the user code, so it can be swallowed
+ }
+ }
+ public static void TryInvoke(this Action handler, SocketIOResponse response)
+ {
+ try
+ {
+ handler(response);
+ }
+ catch
+ {
+ // The exception is thrown by the user code, so it can be swallowed
+ }
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/JsonSerializer/ByteArrayConverter.cs b/ElectronNET.API/SocketIO/JsonSerializer/ByteArrayConverter.cs
new file mode 100644
index 00000000..30bc7046
--- /dev/null
+++ b/ElectronNET.API/SocketIO/JsonSerializer/ByteArrayConverter.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SocketIOClient.JsonSerializer
+{
+ class ByteArrayConverter : JsonConverter
+ {
+ public ByteArrayConverter()
+ {
+ Bytes = new List();
+ }
+
+
+ public List Bytes { get; }
+
+ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ byte[] bytes = null;
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ reader.Read();
+ if (reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "_placeholder")
+ {
+ reader.Read();
+ if (reader.TokenType == JsonTokenType.True && reader.GetBoolean())
+ {
+ reader.Read();
+ if (reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "num")
+ {
+ reader.Read();
+ int num = reader.GetInt32();
+ bytes = Bytes[num];
+ reader.Read();
+ }
+ }
+ }
+ }
+ return bytes;
+ }
+
+ public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
+ {
+ Bytes.Add(value);
+ writer.WriteStartObject();
+ writer.WritePropertyName("_placeholder");
+ writer.WriteBooleanValue(true);
+ writer.WritePropertyName("num");
+ writer.WriteNumberValue(Bytes.Count - 1);
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/JsonSerializer/IJsonSerializer.cs b/ElectronNET.API/SocketIO/JsonSerializer/IJsonSerializer.cs
new file mode 100644
index 00000000..8cfe4b8a
--- /dev/null
+++ b/ElectronNET.API/SocketIO/JsonSerializer/IJsonSerializer.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace SocketIOClient.JsonSerializer
+{
+ public interface IJsonSerializer
+ {
+ JsonSerializeResult Serialize(object[] data);
+ T Deserialize(string json);
+ T Deserialize(string json, IList incomingBytes);
+ }
+}
diff --git a/ElectronNET.API/SocketIO/JsonSerializer/JsonSerializeResult.cs b/ElectronNET.API/SocketIO/JsonSerializer/JsonSerializeResult.cs
new file mode 100644
index 00000000..c4bc1e8c
--- /dev/null
+++ b/ElectronNET.API/SocketIO/JsonSerializer/JsonSerializeResult.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace SocketIOClient.JsonSerializer
+{
+ public class JsonSerializeResult
+ {
+ public string Json { get; set; }
+ public IList Bytes { get; set; }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/JsonSerializer/SystemTextJsonSerializer.cs b/ElectronNET.API/SocketIO/JsonSerializer/SystemTextJsonSerializer.cs
new file mode 100644
index 00000000..9a90981e
--- /dev/null
+++ b/ElectronNET.API/SocketIO/JsonSerializer/SystemTextJsonSerializer.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace SocketIOClient.JsonSerializer
+{
+ public class SystemTextJsonSerializer : IJsonSerializer
+ {
+ public JsonSerializeResult Serialize(object[] data)
+ {
+ var converter = new ByteArrayConverter();
+ var options = GetOptions();
+ options.Converters.Add(converter);
+ string json = System.Text.Json.JsonSerializer.Serialize(data, options);
+ return new JsonSerializeResult
+ {
+ Json = json,
+ Bytes = converter.Bytes
+ };
+ }
+
+ public T Deserialize(string json)
+ {
+ var options = GetOptions();
+ return System.Text.Json.JsonSerializer.Deserialize(json, options);
+ }
+
+ public T Deserialize(string json, IList bytes)
+ {
+ var options = GetOptions();
+ var converter = new ByteArrayConverter();
+ options.Converters.Add(converter);
+ converter.Bytes.AddRange(bytes);
+ return System.Text.Json.JsonSerializer.Deserialize(json, options);
+ }
+
+ private JsonSerializerOptions GetOptions()
+ {
+ JsonSerializerOptions options = null;
+ if (OptionsProvider != null)
+ {
+ options = OptionsProvider();
+ }
+ if (options == null)
+ {
+ options = new JsonSerializerOptions();
+ }
+ return options;
+ }
+
+ public Func OptionsProvider { get; set; }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/LICENSE b/ElectronNET.API/SocketIO/LICENSE
new file mode 100644
index 00000000..c0584ddf
--- /dev/null
+++ b/ElectronNET.API/SocketIO/LICENSE
@@ -0,0 +1,23 @@
+Code from https://github.com/doghappy/socket.io-client-csharp
+
+MIT License
+
+Copyright (c) 2019 HeroWong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ElectronNET.API/SocketIO/Messages/BinaryMessage.cs b/ElectronNET.API/SocketIO/Messages/BinaryMessage.cs
new file mode 100644
index 00000000..df2a7c00
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/BinaryMessage.cs
@@ -0,0 +1,100 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ public class BinaryMessage : IMessage
+ {
+ public MessageType Type => MessageType.BinaryMessage;
+
+ public string Namespace { get; set; }
+
+ public string Event { get; set; }
+
+ public int Id { get; set; }
+
+ public List JsonElements { get; set; }
+
+ public string Json { get; set; }
+
+ public int BinaryCount { get; set; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public void Read(string msg)
+ {
+ int index1 = msg.IndexOf('-');
+ BinaryCount = int.Parse(msg.Substring(0, index1));
+
+ int index2 = msg.IndexOf('[');
+
+ int index3 = msg.LastIndexOf(',', index2);
+ if (index3 > -1)
+ {
+ Namespace = msg.Substring(index1 + 1, index3 - index1 - 1);
+ int idLength = index2 - index3 - 1;
+ if (idLength > 0)
+ {
+ Id = int.Parse(msg.Substring(index3 + 1, idLength));
+ }
+ }
+ else
+ {
+ int idLength = index2 - index1 - 1;
+ if (idLength > 0)
+ {
+ Id = int.Parse(msg.Substring(index1 + 1, idLength));
+ }
+ }
+
+ string json = msg.Substring(index2);
+
+ var array = JsonDocument.Parse(json).RootElement.EnumerateArray();
+ int i = -1;
+ foreach (var item in array)
+ {
+ i++;
+ if (i == 0)
+ {
+ Event = item.GetString();
+ JsonElements = new List();
+ }
+ else
+ {
+ JsonElements.Add(item);
+ }
+ }
+ }
+
+ public string Write()
+ {
+ var builder = new StringBuilder();
+ builder
+ .Append("45")
+ .Append(OutgoingBytes.Count)
+ .Append('-');
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ if (string.IsNullOrEmpty(Json))
+ {
+ builder.Append("[\"").Append(Event).Append("\"]");
+ }
+ else
+ {
+ string data = Json.Insert(1, $"\"{Event}\",");
+ builder.Append(data);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/ClientAckMessage.cs b/ElectronNET.API/SocketIO/Messages/ClientAckMessage.cs
new file mode 100644
index 00000000..2b4dcda6
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/ClientAckMessage.cs
@@ -0,0 +1,75 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ ///
+ /// The server calls the client's callback
+ ///
+ public class ClientAckMessage : IMessage
+ {
+ public MessageType Type => MessageType.AckMessage;
+
+ public string Namespace { get; set; }
+
+ public string Event { get; set; }
+
+ public List JsonElements { get; set; }
+
+ public string Json { get; set; }
+
+ public int Id { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public void Read(string msg)
+ {
+ int index = msg.IndexOf('[');
+ int lastIndex = msg.LastIndexOf(',', index);
+ if (lastIndex > -1)
+ {
+ string text = msg.Substring(0, index);
+ Namespace = text.Substring(0, lastIndex);
+ Id = int.Parse(text.Substring(lastIndex + 1));
+ }
+ else
+ {
+ Id = int.Parse(msg.Substring(0, index));
+ }
+ msg = msg.Substring(index);
+ JsonElements = JsonDocument.Parse(msg).RootElement.EnumerateArray().ToList();
+ }
+
+ public string Write()
+ {
+ var builder = new StringBuilder();
+ builder.Append("42");
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ builder.Append(Id);
+ if (string.IsNullOrEmpty(Json))
+ {
+ builder.Append("[\"").Append(Event).Append("\"]");
+ }
+ else
+ {
+ string data = Json.Insert(1, $"\"{Event}\",");
+ builder.Append(data);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/ClientBinaryAckMessage.cs b/ElectronNET.API/SocketIO/Messages/ClientBinaryAckMessage.cs
new file mode 100644
index 00000000..f1512683
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/ClientBinaryAckMessage.cs
@@ -0,0 +1,82 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ ///
+ /// The server calls the client's callback with binary
+ ///
+ public class ClientBinaryAckMessage : IMessage
+ {
+ public MessageType Type => MessageType.BinaryAckMessage;
+
+ public string Namespace { get; set; }
+
+ public string Event { get; set; }
+
+ public List JsonElements { get; set; }
+
+ public string Json { get; set; }
+
+ public int Id { get; set; }
+
+ public int BinaryCount { get; set; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public void Read(string msg)
+ {
+ int index1 = msg.IndexOf('-');
+ BinaryCount = int.Parse(msg.Substring(0, index1));
+
+ int index2 = msg.IndexOf('[');
+
+ int index3 = msg.LastIndexOf(',', index2);
+ if (index3 > -1)
+ {
+ Namespace = msg.Substring(index1 + 1, index3 - index1 - 1);
+ Id = int.Parse(msg.Substring(index3 + 1, index2 - index3 - 1));
+ }
+ else
+ {
+ Id = int.Parse(msg.Substring(index1 + 1, index2 - index1 - 1));
+ }
+
+ string json = msg.Substring(index2);
+ JsonElements = JsonDocument.Parse(json).RootElement.EnumerateArray().ToList();
+ }
+
+ public string Write()
+ {
+ var builder = new StringBuilder();
+ builder
+ .Append("45")
+ .Append(OutgoingBytes.Count)
+ .Append('-');
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ builder.Append(Id);
+ if (string.IsNullOrEmpty(Json))
+ {
+ builder.Append("[\"").Append(Event).Append("\"]");
+ }
+ else
+ {
+ string data = Json.Insert(1, $"\"{Event}\",");
+ builder.Append(data);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/ConnectedMessage.cs b/ElectronNET.API/SocketIO/Messages/ConnectedMessage.cs
new file mode 100644
index 00000000..0663ad58
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/ConnectedMessage.cs
@@ -0,0 +1,128 @@
+using System;
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ public class ConnectedMessage : IMessage
+ {
+ public MessageType Type => MessageType.Connected;
+
+ public string Namespace { get; set; }
+
+ public string Sid { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public IEnumerable> Query { get; set; }
+ public string AuthJsonStr { get; set; }
+
+ public void Read(string msg)
+ {
+ if (Eio == 3)
+ {
+ Eio3Read(msg);
+ }
+ else
+ {
+ Eio4Read(msg);
+ }
+ }
+
+ public string Write()
+ {
+ if (Eio == 3)
+ {
+ return Eio3Write();
+ }
+ return Eio4Write();
+ }
+
+ public void Eio4Read(string msg)
+ {
+ int index = msg.IndexOf('{');
+ if (index > 0)
+ {
+ Namespace = msg.Substring(0, index - 1);
+ msg = msg.Substring(index);
+ }
+ else
+ {
+ Namespace = string.Empty;
+ }
+ Sid = JsonDocument.Parse(msg).RootElement.GetProperty("sid").GetString();
+ }
+
+ public string Eio4Write()
+ {
+ var builder = new StringBuilder("40");
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ builder.Append(AuthJsonStr);
+ return builder.ToString();
+ }
+
+ public void Eio3Read(string msg)
+ {
+ if (msg.Length >= 2)
+ {
+ int startIndex = msg.IndexOf('/');
+ if (startIndex == -1)
+ {
+ return;
+ }
+ int endIndex = msg.IndexOf('?', startIndex);
+ if (endIndex == -1)
+ {
+ endIndex = msg.IndexOf(',', startIndex);
+ }
+ if (endIndex == -1)
+ {
+ endIndex = msg.Length;
+ }
+ Namespace = msg.Substring(startIndex, endIndex);
+ }
+ }
+
+ public string Eio3Write()
+ {
+ if (string.IsNullOrEmpty(Namespace))
+ {
+ return string.Empty;
+ }
+ var builder = new StringBuilder("40");
+ builder.Append(Namespace);
+ if (Query != null)
+ {
+ int i = -1;
+ foreach (var item in Query)
+ {
+ i++;
+ if (i == 0)
+ {
+ builder.Append('?');
+ }
+ else
+ {
+ builder.Append('&');
+ }
+ builder.Append(item.Key).Append('=').Append(item.Value);
+ }
+ }
+ builder.Append(',');
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/DisconnectedMessage.cs b/ElectronNET.API/SocketIO/Messages/DisconnectedMessage.cs
new file mode 100644
index 00000000..4bceba9b
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/DisconnectedMessage.cs
@@ -0,0 +1,36 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+
+namespace SocketIOClient.Messages
+{
+ public class DisconnectedMessage : IMessage
+ {
+ public MessageType Type => MessageType.Disconnected;
+
+ public string Namespace { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public void Read(string msg)
+ {
+ Namespace = msg.TrimEnd(',');
+ }
+
+ public string Write()
+ {
+ if (string.IsNullOrEmpty(Namespace))
+ {
+ return "41";
+ }
+ return "41" + Namespace + ",";
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/ErrorMessage.cs b/ElectronNET.API/SocketIO/Messages/ErrorMessage.cs
new file mode 100644
index 00000000..f36d78b0
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/ErrorMessage.cs
@@ -0,0 +1,50 @@
+using SocketIOClient.Transport;
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ public class ErrorMessage : IMessage
+ {
+ public MessageType Type => MessageType.ErrorMessage;
+
+ public string Message { get; set; }
+
+ public string Namespace { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public void Read(string msg)
+ {
+ if (Eio == 3)
+ {
+ Message = msg.Trim('"');
+ }
+ else
+ {
+ int index = msg.IndexOf('{');
+ if (index > 0)
+ {
+ Namespace = msg.Substring(0, index - 1);
+ msg = msg.Substring(index);
+ }
+ var doc = JsonDocument.Parse(msg);
+ Message = doc.RootElement.GetProperty("message").GetString();
+ }
+ }
+
+ public string Write()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/EventMessage.cs b/ElectronNET.API/SocketIO/Messages/EventMessage.cs
new file mode 100644
index 00000000..522bd209
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/EventMessage.cs
@@ -0,0 +1,97 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ public class EventMessage : IMessage
+ {
+ public MessageType Type => MessageType.EventMessage;
+
+ public string Namespace { get; set; }
+
+ public string Event { get; set; }
+
+ public int Id { get; set; }
+
+ public List JsonElements { get; set; }
+
+ public string Json { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public void Read(string msg)
+ {
+ int index = msg.IndexOf('[');
+ int lastIndex = msg.LastIndexOf(',', index);
+ if (lastIndex > -1)
+ {
+ string text = msg.Substring(0, index);
+ Namespace = text.Substring(0, lastIndex);
+ if (index - lastIndex > 1)
+ {
+ Id = int.Parse(text.Substring(lastIndex + 1));
+ }
+ }
+ else
+ {
+ if (index > 0)
+ {
+ Id = int.Parse(msg.Substring(0, index));
+ }
+ }
+ msg = msg.Substring(index);
+
+ //int index = msg.IndexOf('[');
+ //if (index > 0)
+ //{
+ // Namespace = msg.Substring(0, index - 1);
+ // msg = msg.Substring(index);
+ //}
+ var array = JsonDocument.Parse(msg).RootElement.EnumerateArray();
+ int i = -1;
+ foreach (var item in array)
+ {
+ i++;
+ if (i == 0)
+ {
+ Event = item.GetString();
+ JsonElements = new List();
+ }
+ else
+ {
+ JsonElements.Add(item);
+ }
+ }
+ }
+
+ public string Write()
+ {
+ var builder = new StringBuilder();
+ builder.Append("42");
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ if (string.IsNullOrEmpty(Json))
+ {
+ builder.Append("[\"").Append(Event).Append("\"]");
+ }
+ else
+ {
+ string data = Json.Insert(1, $"\"{Event}\",");
+ builder.Append(data);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/IMessage.cs b/ElectronNET.API/SocketIO/Messages/IMessage.cs
new file mode 100644
index 00000000..c7f0e250
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/IMessage.cs
@@ -0,0 +1,30 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+
+namespace SocketIOClient.Messages
+{
+ public interface IMessage
+ {
+ MessageType Type { get; }
+
+ List OutgoingBytes { get; set; }
+
+ List IncomingBytes { get; set; }
+
+ int BinaryCount { get; }
+
+ int Eio { get; set; }
+
+ TransportProtocol Protocol { get; set; }
+
+ void Read(string msg);
+
+ //void Eio3WsRead(string msg);
+
+ //void Eio3HttpRead(string msg);
+
+ string Write();
+
+ //string Eio3WsWrite();
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/MessageFactory.cs b/ElectronNET.API/SocketIO/Messages/MessageFactory.cs
new file mode 100644
index 00000000..227a6f72
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/MessageFactory.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SocketIOClient.Messages
+{
+ public static class MessageFactory
+ {
+ private static IMessage CreateMessage(MessageType type)
+ {
+ switch (type)
+ {
+ case MessageType.Opened:
+ return new OpenedMessage();
+ case MessageType.Ping:
+ return new PingMessage();
+ case MessageType.Pong:
+ return new PongMessage();
+ case MessageType.Connected:
+ return new ConnectedMessage();
+ case MessageType.Disconnected:
+ return new DisconnectedMessage();
+ case MessageType.EventMessage:
+ return new EventMessage();
+ case MessageType.AckMessage:
+ return new ClientAckMessage();
+ case MessageType.ErrorMessage:
+ return new ErrorMessage();
+ case MessageType.BinaryMessage:
+ return new BinaryMessage();
+ case MessageType.BinaryAckMessage:
+ return new ClientBinaryAckMessage();
+ }
+ return null;
+ }
+
+ private static readonly Dictionary _messageTypes = Enum.GetValues().ToDictionary(v => ((int)v).ToString(), v => v);
+
+ public static IMessage CreateMessage(int eio, string msg)
+ {
+ foreach (var (prefix,item) in _messageTypes)
+ {
+ if (msg.StartsWith(prefix))
+ {
+ IMessage result = CreateMessage(item);
+ if (result != null)
+ {
+ result.Eio = eio;
+ result.Read(msg.Substring(prefix.Length));
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static OpenedMessage CreateOpenedMessage(string msg)
+ {
+ var openedMessage = new OpenedMessage();
+ if (msg[0] == '0')
+ {
+ openedMessage.Eio = 4;
+ openedMessage.Read(msg.Substring(1));
+ }
+ else
+ {
+ openedMessage.Eio = 3;
+ int index = msg.IndexOf(':');
+ openedMessage.Read(msg.Substring(index + 2));
+ }
+ return openedMessage;
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/MessageType.cs b/ElectronNET.API/SocketIO/Messages/MessageType.cs
new file mode 100644
index 00000000..345c98f6
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/MessageType.cs
@@ -0,0 +1,16 @@
+namespace SocketIOClient.Messages
+{
+ public enum MessageType
+ {
+ Opened,
+ Ping = 2,
+ Pong,
+ Connected = 40,
+ Disconnected,
+ EventMessage,
+ AckMessage,
+ ErrorMessage,
+ BinaryMessage,
+ BinaryAckMessage
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/OpenedMessage.cs b/ElectronNET.API/SocketIO/Messages/OpenedMessage.cs
new file mode 100644
index 00000000..df297eae
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/OpenedMessage.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Text.Json;
+using System.Collections.Generic;
+using SocketIOClient.Transport;
+
+namespace SocketIOClient.Messages
+{
+ public class OpenedMessage : IMessage
+ {
+ public MessageType Type => MessageType.Opened;
+
+ public string Sid { get; set; }
+
+ public string Namespace { get; set; }
+
+ public List Upgrades { get; private set; }
+
+ public int PingInterval { get; private set; }
+
+ public int PingTimeout { get; private set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ private int GetInt32FromJsonElement(JsonElement element, string msg, string name)
+ {
+ var p = element.GetProperty(name);
+ int val;
+ switch (p.ValueKind)
+ {
+ case JsonValueKind.String:
+ val = int.Parse(p.GetString());
+ break;
+ case JsonValueKind.Number:
+ val = p.GetInt32();
+ break;
+ default:
+ throw new ArgumentException($"Invalid message: '{msg}'");
+ }
+ return val;
+ }
+
+ public void Read(string msg)
+ {
+ var doc = JsonDocument.Parse(msg);
+ var root = doc.RootElement;
+ Sid = root.GetProperty("sid").GetString();
+
+ PingInterval = GetInt32FromJsonElement(root, msg, "pingInterval");
+ PingTimeout = GetInt32FromJsonElement(root, msg, "pingTimeout");
+
+ Upgrades = new List();
+ var upgrades = root.GetProperty("upgrades").EnumerateArray();
+ foreach (var item in upgrades)
+ {
+ Upgrades.Add(item.GetString());
+ }
+ }
+
+ public string Write()
+ {
+ //var builder = new StringBuilder();
+ //builder.Append("40");
+ //if (!string.IsNullOrEmpty(Namespace))
+ //{
+ // builder.Append(Namespace).Append(',');
+ //}
+ //return builder.ToString();
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/PingMessage.cs b/ElectronNET.API/SocketIO/Messages/PingMessage.cs
new file mode 100644
index 00000000..fa2b1346
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/PingMessage.cs
@@ -0,0 +1,26 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+
+namespace SocketIOClient.Messages
+{
+ public class PingMessage : IMessage
+ {
+ public MessageType Type => MessageType.Ping;
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public void Read(string msg)
+ {
+ }
+
+ public string Write() => "2";
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/PongMessage.cs b/ElectronNET.API/SocketIO/Messages/PongMessage.cs
new file mode 100644
index 00000000..fb4ccfa4
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/PongMessage.cs
@@ -0,0 +1,29 @@
+using SocketIOClient.Transport;
+using System;
+using System.Collections.Generic;
+
+namespace SocketIOClient.Messages
+{
+ public class PongMessage : IMessage
+ {
+ public MessageType Type => MessageType.Pong;
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public TimeSpan Duration { get; set; }
+
+ public void Read(string msg)
+ {
+ }
+
+ public string Write() => "3";
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/ServerAckMessage.cs b/ElectronNET.API/SocketIO/Messages/ServerAckMessage.cs
new file mode 100644
index 00000000..ce8ce1ae
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/ServerAckMessage.cs
@@ -0,0 +1,54 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SocketIOClient.Messages
+{
+ ///
+ /// The client calls the server's callback
+ ///
+ public class ServerAckMessage : IMessage
+ {
+ public MessageType Type => MessageType.AckMessage;
+
+ public string Namespace { get; set; }
+
+ public string Json { get; set; }
+
+ public int Id { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public void Read(string msg)
+ {
+ }
+
+ public string Write()
+ {
+ var builder = new StringBuilder();
+ builder.Append("43");
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ builder.Append(Id);
+ if (string.IsNullOrEmpty(Json))
+ {
+ builder.Append("[]");
+ }
+ else
+ {
+ builder.Append(Json);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Messages/ServerBinaryAckMessage.cs b/ElectronNET.API/SocketIO/Messages/ServerBinaryAckMessage.cs
new file mode 100644
index 00000000..199f309f
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Messages/ServerBinaryAckMessage.cs
@@ -0,0 +1,60 @@
+using SocketIOClient.Transport;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+
+namespace SocketIOClient.Messages
+{
+ ///
+ /// The client calls the server's callback with binary
+ ///
+ public class ServerBinaryAckMessage : IMessage
+ {
+ public MessageType Type => MessageType.BinaryAckMessage;
+
+ public string Namespace { get; set; }
+
+ public List JsonElements { get; set; }
+
+ public string Json { get; set; }
+
+ public int Id { get; set; }
+
+ public int BinaryCount { get; }
+
+ public int Eio { get; set; }
+
+ public TransportProtocol Protocol { get; set; }
+
+ public List OutgoingBytes { get; set; }
+
+ public List IncomingBytes { get; set; }
+
+ public void Read(string msg)
+ {
+ }
+
+ public string Write()
+ {
+ var builder = new StringBuilder();
+ builder
+ .Append("46")
+ .Append(OutgoingBytes.Count)
+ .Append('-');
+ if (!string.IsNullOrEmpty(Namespace))
+ {
+ builder.Append(Namespace).Append(',');
+ }
+ builder.Append(Id);
+ if (string.IsNullOrEmpty(Json))
+ {
+ builder.Append("[]");
+ }
+ else
+ {
+ builder.Append(Json);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/NewtonsoftJsonSerializer.cs b/ElectronNET.API/SocketIO/NewtonsoftJsonSerializer.cs
new file mode 100644
index 00000000..9691ffca
--- /dev/null
+++ b/ElectronNET.API/SocketIO/NewtonsoftJsonSerializer.cs
@@ -0,0 +1,56 @@
+using System;
+using Newtonsoft.Json;
+using SocketIOClient.JsonSerializer;
+using System.Collections.Generic;
+
+namespace SocketIOClient.Newtonsoft.Json
+{
+ public class NewtonsoftJsonSerializer : IJsonSerializer
+ {
+ public Func JsonSerializerOptions { get; }
+
+ public JsonSerializeResult Serialize(object[] data)
+ {
+ var converter = new ByteArrayConverter();
+ var settings = GetOptions();
+ settings.Converters.Add(converter);
+ string json = JsonConvert.SerializeObject(data, settings);
+ return new JsonSerializeResult
+ {
+ Json = json,
+ Bytes = converter.Bytes
+ };
+ }
+
+ public T Deserialize(string json)
+ {
+ var settings = GetOptions();
+ return JsonConvert.DeserializeObject(json, settings);
+ }
+
+ public T Deserialize(string json, IList bytes)
+ {
+ var converter = new ByteArrayConverter();
+ converter.Bytes.AddRange(bytes);
+ var settings = GetOptions();
+ settings.Converters.Add(converter);
+ return JsonConvert.DeserializeObject(json, settings);
+ }
+
+ private JsonSerializerSettings GetOptions()
+ {
+ JsonSerializerSettings options = null;
+ if (OptionsProvider != null)
+ {
+ options = OptionsProvider();
+ }
+ if (options == null)
+ {
+ options = new JsonSerializerSettings();
+ }
+ return options;
+ }
+
+ public Func OptionsProvider { get; set; }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/SocketIO.cs b/ElectronNET.API/SocketIO/SocketIO.cs
new file mode 100644
index 00000000..edf2f1c3
--- /dev/null
+++ b/ElectronNET.API/SocketIO/SocketIO.cs
@@ -0,0 +1,769 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net.Http;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using SocketIOClient.Extensions;
+using SocketIOClient.JsonSerializer;
+using SocketIOClient.Messages;
+using SocketIOClient.Transport;
+using SocketIOClient.UriConverters;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace SocketIOClient
+{
+ ///
+ /// socket.io client class
+ ///
+ public class SocketIO : IDisposable
+ {
+ ///
+ /// Create SocketIO object with default options
+ ///
+ ///
+ public SocketIO(string uri) : this(new Uri(uri)) { }
+
+ ///
+ /// Create SocketIO object with options
+ ///
+ ///
+ public SocketIO(Uri uri) : this(uri, new SocketIOOptions()) { }
+
+ ///
+ /// Create SocketIO object with options
+ ///
+ ///
+ ///
+ public SocketIO(string uri, SocketIOOptions options) : this(new Uri(uri), options) { }
+
+ ///
+ /// Create SocketIO object with options
+ ///
+ ///
+ ///
+ public SocketIO(Uri uri, SocketIOOptions options)
+ {
+ ServerUri = uri ?? throw new ArgumentNullException("uri");
+ Options = options ?? throw new ArgumentNullException("options");
+ Initialize();
+ }
+
+ Uri _serverUri;
+ private Uri ServerUri
+ {
+ get => _serverUri;
+ set
+ {
+ if (_serverUri != value)
+ {
+ _serverUri = value;
+ if (value != null && value.AbsolutePath != "/")
+ {
+ _namespace = value.AbsolutePath;
+ }
+ }
+ }
+ }
+
+ ///
+ /// An unique identifier for the socket session. Set after the connect event is triggered, and updated after the reconnect event.
+ ///
+ public string Id { get; private set; }
+
+ string _namespace;
+
+ ///
+ /// Whether or not the socket is connected to the server.
+ ///
+ public bool Connected { get; private set; }
+
+ int _attempts;
+
+ [Obsolete]
+ ///
+ /// Whether or not the socket is disconnected from the server.
+ ///
+ public bool Disconnected => !Connected;
+
+ public SocketIOOptions Options { get; }
+
+ public IJsonSerializer JsonSerializer { get; set; }
+
+ public IUriConverter UriConverter { get; set; }
+
+ internal ILogger Logger { get; set; }
+
+ ILoggerFactory _loggerFactory;
+ public ILoggerFactory LoggerFactory
+ {
+ get => _loggerFactory;
+ set
+ {
+ _loggerFactory = value ?? throw new ArgumentNullException(nameof(LoggerFactory));
+ Logger = _loggerFactory.CreateLogger();
+ }
+ }
+
+ public HttpClient HttpClient { get; set; }
+
+ public Func ClientWebSocketProvider { get; set; }
+ private IClientWebSocket _clientWebsocket;
+
+ BaseTransport _transport;
+
+ List _expectedExceptions;
+
+ int _packetId;
+ bool _isConnectCoreRunning;
+ Uri _realServerUri;
+ Exception _connectCoreException;
+ Dictionary> _ackHandlers;
+ List _onAnyHandlers;
+ Dictionary> _eventHandlers;
+ CancellationTokenSource _connectionTokenSource;
+ double _reconnectionDelay;
+ bool _hasError;
+ bool _isFaild;
+ readonly static object _connectionLock = new object();
+
+ #region Socket.IO event
+ public event EventHandler OnConnected;
+ //public event EventHandler OnConnectError;
+ //public event EventHandler OnConnectTimeout;
+ public event EventHandler OnError;
+ public event EventHandler OnDisconnected;
+
+ ///
+ /// Fired upon a successful reconnection.
+ ///
+ public event EventHandler OnReconnected;
+
+ ///
+ /// Fired upon an attempt to reconnect.
+ ///
+ public event EventHandler OnReconnectAttempt;
+
+ ///
+ /// Fired upon a reconnection attempt error.
+ ///
+ public event EventHandler OnReconnectError;
+
+ ///
+ /// Fired when couldn’t reconnect within reconnectionAttempts
+ ///
+ public event EventHandler OnReconnectFailed;
+ public event EventHandler OnPing;
+ public event EventHandler OnPong;
+
+ #endregion
+
+ #region Observable Event
+ //Subject _onConnected;
+ //public IObservable ConnectedObservable { get; private set; }
+ #endregion
+
+ private void Initialize()
+ {
+ _packetId = -1;
+ _ackHandlers = new Dictionary>();
+ _eventHandlers = new Dictionary>();
+ _onAnyHandlers = new List();
+
+ JsonSerializer = new SystemTextJsonSerializer();
+ UriConverter = new UriConverter();
+
+ HttpClient = new HttpClient();
+ ClientWebSocketProvider = () => new SystemNetWebSocketsClientWebSocket(Options.EIO);
+ _expectedExceptions = new List
+ {
+ typeof(TimeoutException),
+ typeof(WebSocketException),
+ typeof(HttpRequestException),
+ typeof(OperationCanceledException),
+ typeof(TaskCanceledException)
+ };
+ LoggerFactory = NullLoggerFactory.Instance;
+ }
+
+ private async Task CreateTransportAsync()
+ {
+ Options.Transport = await GetProtocolAsync();
+ if (Options.Transport == TransportProtocol.Polling)
+ {
+ HttpPollingHandler handler;
+ if (Options.EIO == 3)
+ handler = new Eio3HttpPollingHandler(HttpClient);
+ else
+ handler = new Eio4HttpPollingHandler(HttpClient);
+ _transport = new HttpTransport(HttpClient, handler, Options, JsonSerializer, Logger);
+ }
+ else
+ {
+ _clientWebsocket = ClientWebSocketProvider();
+ _transport = new WebSocketTransport(_clientWebsocket, Options, JsonSerializer, Logger);
+ }
+ _transport.Namespace = _namespace;
+ SetHeaders();
+ }
+
+ private void SetHeaders()
+ {
+ if (Options.ExtraHeaders != null)
+ {
+ foreach (var item in Options.ExtraHeaders)
+ {
+ _transport.AddHeader(item.Key, item.Value);
+ }
+ }
+ }
+
+ private void SyncExceptionToMain(Exception e)
+ {
+ _connectCoreException = e;
+ _isConnectCoreRunning = false;
+ }
+
+ private void ConnectCore()
+ {
+ DisposeForReconnect();
+ _reconnectionDelay = Options.ReconnectionDelay;
+ _connectionTokenSource = new CancellationTokenSource();
+ var cct = _connectionTokenSource.Token;
+ Task.Factory.StartNew(async () =>
+ {
+ while (true)
+ {
+ _clientWebsocket.TryDispose();
+ _transport.TryDispose();
+ CreateTransportAsync().Wait();
+ _realServerUri = UriConverter.GetServerUri(Options.Transport == TransportProtocol.WebSocket, ServerUri, Options.EIO, Options.Path, Options.Query);
+ try
+ {
+ if (cct.IsCancellationRequested)
+ break;
+ if (_attempts > 0)
+ OnReconnectAttempt.TryInvoke(this, _attempts);
+ var timeoutCts = new CancellationTokenSource(Options.ConnectionTimeout);
+ _transport.Subscribe(OnMessageReceived, OnErrorReceived);
+ await _transport.ConnectAsync(_realServerUri, timeoutCts.Token).ConfigureAwait(false);
+ break;
+ }
+ catch (Exception e)
+ {
+ if (_expectedExceptions.Contains(e.GetType()))
+ {
+ if (!Options.Reconnection)
+ {
+ SyncExceptionToMain(e);
+ throw;
+ }
+ if (_attempts > 0)
+ {
+ OnReconnectError.TryInvoke(this, e);
+ }
+ _attempts++;
+ if (_attempts <= Options.ReconnectionAttempts)
+ {
+ if (_reconnectionDelay < Options.ReconnectionDelayMax)
+ {
+ _reconnectionDelay += 2 * Options.RandomizationFactor;
+ }
+ if (_reconnectionDelay > Options.ReconnectionDelayMax)
+ {
+ _reconnectionDelay = Options.ReconnectionDelayMax;
+ }
+ Thread.Sleep((int)_reconnectionDelay);
+ }
+ else
+ {
+ _isFaild = true;
+ OnReconnectFailed.TryInvoke(this, EventArgs.Empty);
+ break;
+ }
+ }
+ else
+ {
+ SyncExceptionToMain(e);
+ throw;
+ }
+ }
+ }
+ _isConnectCoreRunning = false;
+ });
+ }
+
+ private async Task GetProtocolAsync()
+ {
+ if (Options.Transport == TransportProtocol.Polling && Options.AutoUpgrade)
+ {
+ Uri uri = UriConverter.GetServerUri(false, ServerUri, Options.EIO, Options.Path, Options.Query);
+ try
+ {
+ string text = await HttpClient.GetStringAsync(uri);
+ if (text.Contains("websocket"))
+ {
+ return TransportProtocol.WebSocket;
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning(e, e.Message);
+ }
+ }
+ return Options.Transport;
+ }
+
+ public async Task ConnectAsync()
+ {
+ if (Connected || _isConnectCoreRunning)
+ return;
+
+ lock (_connectionLock)
+ {
+ if (_isConnectCoreRunning)
+ return;
+ _isConnectCoreRunning = true;
+ }
+ ConnectCore();
+ while (_isConnectCoreRunning)
+ {
+ await Task.Delay(100);
+ }
+ if (_connectCoreException != null)
+ {
+ Logger.LogError(_connectCoreException, _connectCoreException.Message);
+ throw _connectCoreException;
+ }
+ int ms = 0;
+ while (!Connected)
+ {
+ if (_hasError)
+ {
+ Logger.LogWarning($"Got a connection error, try to use '{nameof(OnError)}' to detect it.");
+ break;
+ }
+ if (_isFaild)
+ {
+ Logger.LogWarning($"Reconnect failed, try to use '{nameof(OnReconnectFailed)}' to detect it.");
+ break;
+ }
+ ms += 100;
+ if (ms > Options.ConnectionTimeout.TotalMilliseconds)
+ {
+ throw new TimeoutException();
+ }
+ await Task.Delay(100);
+ }
+ }
+
+ private void PingHandler()
+ {
+ OnPing.TryInvoke(this, EventArgs.Empty);
+ }
+
+ private void PongHandler(PongMessage msg)
+ {
+ OnPong.TryInvoke(this, msg.Duration);
+ }
+
+ private void ConnectedHandler(ConnectedMessage msg)
+ {
+ Id = msg.Sid;
+ Connected = true;
+ OnConnected.TryInvoke(this, EventArgs.Empty);
+ if (_attempts > 0)
+ {
+ OnReconnected.TryInvoke(this, _attempts);
+ }
+ _attempts = 0;
+ }
+
+ private void DisconnectedHandler()
+ {
+ _ = InvokeDisconnect(DisconnectReason.IOServerDisconnect);
+ }
+
+ private void EventMessageHandler(EventMessage m)
+ {
+ var res = new SocketIOResponse(m.JsonElements, this)
+ {
+ PacketId = m.Id
+ };
+ foreach (var item in _onAnyHandlers)
+ {
+ item.TryInvoke(m.Event, res);
+ }
+ if (_eventHandlers.ContainsKey(m.Event))
+ {
+ _eventHandlers[m.Event].TryInvoke(res);
+ }
+ }
+
+ private void AckMessageHandler(ClientAckMessage m)
+ {
+ if (_ackHandlers.ContainsKey(m.Id))
+ {
+ var res = new SocketIOResponse(m.JsonElements, this);
+ _ackHandlers[m.Id].TryInvoke(res);
+ _ackHandlers.Remove(m.Id);
+ }
+ }
+
+ private void ErrorMessageHandler(ErrorMessage msg)
+ {
+ _hasError = true;
+ OnError.TryInvoke(this, msg.Message);
+ }
+
+ private void BinaryMessageHandler(BinaryMessage msg)
+ {
+ var response = new SocketIOResponse(msg.JsonElements, this)
+ {
+ PacketId = msg.Id,
+ };
+ response.InComingBytes.AddRange(msg.IncomingBytes);
+ foreach (var item in _onAnyHandlers)
+ {
+ item.TryInvoke(msg.Event, response);
+ }
+ if (_eventHandlers.ContainsKey(msg.Event))
+ {
+ _eventHandlers[msg.Event].TryInvoke(response);
+ }
+ }
+
+ private void BinaryAckMessageHandler(ClientBinaryAckMessage msg)
+ {
+ if (_ackHandlers.ContainsKey(msg.Id))
+ {
+ var response = new SocketIOResponse(msg.JsonElements, this)
+ {
+ PacketId = msg.Id,
+ };
+ response.InComingBytes.AddRange(msg.IncomingBytes);
+ _ackHandlers[msg.Id].TryInvoke(response);
+ }
+ }
+
+ private void OnErrorReceived(Exception ex)
+ {
+ Logger.LogError(ex, ex.Message);
+ _ = InvokeDisconnect(DisconnectReason.TransportClose);
+ }
+
+ private void OnMessageReceived(IMessage msg)
+ {
+ try
+ {
+ switch (msg.Type)
+ {
+ case MessageType.Ping:
+ PingHandler();
+ break;
+ case MessageType.Pong:
+ PongHandler(msg as PongMessage);
+ break;
+ case MessageType.Connected:
+ ConnectedHandler(msg as ConnectedMessage);
+ break;
+ case MessageType.Disconnected:
+ DisconnectedHandler();
+ break;
+ case MessageType.EventMessage:
+ EventMessageHandler(msg as EventMessage);
+ break;
+ case MessageType.AckMessage:
+ AckMessageHandler(msg as ClientAckMessage);
+ break;
+ case MessageType.ErrorMessage:
+ ErrorMessageHandler(msg as ErrorMessage);
+ break;
+ case MessageType.BinaryMessage:
+ BinaryMessageHandler(msg as BinaryMessage);
+ break;
+ case MessageType.BinaryAckMessage:
+ BinaryAckMessageHandler(msg as ClientBinaryAckMessage);
+ break;
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, e.Message);
+ }
+ }
+
+ public async Task DisconnectAsync()
+ {
+ if (Connected)
+ {
+ var msg = new DisconnectedMessage
+ {
+ Namespace = _namespace
+ };
+ try
+ {
+ await _transport.SendAsync(msg, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, e.Message);
+ }
+ await InvokeDisconnect(DisconnectReason.IOClientDisconnect);
+ }
+ }
+
+ ///
+ /// Register a new handler for the given event.
+ ///
+ ///
+ ///
+ public void On(string eventName, Action callback)
+ {
+ if (_eventHandlers.ContainsKey(eventName))
+ {
+ _eventHandlers.Remove(eventName);
+ }
+ _eventHandlers.Add(eventName, callback);
+ }
+
+
+
+ ///
+ /// Unregister a new handler for the given event.
+ ///
+ ///
+ public void Off(string eventName)
+ {
+ if (_eventHandlers.ContainsKey(eventName))
+ {
+ _eventHandlers.Remove(eventName);
+ }
+ }
+
+ public void OnAny(OnAnyHandler handler)
+ {
+ if (handler != null)
+ {
+ _onAnyHandlers.Add(handler);
+ }
+ }
+
+ public void PrependAny(OnAnyHandler handler)
+ {
+ if (handler != null)
+ {
+ _onAnyHandlers.Insert(0, handler);
+ }
+ }
+
+ public void OffAny(OnAnyHandler handler)
+ {
+ if (handler != null)
+ {
+ _onAnyHandlers.Remove(handler);
+ }
+ }
+
+ public OnAnyHandler[] ListenersAny() => _onAnyHandlers.ToArray();
+
+ internal async Task ClientAckAsync(int packetId, CancellationToken cancellationToken, params object[] data)
+ {
+ IMessage msg;
+ if (data != null && data.Length > 0)
+ {
+ var result = JsonSerializer.Serialize(data);
+ if (result.Bytes.Count > 0)
+ {
+ msg = new ServerBinaryAckMessage
+ {
+ Id = packetId,
+ Namespace = _namespace,
+ Json = result.Json
+ };
+ msg.OutgoingBytes = new List(result.Bytes);
+ }
+ else
+ {
+ msg = new ServerAckMessage
+ {
+ Namespace = _namespace,
+ Id = packetId,
+ Json = result.Json
+ };
+ }
+ }
+ else
+ {
+ msg = new ServerAckMessage
+ {
+ Namespace = _namespace,
+ Id = packetId
+ };
+ }
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Emits an event to the socket
+ ///
+ ///
+ /// Any other parameters can be included. All serializable datastructures are supported, including byte[]
+ ///
+ public async Task EmitAsync(string eventName, params object[] data)
+ {
+ await EmitAsync(eventName, CancellationToken.None, data).ConfigureAwait(false);
+ }
+
+ public async Task EmitAsync(string eventName, CancellationToken cancellationToken, params object[] data)
+ {
+ if (data != null && data.Length > 0)
+ {
+ var result = JsonSerializer.Serialize(data);
+ if (result.Bytes.Count > 0)
+ {
+ var msg = new BinaryMessage
+ {
+ Namespace = _namespace,
+ OutgoingBytes = new List(result.Bytes),
+ Event = eventName,
+ Json = result.Json
+ };
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ var msg = new EventMessage
+ {
+ Namespace = _namespace,
+ Event = eventName,
+ Json = result.Json
+ };
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ var msg = new EventMessage
+ {
+ Namespace = _namespace,
+ Event = eventName
+ };
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Emits an event to the socket
+ ///
+ ///
+ /// will be called with the server answer.
+ /// Any other parameters can be included. All serializable datastructures are supported, including byte[]
+ ///
+ public async Task EmitAsync(string eventName, Action ack, params object[] data)
+ {
+ await EmitAsync(eventName, CancellationToken.None, ack, data).ConfigureAwait(false);
+ }
+
+ public async Task EmitAsync(string eventName, CancellationToken cancellationToken, Action ack, params object[] data)
+ {
+ _ackHandlers.Add(++_packetId, ack);
+ if (data != null && data.Length > 0)
+ {
+ var result = JsonSerializer.Serialize(data);
+ if (result.Bytes.Count > 0)
+ {
+ var msg = new ClientBinaryAckMessage
+ {
+ Event = eventName,
+ Namespace = _namespace,
+ Json = result.Json,
+ Id = _packetId,
+ OutgoingBytes = new List(result.Bytes)
+ };
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ var msg = new ClientAckMessage
+ {
+ Event = eventName,
+ Namespace = _namespace,
+ Id = _packetId,
+ Json = result.Json
+ };
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ var msg = new ClientAckMessage
+ {
+ Event = eventName,
+ Namespace = _namespace,
+ Id = _packetId
+ };
+ await _transport.SendAsync(msg, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task InvokeDisconnect(string reason)
+ {
+ if (Connected)
+ {
+ Connected = false;
+ Id = null;
+ OnDisconnected.TryInvoke(this, reason);
+ try
+ {
+ await _transport.DisconnectAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, e.Message);
+ }
+ if (reason != DisconnectReason.IOServerDisconnect && reason != DisconnectReason.IOClientDisconnect)
+ {
+ //In the this cases (explicit disconnection), the client will not try to reconnect and you need to manually call socket.connect().
+ if (Options.Reconnection)
+ {
+ ConnectCore();
+ }
+ }
+ }
+ }
+
+ public void AddExpectedException(Type type)
+ {
+ if (!_expectedExceptions.Contains(type))
+ {
+ _expectedExceptions.Add(type);
+ }
+ }
+
+ private void DisposeForReconnect()
+ {
+ _hasError = false;
+ _isFaild = false;
+ _packetId = -1;
+ _ackHandlers.Clear();
+ _connectCoreException = null;
+ _hasError = false;
+ _connectionTokenSource.TryCancel();
+ _connectionTokenSource.TryDispose();
+ }
+
+ public void Dispose()
+ {
+ HttpClient.Dispose();
+ _transport.TryDispose();
+ _ackHandlers.Clear();
+ _onAnyHandlers.Clear();
+ _eventHandlers.Clear();
+ _connectionTokenSource.TryCancel();
+ _connectionTokenSource.TryDispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/ElectronNET.API/SocketIO/SocketIOOptions.cs b/ElectronNET.API/SocketIO/SocketIOOptions.cs
new file mode 100644
index 00000000..dc154bb9
--- /dev/null
+++ b/ElectronNET.API/SocketIO/SocketIOOptions.cs
@@ -0,0 +1,65 @@
+using SocketIOClient.Transport;
+using System;
+using System.Collections.Generic;
+
+namespace SocketIOClient
+{
+ public sealed class SocketIOOptions
+ {
+ public SocketIOOptions()
+ {
+ RandomizationFactor = 0.5;
+ ReconnectionDelay = 1000;
+ ReconnectionDelayMax = 5000;
+ ReconnectionAttempts = int.MaxValue;
+ Path = "/socket.io";
+ ConnectionTimeout = TimeSpan.FromSeconds(20);
+ Reconnection = true;
+ Transport = TransportProtocol.Polling;
+ EIO = 4;
+ AutoUpgrade = true;
+ }
+
+ public string Path { get; set; }
+
+ public TimeSpan ConnectionTimeout { get; set; }
+
+ public IEnumerable> Query { get; set; }
+
+ ///
+ /// Whether to allow reconnection if accidentally disconnected
+ ///
+ public bool Reconnection { get; set; }
+
+ public double ReconnectionDelay { get; set; }
+ public int ReconnectionDelayMax { get; set; }
+ public int ReconnectionAttempts { get; set; }
+
+ double _randomizationFactor;
+ public double RandomizationFactor
+ {
+ get => _randomizationFactor;
+ set
+ {
+ if (value >= 0 && value <= 1)
+ {
+ _randomizationFactor = value;
+ }
+ else
+ {
+ throw new ArgumentException($"{nameof(RandomizationFactor)} should be greater than or equal to 0.0, and less than 1.0.");
+ }
+ }
+ }
+
+ public Dictionary ExtraHeaders { get; set; }
+
+ public TransportProtocol Transport { get; set; }
+
+ public int EIO { get; set; }
+
+ public bool AutoUpgrade { get; set; }
+
+ public object Auth { get; set; }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/SocketIOResponse.cs b/ElectronNET.API/SocketIO/SocketIOResponse.cs
new file mode 100644
index 00000000..42d979c8
--- /dev/null
+++ b/ElectronNET.API/SocketIO/SocketIOResponse.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SocketIOClient
+{
+ public class SocketIOResponse
+ {
+ public SocketIOResponse(IList array, SocketIO socket)
+ {
+ _array = array;
+ InComingBytes = new List();
+ SocketIO = socket;
+ PacketId = -1;
+ }
+
+ readonly IList _array;
+
+ public List InComingBytes { get; }
+ public SocketIO SocketIO { get; }
+ public int PacketId { get; set; }
+
+ public T GetValue(int index = 0)
+ {
+ var element = GetValue(index);
+ string json = element.GetRawText();
+ return SocketIO.JsonSerializer.Deserialize(json, InComingBytes);
+ }
+
+ public JsonElement GetValue(int index = 0) => _array[index];
+
+ public int Count => _array.Count;
+
+ public override string ToString()
+ {
+ var builder = new StringBuilder();
+ builder.Append('[');
+ foreach (var item in _array)
+ {
+ builder.Append(item.GetRawText());
+ if (_array.IndexOf(item) < _array.Count - 1)
+ {
+ builder.Append(',');
+ }
+ }
+ builder.Append(']');
+ return builder.ToString();
+ }
+
+ public async Task CallbackAsync(params object[] data)
+ {
+ await SocketIO.ClientAckAsync(PacketId, CancellationToken.None, data).ConfigureAwait(false);
+ }
+
+ public async Task CallbackAsync(CancellationToken cancellationToken, params object[] data)
+ {
+ await SocketIO.ClientAckAsync(PacketId, cancellationToken, data).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/ElectronNET.API/SocketIO/Transport/BaseTransport.cs b/ElectronNET.API/SocketIO/Transport/BaseTransport.cs
new file mode 100644
index 00000000..efbdc644
--- /dev/null
+++ b/ElectronNET.API/SocketIO/Transport/BaseTransport.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Reactive.Subjects;
+using Microsoft.Extensions.Logging;
+using SocketIOClient.JsonSerializer;
+using SocketIOClient.Messages;
+using SocketIOClient.UriConverters;
+
+namespace SocketIOClient.Transport
+{
+ public abstract class BaseTransport : IObserver, IObserver, IObservable, IDisposable
+ {
+ public BaseTransport(SocketIOOptions options, IJsonSerializer jsonSerializer, ILogger logger)
+ {
+ Options = options;
+ MessageSubject = new Subject();
+ JsonSerializer = jsonSerializer;
+ UriConverter = new UriConverter();
+ _messageQueue = new Queue