mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-02-13 21:24:00 +00:00
Merge of theolivenbaum PR
This commit is contained in:
@@ -8,7 +8,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Extensions;
|
||||
using ElectronNET.API.Interfaces;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
@@ -21,6 +25,90 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
public static bool SocketDebug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public static event Action OnSocketConnectFail;
|
||||
|
||||
internal static bool TryRaiseOnSocketConnectFail()
|
||||
{
|
||||
if (OnSocketConnectFail is object)
|
||||
{
|
||||
OnSocketConnectFail();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when the user clicks on the dock on Mac
|
||||
/// <para/>
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Emitted on the first instance when the user opens a second instance of the app, and the app is single instance
|
||||
/// <para/>
|
||||
/// </summary>
|
||||
public event Action<string[]> ActivateFromSecondInstance
|
||||
{
|
||||
add
|
||||
{
|
||||
if (_appActivateFromSecondInstance == null)
|
||||
{
|
||||
BridgeConnector.On<string[]>("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<string[]> _appActivateFromSecondInstance;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when all windows have been closed.
|
||||
/// <para/>
|
||||
@@ -338,6 +426,8 @@ namespace ElectronNET.API
|
||||
/// screen readers, are enabled or disabled. See https://www.chromium.org/developers/design-documents/accessibility for more details.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when Chrome's accessibility support is enabled, <see langword="false"/> otherwise.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action<bool> AccessibilitySupportChanged
|
||||
{
|
||||
add
|
||||
@@ -412,6 +502,7 @@ namespace ElectronNET.API
|
||||
/// <para/>
|
||||
/// On Windows, you have to parse the arguments using App.CommandLine to get the filepath.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action<string> OpenFile
|
||||
{
|
||||
add
|
||||
@@ -440,8 +531,7 @@ namespace ElectronNET.API
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public event Action<string> OpenUrl
|
||||
{
|
||||
@@ -493,12 +583,16 @@ namespace ElectronNET.API
|
||||
/// should usually also specify a productName field, which is your application's full capitalized name, and
|
||||
/// which will be preferred over name by Electron.
|
||||
/// </summary>
|
||||
public Task<string> GetNameAsync() => BridgeConnector.OnResult<string>("appGetName", "appGetNameCompleted");
|
||||
public Task<string> GetNameAsync => BridgeConnector.OnResult<string>("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
|
||||
@@ -529,7 +623,7 @@ namespace ElectronNET.API
|
||||
}
|
||||
|
||||
private static App _app;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Try to close all windows. The <see cref="BeforeQuit"/> event will be emitted first. If all windows are successfully
|
||||
@@ -600,6 +694,7 @@ namespace ElectronNET.API
|
||||
/// <para/>
|
||||
/// You should seek to use the <see cref="FocusOptions.Steal"/> option as sparingly as possible.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void Focus(FocusOptions focusOptions)
|
||||
{
|
||||
BridgeConnector.Emit("appFocus", JObject.FromObject(focusOptions, _jsonSerializer));
|
||||
@@ -608,6 +703,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Hides all application windows without minimizing them.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void Hide()
|
||||
{
|
||||
BridgeConnector.Emit("appHide");
|
||||
@@ -616,6 +712,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Shows application windows after they were hidden. Does not automatically focus them.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void Show()
|
||||
{
|
||||
BridgeConnector.Emit("appShow");
|
||||
@@ -689,6 +786,8 @@ namespace ElectronNET.API
|
||||
/// list from the task bar, and on macOS you can visit it from dock menu.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to add.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void AddRecentDocument(string path)
|
||||
{
|
||||
BridgeConnector.Emit("appAddRecentDocument", path);
|
||||
@@ -697,6 +796,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Clears the recent documents list.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void ClearRecentDocuments()
|
||||
{
|
||||
BridgeConnector.Emit("appClearRecentDocuments");
|
||||
@@ -727,6 +828,8 @@ namespace ElectronNET.API
|
||||
/// call this method with electron as the parameter.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public async Task<bool> SetAsDefaultProtocolClientAsync(string protocol, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SetAsDefaultProtocolClientAsync(protocol, null, null, cancellationToken);
|
||||
@@ -758,6 +861,8 @@ namespace ElectronNET.API
|
||||
/// <param name="path">The path to the Electron executable. Defaults to process.execPath</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public async Task<bool> SetAsDefaultProtocolClientAsync(string protocol, string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SetAsDefaultProtocolClientAsync(protocol, path, null, cancellationToken);
|
||||
@@ -790,6 +895,8 @@ namespace ElectronNET.API
|
||||
/// <param name="args">Arguments passed to the executable. Defaults to an empty array.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<bool> SetAsDefaultProtocolClientAsync(string protocol, string path, string[] args, CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appSetAsDefaultProtocolClient", "appSetAsDefaultProtocolClientCompleted", cancellationToken, protocol, path, args);
|
||||
|
||||
/// <summary>
|
||||
@@ -799,6 +906,8 @@ namespace ElectronNET.API
|
||||
/// <param name="protocol">The name of your protocol, without ://.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public async Task<bool> RemoveAsDefaultProtocolClientAsync(string protocol, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await RemoveAsDefaultProtocolClientAsync(protocol, null, null, cancellationToken);
|
||||
@@ -812,6 +921,8 @@ namespace ElectronNET.API
|
||||
/// <param name="path">Defaults to process.execPath.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public async Task<bool> RemoveAsDefaultProtocolClientAsync(string protocol, string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await RemoveAsDefaultProtocolClientAsync(protocol, path, null, cancellationToken);
|
||||
@@ -826,6 +937,8 @@ namespace ElectronNET.API
|
||||
/// <param name="args">Defaults to an empty array.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<bool> RemoveAsDefaultProtocolClientAsync(string protocol, string path, string[] args, CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appRemoveAsDefaultProtocolClient", "appRemoveAsDefaultProtocolClientCompleted", cancellationToken, protocol, path, args);
|
||||
|
||||
|
||||
@@ -842,6 +955,8 @@ namespace ElectronNET.API
|
||||
/// <param name="protocol">The name of your protocol, without ://.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the current executable is the default handler for a protocol (aka URI scheme).</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public async Task<bool> IsDefaultProtocolClientAsync(string protocol, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await IsDefaultProtocolClientAsync(protocol, null, null, cancellationToken);
|
||||
@@ -861,6 +976,8 @@ namespace ElectronNET.API
|
||||
/// <param name="path">Defaults to process.execPath.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the current executable is the default handler for a protocol (aka URI scheme).</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public async Task<bool> IsDefaultProtocolClientAsync(string protocol, string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await IsDefaultProtocolClientAsync(protocol, path, null, cancellationToken);
|
||||
@@ -881,6 +998,8 @@ namespace ElectronNET.API
|
||||
/// <param name="args">Defaults to an empty array.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the current executable is the default handler for a protocol (aka URI scheme).</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<bool> IsDefaultProtocolClientAsync(string protocol, string path, string[] args, CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appIsDefaultProtocolClient", "appIsDefaultProtocolClientCompleted", cancellationToken, protocol, path, args);
|
||||
|
||||
|
||||
@@ -892,6 +1011,7 @@ namespace ElectronNET.API
|
||||
/// <param name="userTasks">Array of <see cref="UserTask"/> objects.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<bool> SetUserTasksAsync(UserTask[] userTasks, CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appSetUserTasks", "appSetUserTasksCompleted", cancellationToken, JArray.FromObject(userTasks, _jsonSerializer));
|
||||
|
||||
/// <summary>
|
||||
@@ -899,6 +1019,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Jump List settings.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<JumpListSettings> GetJumpListSettingsAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult<JumpListSettings>("appGetJumpListSettings", "appGetJumpListSettingsCompleted", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@@ -917,6 +1038,7 @@ namespace ElectronNET.API
|
||||
/// omitted from the Jump List. The list of removed items can be obtained using <see cref="GetJumpListSettingsAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="categories">Array of <see cref="JumpListCategory"/> objects.</param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void SetJumpList(JumpListCategory[] categories)
|
||||
{
|
||||
BridgeConnector.Emit("appSetJumpList", JArray.FromObject(categories, _jsonSerializer));
|
||||
@@ -992,6 +1114,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="type">Uniquely identifies the activity. Maps to <see href="https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSUserActivity_Class/index.html#//apple_ref/occ/instp/NSUserActivity/activityType">NSUserActivity.activityType</see>.</param>
|
||||
/// <param name="userInfo">App-specific state to store for use by another device.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetUserActivity(string type, object userInfo)
|
||||
{
|
||||
SetUserActivity(type, userInfo, null);
|
||||
@@ -1009,6 +1132,7 @@ namespace ElectronNET.API
|
||||
/// <param name="webpageUrl">
|
||||
/// The webpage to load in a browser if no suitable app is installed on the resuming device. The scheme must be http or https.
|
||||
/// </param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetUserActivity(string type, object userInfo, string webpageUrl)
|
||||
{
|
||||
BridgeConnector.Emit("appSetUserActivity", type, userInfo, webpageUrl);
|
||||
@@ -1018,12 +1142,14 @@ namespace ElectronNET.API
|
||||
/// The type of the currently running activity.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<string> GetCurrentActivityTypeAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult<string>("appGetCurrentActivityType", "appGetCurrentActivityTypeCompleted", cancellationToken);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the current <see href="https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/Handoff/HandoffFundamentals/HandoffFundamentals.html">Handoff</see> user activity.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void InvalidateCurrentActivity()
|
||||
{
|
||||
BridgeConnector.Emit("appInvalidateCurrentActivity");
|
||||
@@ -1032,6 +1158,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Marks the current <see href="https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/Handoff/HandoffFundamentals/HandoffFundamentals.html">Handoff</see> user activity as inactive without invalidating it.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void ResignCurrentActivity()
|
||||
{
|
||||
BridgeConnector.Emit("appResignCurrentActivity");
|
||||
@@ -1041,6 +1168,7 @@ namespace ElectronNET.API
|
||||
/// Changes the <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx">Application User Model ID</see> to id.
|
||||
/// </summary>
|
||||
/// <param name="id">Model Id.</param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void SetAppUserModelId(string id)
|
||||
{
|
||||
BridgeConnector.Emit("appSetAppUserModelId", id);
|
||||
@@ -1055,6 +1183,7 @@ namespace ElectronNET.API
|
||||
/// <param name="options"></param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Result of import. Value of 0 indicates success.</returns>
|
||||
[SupportedOSPlatform("linux")]
|
||||
public Task<int> ImportCertificateAsync(ImportCertificateOptions options, CancellationToken cancellationToken = default) => BridgeConnector.OnResult<int>("appImportCertificate", "appImportCertificateCompleted", cancellationToken, JObject.FromObject(options, _jsonSerializer));
|
||||
|
||||
/// <summary>
|
||||
@@ -1085,12 +1214,16 @@ namespace ElectronNET.API
|
||||
/// <param name="count">Counter badge.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Whether the call succeeded.</returns>
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> SetBadgeCountAsync(int count, CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appSetBadgeCount", "appSetBadgeCountCompleted", cancellationToken, count);
|
||||
|
||||
/// <summary>
|
||||
/// The current value displayed in the counter badge.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<int> GetBadgeCountAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult<int>("appGetBadgeCount", "appGetBadgeCountCompleted", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@@ -1102,12 +1235,15 @@ namespace ElectronNET.API
|
||||
/// Whether the current desktop environment is Unity launcher.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
[SupportedOSPlatform("linux")]
|
||||
public Task<bool> IsUnityRunningAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appIsUnityRunning", "appIsUnityRunningCompleted", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// If you provided path and args options to <see cref="SetLoginItemSettings"/> then you need to pass the same
|
||||
/// arguments here for <see cref="LoginItemSettings.OpenAtLogin"/> to be set correctly.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public async Task<LoginItemSettings> GetLoginItemSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetLoginItemSettingsAsync(null, cancellationToken);
|
||||
@@ -1119,6 +1255,8 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<LoginItemSettings> GetLoginItemSettingsAsync(LoginItemSettingsOptions options, CancellationToken cancellationToken = default) =>
|
||||
options is null ? BridgeConnector.OnResult<LoginItemSettings>("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken)
|
||||
: BridgeConnector.OnResult<LoginItemSettings>("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken, JObject.FromObject(options, _jsonSerializer));
|
||||
@@ -1129,6 +1267,8 @@ namespace ElectronNET.API
|
||||
/// you'll want to set the launch path to Update.exe, and pass arguments that specify your application name.
|
||||
/// </summary>
|
||||
/// <param name="loginSettings"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetLoginItemSettings(LoginSettings loginSettings)
|
||||
{
|
||||
BridgeConnector.Emit("appSetLoginItemSettings", JObject.FromObject(loginSettings, _jsonSerializer));
|
||||
@@ -1140,6 +1280,8 @@ namespace ElectronNET.API
|
||||
/// See <see href="chromium.org/developers/design-documents/accessibility">Chromium's accessibility docs</see> for more details.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if Chrome’s accessibility support is enabled, <see langword="false"/> otherwise.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> IsAccessibilitySupportEnabledAsync(CancellationToken cancellationToken = default) => BridgeConnector.OnResult<bool>("appIsAccessibilitySupportEnabled", "appIsAccessibilitySupportEnabledCompleted", cancellationToken);
|
||||
|
||||
|
||||
@@ -1153,6 +1295,8 @@ namespace ElectronNET.API
|
||||
/// Note: Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default.
|
||||
/// </summary>
|
||||
/// <param name="enabled">Enable or disable <see href="https://developers.google.com/web/fundamentals/accessibility/semantics-builtin/the-accessibility-tree">accessibility tree</see> rendering.</param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetAccessibilitySupportEnabled(bool enabled)
|
||||
{
|
||||
BridgeConnector.Emit("appSetAboutPanelOptions", enabled);
|
||||
@@ -1183,6 +1327,13 @@ namespace ElectronNET.API
|
||||
BridgeConnector.Emit("appSetAboutPanelOptions", JObject.FromObject(options, _jsonSerializer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a path's associated icon.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public Task<NativeImage> GetFileIcon(string path) => BridgeConnector.OnResult<NativeImage>("appGetFileIcon", "appGetFileIconCompleted", path);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="string"/> which is the user agent string Electron will use as a global fallback.
|
||||
/// <para/>
|
||||
@@ -1207,7 +1358,7 @@ namespace ElectronNET.API
|
||||
/// custom value as early as possible in your app's initialization to ensure that your overridden value
|
||||
/// is used.
|
||||
/// </summary>
|
||||
public Task<string> GetUserAgentFallbackAsync() => BridgeConnector.OnResult<string>("appGetUserAgentFallback", "appGetUserAgentFallbackCompleted");
|
||||
public Task<string> GetUserAgentFallbackAsync => BridgeConnector.OnResult<string>("appGetUserAgentFallback", "appGetUserAgentFallbackCompleted");
|
||||
|
||||
internal void PreventQuit()
|
||||
{
|
||||
@@ -1245,7 +1396,14 @@ namespace ElectronNET.API
|
||||
/// <param name="fn">The handler</param>
|
||||
public void Once(string eventName, Action<object> fn) => Events.Instance.Once(ModuleName, eventName, fn);
|
||||
|
||||
private readonly JsonSerializer _jsonSerializer = new JsonSerializer()
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void DestroySplashScreen() => BridgeConnector.Emit("splashscreen-destroy");
|
||||
|
||||
private readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using ElectronNET.API.Interfaces;
|
||||
using Quobject.SocketIoClientDotNet.Client;
|
||||
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper for the underlying Socket connection
|
||||
/// </summary>
|
||||
public class ApplicationSocket : IApplicationSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket used to communicate with main.js
|
||||
/// </summary>
|
||||
public Socket Socket { get; internal set; }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Interfaces;
|
||||
using ElectronNET.API;
|
||||
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
@@ -17,14 +18,14 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Whether to automatically download an update when it is found. (Default is true)
|
||||
/// </summary>
|
||||
public Task<bool> IsAutoDownloadEnabledAsync() => BridgeConnector.OnResult<bool>("autoUpdater-autoDownload-get", "autoUpdater-autoDownload-get-reply");
|
||||
public Task<bool> IsAutoDownloadEnabledAsync => BridgeConnector.OnResult<bool>("autoUpdater-autoDownload-get", "autoUpdater-autoDownload-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically install a downloaded update on app quit (if `QuitAndInstall` was not called before).
|
||||
///
|
||||
/// Applicable only on Windows and Linux.
|
||||
/// </summary>
|
||||
public Task<bool> IsAutoInstallOnAppQuitEnabledAsync() => BridgeConnector.OnResult<bool>("autoUpdater-autoInstallOnAppQuit-get", "autoUpdater-autoInstallOnAppQuit-get-reply");
|
||||
public Task<bool> IsAutoInstallOnAppQuitEnabledAsync => BridgeConnector.OnResult<bool>("autoUpdater-autoInstallOnAppQuit-get", "autoUpdater-autoInstallOnAppQuit-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// *GitHub provider only.* Whether to allow update to pre-release versions.
|
||||
@@ -32,17 +33,16 @@ namespace ElectronNET.API
|
||||
///
|
||||
/// If "true", downgrade will be allowed("allowDowngrade" will be set to "true").
|
||||
/// </summary>
|
||||
public Task<bool> IsAllowPrereleaseEnabledAsync() => BridgeConnector.OnResult<bool>("autoUpdater-allowPrerelease-get", "autoUpdater-allowPrerelease-get-reply");
|
||||
public Task<bool> IsAllowPrereleaseEnabledAsync => BridgeConnector.OnResult<bool>("autoUpdater-allowPrerelease-get", "autoUpdater-allowPrerelease-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// *GitHub provider only.*
|
||||
/// Get all release notes (from current version to latest), not just the latest (Default is false).
|
||||
/// </summary>
|
||||
public Task<bool> IsFullChangeLogEnabledAsync() => BridgeConnector.OnResult<bool>("autoUpdater-fullChangelog-get", "autoUpdater-fullChangelog-get-reply");
|
||||
public Task<bool> IsFullChangeLogEnabledAsync => BridgeConnector.OnResult<bool>("autoUpdater-fullChangelog-get", "autoUpdater-fullChangelog-get-reply");
|
||||
|
||||
public Task<bool> IsAllowDowngradeEnabledAsync() => BridgeConnector.OnResult<bool>("autoUpdater-allowDowngrade-get", "autoUpdater-allowDowngrade-get-reply");
|
||||
public Task<bool> IsAllowDowngradeEnabledAsync => BridgeConnector.OnResult<bool>("autoUpdater-allowDowngrade-get", "autoUpdater-allowDowngrade-get-reply");
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically download an update when it is found. (Default is true)
|
||||
/// </summary>
|
||||
@@ -109,23 +109,23 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// For test only.
|
||||
/// </summary>
|
||||
public Task<string> GetUpdateConfigPathAsync() => BridgeConnector.OnResult<string>("autoUpdater-updateConfigPath-get", "autoUpdater-updateConfigPath-get-reply");
|
||||
public Task<string> GetUpdateConfigPathAsync => BridgeConnector.OnResult<string>("autoUpdater-updateConfigPath-get", "autoUpdater-updateConfigPath-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// The current application version
|
||||
/// </summary>
|
||||
public Task<SemVer> GetCurrentVersionAsync() => BridgeConnector.OnResult<SemVer>("autoUpdater-updateConcurrentVersionfigPath-get", "autoUpdater-currentVersion-get-reply");
|
||||
public Task<SemVer> GetCurrentVersionAsync => BridgeConnector.OnResult<SemVer>("autoUpdater-updateConcurrentVersionfigPath-get", "autoUpdater-currentVersion-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// Get the update channel. Not applicable for GitHub.
|
||||
/// Doesn’t return channel from the update configuration, only if was previously set.
|
||||
/// </summary>
|
||||
public Task<string> GetChannelAsync() => BridgeConnector.OnResult<string>("autoUpdater-channel-get", "autoUpdater-channel-get-reply");
|
||||
public Task<string> GetChannelAsync => BridgeConnector.OnResult<string>("autoUpdater-channel-get", "autoUpdater-channel-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// The request headers.
|
||||
/// </summary>
|
||||
public Task<Dictionary<string, string>> GetRequestHeadersAsync() => BridgeConnector.OnResult<Dictionary<string, string>>("autoUpdater-requestHeaders-get", "autoUpdater-requestHeaders-get-reply");
|
||||
public Task<Dictionary<string, string>> GetRequestHeadersAsync => BridgeConnector.OnResult<Dictionary<string, string>>("autoUpdater-requestHeaders-get", "autoUpdater-requestHeaders-get-reply");
|
||||
|
||||
/// <summary>
|
||||
/// The request headers.
|
||||
@@ -314,7 +314,7 @@ namespace ElectronNET.API
|
||||
private event Action<UpdateInfo> _updateDownloaded;
|
||||
|
||||
private static AutoUpdater _autoUpdater;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal AutoUpdater() { }
|
||||
|
||||
@@ -427,6 +427,7 @@ namespace ElectronNET.API
|
||||
/// <param name="isForceRunAfter">Run the app after finish even on silent install. Not applicable for macOS. Ignored if `isSilent` is set to `false`.</param>
|
||||
public void QuitAndInstall(bool isSilent = false, bool isForceRunAfter = false)
|
||||
{
|
||||
BridgeConnector.EmitSync("prepare-for-update");
|
||||
BridgeConnector.EmitSync("autoUpdaterQuitAndInstall", isSilent, isForceRunAfter);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Nito.AsyncEx;
|
||||
using SocketIOClient;
|
||||
using SocketIOClient.JsonSerializer;
|
||||
using SocketIOClient.Newtonsoft.Json;
|
||||
|
||||
namespace ElectronNET.API
|
||||
@@ -46,7 +44,7 @@ namespace ElectronNET.API
|
||||
return true; //Was added, so we need to also register the socket events
|
||||
}
|
||||
|
||||
if(_eventKeys.TryGetValue(key, out var existingEventKey) && existingEventKey == eventKey)
|
||||
if (_eventKeys.TryGetValue(key, out var existingEventKey) && existingEventKey == eventKey)
|
||||
{
|
||||
waitThisFirstAndThenTryAgain = null;
|
||||
return false; //No need to register the socket events twice
|
||||
@@ -86,26 +84,63 @@ namespace ElectronNET.API
|
||||
|
||||
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<string, Action<SocketIOResponse>> _eventHandlers = new();
|
||||
|
||||
private static Task<SocketIO> _waitForConnection
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureSocketTaskIsCreated();
|
||||
return GetSocket();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<SocketIO> 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(async () =>
|
||||
Task.Run(() => EmitAsync(eventString, args));
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -117,30 +152,128 @@ namespace ElectronNET.API
|
||||
{
|
||||
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<T>(string eventString, Action<T> fn)
|
||||
{
|
||||
Socket.On(eventString, (o) => fn(o.GetValue<T>(0)));
|
||||
EnsureSocketTaskIsCreated();
|
||||
|
||||
_socketSemaphoreHandlers.Wait();
|
||||
try
|
||||
{
|
||||
if (_eventHandlers.ContainsKey(eventString))
|
||||
{
|
||||
_eventHandlers.Remove(eventString);
|
||||
}
|
||||
|
||||
_eventHandlers.Add(eventString, o =>
|
||||
{
|
||||
try
|
||||
{
|
||||
fn(o.GetValue<T>(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<T>(string eventString, Action<T> fn)
|
||||
@@ -160,7 +293,7 @@ namespace ElectronNET.API
|
||||
// this allow us to wait for previous events first before registering new ones
|
||||
{
|
||||
var hash = new HashCode();
|
||||
foreach(var obj in args)
|
||||
foreach (var obj in args)
|
||||
{
|
||||
hash.Add(obj);
|
||||
}
|
||||
@@ -196,7 +329,7 @@ namespace ElectronNET.API
|
||||
EventTasks<T>.DoneWith(completedEvent, eventKey, taskCompletionSource);
|
||||
});
|
||||
|
||||
Emit(triggerEvent, args);
|
||||
await EmitAsync(triggerEvent, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,166 +390,162 @@ namespace ElectronNET.API
|
||||
|
||||
return await taskCompletionSource.Task;
|
||||
}
|
||||
private static SocketIO Socket
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_socket is null)
|
||||
{
|
||||
if (HybridSupport.IsElectronActive)
|
||||
{
|
||||
|
||||
lock (_syncRoot)
|
||||
internal static void Log(string formatString, params object[] args)
|
||||
{
|
||||
if (Logger is object)
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new Exception("You must call Electron.ReadAuth() first thing on your main entry point.");
|
||||
}
|
||||
|
||||
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.ConnectAsync().Wait();
|
||||
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.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<App> Logger { private get; set; }
|
||||
internal static string AuthKey { get; set; } = null;
|
||||
|
||||
private class CamelCaseNewtonsoftJsonSerializer : NewtonsoftJsonSerializer
|
||||
{
|
||||
public CamelCaseNewtonsoftJsonSerializer(int eio) : base(eio)
|
||||
public CamelCaseNewtonsoftJsonSerializer() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public override JsonSerializerSettings CreateOptions()
|
||||
{
|
||||
return new JsonSerializerSettings()
|
||||
OptionsProvider = () => new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
}
|
||||
}
|
||||
public static async Task<T> GetValueOverSocketAsync<T>(string eventString, string eventCompletedString)
|
||||
{
|
||||
CancellationToken cancellationToken = new();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()))
|
||||
{
|
||||
BridgeConnector.Socket.On(eventCompletedString, (value) =>
|
||||
{
|
||||
BridgeConnector.Socket.Off(eventCompletedString);
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
Console.WriteLine($"ERROR: BridgeConnector (event: '{eventString}') returned null. Socket loop hang.");
|
||||
taskCompletionSource.SetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
taskCompletionSource.SetResult( new JValue(value).ToObject<T>() );
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"ERROR: BridgeConnector (event: '{eventString}') exception: {e.Message}. Socket loop hung.");
|
||||
}
|
||||
});
|
||||
|
||||
await BridgeConnector.Socket.EmitAsync(eventString);
|
||||
|
||||
return await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<T> GetObjectOverSocketAsync<T>(string eventString, string eventCompletedString)
|
||||
{
|
||||
CancellationToken cancellationToken = new();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()))
|
||||
{
|
||||
BridgeConnector.Socket.On(eventCompletedString, (value) =>
|
||||
{
|
||||
BridgeConnector.Socket.Off(eventCompletedString);
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
Console.WriteLine($"ERROR: BridgeConnector (event: '{eventString}') returned null. Socket loop hang.");
|
||||
taskCompletionSource.SetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
taskCompletionSource.SetResult( ((JObject)value).ToObject<T>() );
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"ERROR: BridgeConnector (event: '{eventString}') exception: {e.Message}. Socket loop hung.");
|
||||
}
|
||||
});
|
||||
|
||||
await BridgeConnector.Socket.EmitAsync(eventString);
|
||||
|
||||
return await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<T> GetArrayOverSocketAsync<T>(string eventString, string eventCompletedString)
|
||||
{
|
||||
CancellationToken cancellationToken = new();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()))
|
||||
{
|
||||
BridgeConnector.Socket.On(eventCompletedString, (value) =>
|
||||
{
|
||||
BridgeConnector.Socket.Off(eventCompletedString);
|
||||
if (value == null)
|
||||
{
|
||||
Console.WriteLine($"ERROR: BridgeConnector (event: '{eventString}') returned null. Socket loop hang.");
|
||||
taskCompletionSource.SetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
taskCompletionSource.SetResult(((JArray)value).ToObject<T>() );
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"ERROR: BridgeConnector (event: '{eventString}') exception: {e.Message}. Socket loop hung.");
|
||||
}
|
||||
});
|
||||
|
||||
await BridgeConnector.Socket.EmitAsync(eventString);
|
||||
|
||||
return await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
public Task<Rectangle> GetBoundsAsync() => BridgeConnector.OnResult<Rectangle>("browserView-getBounds", "browserView-getBounds-reply" + Id, Id);
|
||||
|
||||
/// <summary>
|
||||
/// Set the bounds of the current view inside the window
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetBounds(Rectangle value)
|
||||
{
|
||||
BridgeConnector.Emit("browserView-setBounds", Id, value);
|
||||
|
||||
@@ -7,8 +7,11 @@ using System;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
@@ -146,6 +149,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when window session is going to end due to force shutdown or machine restart or session log off.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnSessionEnd
|
||||
{
|
||||
add
|
||||
@@ -465,6 +469,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the window is being resized.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnResize
|
||||
{
|
||||
add
|
||||
@@ -496,6 +502,8 @@ namespace ElectronNET.API
|
||||
///
|
||||
/// Note: On macOS this event is just an alias of moved.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnMove
|
||||
{
|
||||
add
|
||||
@@ -523,8 +531,10 @@ namespace ElectronNET.API
|
||||
private event Action _move;
|
||||
|
||||
/// <summary>
|
||||
/// macOS: Emitted once when the window is moved to a new position.
|
||||
/// Emitted once when the window is moved to a new position.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnMoved
|
||||
{
|
||||
add
|
||||
@@ -676,6 +686,8 @@ namespace ElectronNET.API
|
||||
/// and the APPCOMMAND_ prefix is stripped off.e.g.APPCOMMAND_BROWSER_BACKWARD
|
||||
/// is emitted as browser-backward.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action<string> OnAppCommand
|
||||
{
|
||||
add
|
||||
@@ -705,6 +717,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when scroll wheel event phase has begun.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnScrollTouchBegin
|
||||
{
|
||||
add
|
||||
@@ -734,6 +747,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when scroll wheel event phase has ended.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnScrollTouchEnd
|
||||
{
|
||||
add
|
||||
@@ -763,6 +777,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when scroll wheel event phase filed upon reaching the edge of element.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnScrollTouchEdge
|
||||
{
|
||||
add
|
||||
@@ -792,6 +807,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted on 3-finger swipe. Possible directions are up, right, down, left.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action<string> OnSwipe
|
||||
{
|
||||
add
|
||||
@@ -821,6 +837,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the window opens a sheet.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnSheetBegin
|
||||
{
|
||||
add
|
||||
@@ -850,6 +867,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the window has closed a sheet.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnSheetEnd
|
||||
{
|
||||
add
|
||||
@@ -879,6 +897,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the native new tab button is clicked.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnNewWindowForTab
|
||||
{
|
||||
add
|
||||
@@ -1056,6 +1075,14 @@ namespace ElectronNET.API
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetFullScreen", Id, flag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the background color of the window
|
||||
/// </summary>
|
||||
public void SetBackgroundColor(string color)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetBackgroundColor", Id, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window is in fullscreen mode.
|
||||
@@ -1066,6 +1093,24 @@ namespace ElectronNET.API
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsFullScreen", "browserWindow-isFullScreen-completed" + Id, Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="aspectRatio">The aspect ratio to maintain for some portion of the content view.</param>
|
||||
public void SetAspectRatio(int aspectRatio)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetAspectRatio", Id, aspectRatio, new Size() { Height = 0, Width = 0 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="aspectRatio">The aspect ratio to maintain for some portion of the content view.</param>
|
||||
/// <param name="extraSize">The extra size not to be included while maintaining the aspect ratio.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetAspectRatio(int aspectRatio, Size extraSize)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetAspectRatio", Id, aspectRatio, extraSize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Uses Quick Look to preview a file at a given path.
|
||||
/// </summary>
|
||||
/// <param name="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.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void PreviewFile(string path)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowPreviewFile", Id, path);
|
||||
@@ -1104,6 +1154,7 @@ namespace ElectronNET.API
|
||||
/// file to open.</param>
|
||||
/// <param name="displayname">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.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void PreviewFile(string path, string displayname)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowPreviewFile", Id, path, displayname);
|
||||
@@ -1112,6 +1163,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Closes the currently open Quick Look panel.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void CloseFilePreview()
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowCloseFilePreview", Id);
|
||||
@@ -1131,6 +1183,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="bounds"></param>
|
||||
/// <param name="animate"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetBounds(Rectangle bounds, bool animate)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetBounds", Id, bounds, animate);
|
||||
@@ -1159,6 +1212,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="bounds"></param>
|
||||
/// <param name="animate"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetContentBounds(Rectangle bounds, bool animate)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetContentBounds", Id, bounds, animate);
|
||||
@@ -1189,6 +1243,7 @@ namespace ElectronNET.API
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
/// <param name="animate"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetSize(int width, int height, bool animate)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetSize", Id, width, height, animate);
|
||||
@@ -1219,6 +1274,7 @@ namespace ElectronNET.API
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
/// <param name="animate"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetContentSize(int width, int height, bool animate)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetContentSize", Id, width, height, animate);
|
||||
@@ -1293,6 +1349,8 @@ namespace ElectronNET.API
|
||||
/// Sets whether the window can be moved by user. On Linux does nothing.
|
||||
/// </summary>
|
||||
/// <param name="movable"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetMovable(bool movable)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetMovable", Id, movable);
|
||||
@@ -1300,10 +1358,10 @@ namespace ElectronNET.API
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window can be moved by user.
|
||||
///
|
||||
/// On Linux always returns true.
|
||||
/// </summary>
|
||||
/// <returns>On Linux always returns true.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> IsMovableAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsMovable", "browserWindow-isMovable-completed" + Id, Id);
|
||||
@@ -1313,6 +1371,8 @@ namespace ElectronNET.API
|
||||
/// Sets whether the window can be manually minimized by user. On Linux does nothing.
|
||||
/// </summary>
|
||||
/// <param name="minimizable"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetMinimizable(bool minimizable)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetMinimizable", Id, minimizable);
|
||||
@@ -1320,10 +1380,10 @@ namespace ElectronNET.API
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window can be manually minimized by user.
|
||||
///
|
||||
/// On Linux always returns true.
|
||||
/// </summary>
|
||||
/// <returns>On Linux always returns true.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> IsMinimizableAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsMinimizable", "browserWindow-isMinimizable-completed" + Id, Id);
|
||||
@@ -1333,6 +1393,8 @@ namespace ElectronNET.API
|
||||
/// Sets whether the window can be manually maximized by user. On Linux does nothing.
|
||||
/// </summary>
|
||||
/// <param name="maximizable"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetMaximizable(bool maximizable)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetMaximizable", Id, maximizable);
|
||||
@@ -1340,10 +1402,10 @@ namespace ElectronNET.API
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window can be manually maximized by user.
|
||||
///
|
||||
/// On Linux always returns true.
|
||||
/// </summary>
|
||||
/// <returns>On Linux always returns true.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> IsMaximizableAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsMaximizable", "browserWindow-isMaximizable-completed" + Id, Id);
|
||||
@@ -1371,6 +1433,8 @@ namespace ElectronNET.API
|
||||
/// Sets whether the window can be manually closed by user. On Linux does nothing.
|
||||
/// </summary>
|
||||
/// <param name="closable"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetClosable(bool closable)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetClosable", Id, closable);
|
||||
@@ -1378,10 +1442,10 @@ namespace ElectronNET.API
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window can be manually closed by user.
|
||||
///
|
||||
/// On Linux always returns true.
|
||||
/// </summary>
|
||||
/// <returns>On Linux always returns true.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> IsClosableAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsClosable", "browserWindow-isClosable-completed" + Id, Id);
|
||||
@@ -1407,6 +1471,8 @@ namespace ElectronNET.API
|
||||
/// <param name="level">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</param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetAlwaysOnTop(bool flag, OnTopLevel level)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetAlwaysOnTop", Id, flag, level.GetDescription());
|
||||
@@ -1423,6 +1489,7 @@ namespace ElectronNET.API
|
||||
/// See the macOS docs</param>
|
||||
/// <param name="relativeLevel">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.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetAlwaysOnTop(bool flag, OnTopLevel level, int relativeLevel)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetAlwaysOnTop", Id, flag, level.GetDescription(), relativeLevel);
|
||||
@@ -1456,7 +1523,7 @@ namespace ElectronNET.API
|
||||
// https://github.com/electron/electron/issues/4045
|
||||
if (isWindows10())
|
||||
{
|
||||
x = x - 7;
|
||||
x -= 7;
|
||||
}
|
||||
|
||||
BridgeConnector.Emit("browserWindowSetPosition", Id, x, y);
|
||||
@@ -1468,13 +1535,14 @@ namespace ElectronNET.API
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <param name="animate"></param>
|
||||
[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 @@ namespace ElectronNET.API
|
||||
|
||||
private bool isWindows10()
|
||||
{
|
||||
return RuntimeInformation.OSDescription.Contains("Windows 10");
|
||||
return OperatingSystem.IsWindowsVersionAtLeast(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1520,6 +1588,7 @@ namespace ElectronNET.API
|
||||
/// but you may want to display them beneath a HTML-rendered toolbar.
|
||||
/// </summary>
|
||||
/// <param name="offsetY"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetSheetOffset(float offsetY)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetSheetOffset", Id, offsetY);
|
||||
@@ -1532,6 +1601,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="offsetY"></param>
|
||||
/// <param name="offsetX"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetSheetOffset(float offsetY, float offsetX)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetSheetOffset", Id, offsetY, offsetX);
|
||||
@@ -1587,6 +1657,7 @@ namespace ElectronNET.API
|
||||
/// and the icon of the file will show in window’s title bar.
|
||||
/// </summary>
|
||||
/// <param name="filename"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetRepresentedFilename(string filename)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetRepresentedFilename", Id, filename);
|
||||
@@ -1596,6 +1667,7 @@ namespace ElectronNET.API
|
||||
/// The pathname of the file the window represents.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<string> GetRepresentedFilenameAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<string>("browserWindowGetRepresentedFilename", "browserWindow-getRepresentedFilename-completed" + Id, Id);
|
||||
@@ -1606,6 +1678,7 @@ namespace ElectronNET.API
|
||||
/// and the icon in title bar will become gray when set to true.
|
||||
/// </summary>
|
||||
/// <param name="edited"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetDocumentEdited(bool edited)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetDocumentEdited", Id, edited);
|
||||
@@ -1615,6 +1688,7 @@ namespace ElectronNET.API
|
||||
/// Whether the window’s document has been edited.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> IsDocumentEditedAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsDocumentEdited", "browserWindow-isDocumentEdited-completed" + Id, Id);
|
||||
@@ -1672,13 +1746,15 @@ namespace ElectronNET.API
|
||||
/// The menu items.
|
||||
/// </value>
|
||||
public IReadOnlyCollection<MenuItem> MenuItems { get { return _items.AsReadOnly(); } }
|
||||
private List<MenuItem> _items = new List<MenuItem>();
|
||||
private readonly List<MenuItem> _items = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the menu as the window’s menu bar,
|
||||
/// setting it to null will remove the menu bar.
|
||||
/// </summary>
|
||||
/// <param name="menuItems"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("linux")]
|
||||
public void SetMenu(MenuItem[] menuItems)
|
||||
{
|
||||
menuItems.AddMenuItemsId();
|
||||
@@ -1696,6 +1772,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Remove the window's menu bar.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("linux")]
|
||||
public void RemoveMenu()
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowRemoveMenu", Id);
|
||||
@@ -1729,6 +1807,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="progress"></param>
|
||||
/// <param name="progressBarOptions"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void SetProgressBar(double progress, ProgressBarOptions progressBarOptions)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetProgressBar", Id, progress, progressBarOptions);
|
||||
@@ -1738,6 +1817,8 @@ namespace ElectronNET.API
|
||||
/// Sets whether the window should have a shadow. On Windows and Linux does nothing.
|
||||
/// </summary>
|
||||
/// <param name="hasShadow"></param>
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetHasShadow(bool hasShadow)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetHasShadow", Id, hasShadow);
|
||||
@@ -1762,7 +1843,7 @@ namespace ElectronNET.API
|
||||
/// </value>
|
||||
public IReadOnlyCollection<ThumbarButton> ThumbarButtons { get { return _thumbarButtons.AsReadOnly(); } }
|
||||
|
||||
private List<ThumbarButton> _thumbarButtons = new List<ThumbarButton>();
|
||||
private readonly List<ThumbarButton> _thumbarButtons = new();
|
||||
|
||||
/// <summary>
|
||||
/// Add a thumbnail toolbar with a specified set of buttons to the thumbnail
|
||||
@@ -1776,6 +1857,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="thumbarButtons"></param>
|
||||
/// <returns>Whether the buttons were added successfully.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<bool> SetThumbarButtonsAsync(ThumbarButton[] thumbarButtons)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
@@ -1808,6 +1890,7 @@ namespace ElectronNET.API
|
||||
/// an empty region: {x: 0, y: 0, width: 0, height: 0}.
|
||||
/// </summary>
|
||||
/// <param name="rectangle"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void SetThumbnailClip(Rectangle rectangle)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetThumbnailClip", Id, rectangle);
|
||||
@@ -1817,6 +1900,7 @@ namespace ElectronNET.API
|
||||
/// Sets the toolTip that is displayed when hovering over the window thumbnail in the taskbar.
|
||||
/// </summary>
|
||||
/// <param name="tooltip"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void SetThumbnailToolTip(string tooltip)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetThumbnailToolTip", Id, tooltip);
|
||||
@@ -1829,14 +1913,29 @@ namespace ElectronNET.API
|
||||
/// If one of those properties is not set, then neither will be used.
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void SetAppDetails(AppDetailsOptions options)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetAppDetails", Id, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///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.
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
[SupportedOSPlatform("win")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetTitleBarOverlay(TitleBarOverlayConfig options)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetTitleBarOverlay", Id, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as webContents.showDefinitionForSelection().
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void ShowDefinitionForSelection()
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowShowDefinitionForSelection", Id);
|
||||
@@ -1868,6 +1967,8 @@ namespace ElectronNET.API
|
||||
/// users can still bring up the menu bar by pressing the single Alt key.
|
||||
/// </summary>
|
||||
/// <param name="visible"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("linux")]
|
||||
public void SetMenuBarVisibility(bool visible)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetMenuBarVisibility", Id, visible);
|
||||
@@ -1877,6 +1978,8 @@ namespace ElectronNET.API
|
||||
/// Whether the menu bar is visible.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("linux")]
|
||||
public Task<bool> IsMenuBarVisibleAsync()
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("browserWindowIsMenuBarVisible", "browserWindow-isMenuBarVisible-completed" + Id, Id);
|
||||
@@ -1923,6 +2026,8 @@ namespace ElectronNET.API
|
||||
/// On Windows it calls SetWindowDisplayAffinity with WDA_MONITOR.
|
||||
/// </summary>
|
||||
/// <param name="enable"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetContentProtection(bool enable)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetContentProtection", Id, enable);
|
||||
@@ -1932,6 +2037,8 @@ namespace ElectronNET.API
|
||||
/// Changes whether the window can be focused.
|
||||
/// </summary>
|
||||
/// <param name="focusable"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetFocusable(bool focusable)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetFocusable", Id, focusable);
|
||||
@@ -1971,6 +2078,7 @@ namespace ElectronNET.API
|
||||
/// Controls whether to hide cursor when typing.
|
||||
/// </summary>
|
||||
/// <param name="autoHide"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetAutoHideCursor(bool autoHide)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetAutoHideCursor", Id, autoHide);
|
||||
@@ -1983,11 +2091,25 @@ namespace ElectronNET.API
|
||||
/// <param name="type">Can be appearance-based, light, dark, titlebar, selection,
|
||||
/// menu, popover, sidebar, medium-light or ultra-dark.
|
||||
/// See the macOS documentation for more details.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetVibrancy(Vibrancy type)
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetVibrancy", Id, type.GetDescription());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vibrancy effect to the browser window.
|
||||
/// Passing null or an empty string will remove the vibrancy effect on the window.
|
||||
/// </summary>
|
||||
/// <param name="type">Can be appearance-based, light, dark, titlebar, selection,
|
||||
/// menu, popover, sidebar, medium-light or ultra-dark.
|
||||
/// See the macOS documentation for more details.</param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void ExcludeFromShownWindowsMenu()
|
||||
{
|
||||
BridgeConnector.Emit("browserWindowSetExcludedFromShownWindowsMenu", Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render and control web pages.
|
||||
/// </summary>
|
||||
@@ -2004,7 +2126,7 @@ namespace ElectronNET.API
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Interfaces;
|
||||
|
||||
@@ -13,7 +14,7 @@ namespace ElectronNET.API
|
||||
public sealed class Clipboard : IClipboard
|
||||
{
|
||||
private static Clipboard _clipboard;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Clipboard() { }
|
||||
|
||||
@@ -94,6 +95,8 @@ namespace ElectronNET.API
|
||||
/// be empty strings when the bookmark is unavailable.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<ReadBookmark> ReadBookmarkAsync() => BridgeConnector.OnResult<ReadBookmark>("clipboard-readBookmark", "clipboard-readBookmark-Completed");
|
||||
|
||||
/// <summary>
|
||||
@@ -106,6 +109,8 @@ namespace ElectronNET.API
|
||||
/// <param name="title"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="type"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void WriteBookmark(string title, string url, string type = "")
|
||||
{
|
||||
BridgeConnector.Emit("clipboard-writeBookmark", title, url, type);
|
||||
@@ -117,6 +122,7 @@ namespace ElectronNET.API
|
||||
/// find pasteboard whenever the application is activated.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<string> ReadFindTextAsync() => BridgeConnector.OnResult<string>("clipboard-readFindText", "clipboard-readFindText-Completed");
|
||||
|
||||
/// <summary>
|
||||
@@ -124,6 +130,7 @@ namespace ElectronNET.API
|
||||
/// synchronous IPC when called from the renderer process.
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void WriteFindText(string text)
|
||||
{
|
||||
BridgeConnector.Emit("clipboard-writeFindText", text);
|
||||
@@ -152,7 +159,7 @@ namespace ElectronNET.API
|
||||
/// <param name="type"></param>
|
||||
public void Write(Data data, string type = "")
|
||||
{
|
||||
BridgeConnector.Emit("clipboard-write", data, type);
|
||||
BridgeConnector.Emit("clipboard-write", JObject.FromObject(data, _jsonSerializer), type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -171,5 +178,12 @@ namespace ElectronNET.API
|
||||
{
|
||||
BridgeConnector.Emit("clipboard-writeImage", JsonConvert.SerializeObject(image), type);
|
||||
}
|
||||
|
||||
private static readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
public sealed class CommandLine
|
||||
{
|
||||
internal CommandLine() { }
|
||||
private CommandLine() { }
|
||||
|
||||
internal static CommandLine Instance
|
||||
{
|
||||
@@ -31,7 +31,7 @@ namespace ElectronNET.API
|
||||
|
||||
private static CommandLine _commandLine;
|
||||
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Append a switch (with optional value) to Chromium's command line.
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace ElectronNET.API
|
||||
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 @@ namespace ElectronNET.API
|
||||
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 @@ namespace ElectronNET.API
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
38
ElectronNET.API/DesktopCapturer.cs
Normal file
38
ElectronNET.API/DesktopCapturer.cs
Normal file
@@ -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<DesktopCapturerSource[]> GetSourcesAsync(SourcesOption option)
|
||||
{
|
||||
return await BridgeConnector.OnResult<DesktopCapturerSource[]>("desktop-capturer-get-sources", "desktop-capturer-get-sources-result", option);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using ElectronNET.API.Interfaces;
|
||||
@@ -16,7 +17,7 @@ namespace ElectronNET.API
|
||||
public sealed class Dialog : IDialog
|
||||
{
|
||||
private static Dialog _dialog;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Dialog() { }
|
||||
|
||||
@@ -159,12 +160,12 @@ namespace ElectronNET.API
|
||||
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -192,6 +193,8 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task ShowCertificateTrustDialogAsync(CertificateTrustDialogOptions options)
|
||||
{
|
||||
return ShowCertificateTrustDialogAsync(null, options);
|
||||
@@ -205,6 +208,8 @@ namespace ElectronNET.API
|
||||
/// <param name="browserWindow"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task ShowCertificateTrustDialogAsync(BrowserWindow browserWindow, CertificateTrustDialogOptions options)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
@@ -220,5 +225,12 @@ namespace ElectronNET.API
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Entities;
|
||||
@@ -13,10 +14,11 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Control your app in the macOS dock.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public sealed class Dock : IDock
|
||||
{
|
||||
private static Dock _dock;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Dock()
|
||||
{
|
||||
@@ -131,7 +133,7 @@ namespace ElectronNET.API
|
||||
/// The menu items.
|
||||
/// </value>
|
||||
public IReadOnlyCollection<MenuItem> MenuItems { get { return _items.AsReadOnly(); } }
|
||||
private List<MenuItem> _items = new List<MenuItem>();
|
||||
private readonly List<MenuItem> _items = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application's dock menu.
|
||||
@@ -164,7 +166,7 @@ namespace ElectronNET.API
|
||||
BridgeConnector.Emit("dock-setIcon", image);
|
||||
}
|
||||
|
||||
private static readonly JsonSerializer _jsonSerializer = new JsonSerializer()
|
||||
private static readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
|
||||
144
ElectronNET.API/Electron.Experimental.cs
Normal file
144
ElectronNET.API/Electron.Experimental.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Experimental code, use with care
|
||||
/// </summary>
|
||||
public static class Experimental
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="webPort"></param>
|
||||
/// <param name="projectPath"></param>
|
||||
/// <param name="extraElectronArguments"></param>
|
||||
/// <param name="clearCache"></param>
|
||||
/// <exception cref="DirectoryNotFoundException"></exception>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a free local TCP port
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,51 @@
|
||||
namespace ElectronNET.API
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.Versioning;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
/// <summary>
|
||||
/// The Electron.NET API
|
||||
/// </summary>
|
||||
public static class Electron
|
||||
public static partial class Electron
|
||||
{
|
||||
private static ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Reads the auth key from the command line. This method must be called first thing.
|
||||
/// </summary>
|
||||
/// <exception cref="Exception"></exception>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logger factory to be used by Electron, if any
|
||||
/// </summary>
|
||||
public static ILoggerFactory LoggerFactory
|
||||
{
|
||||
private get => loggerFactory; set
|
||||
{
|
||||
loggerFactory = value;
|
||||
BridgeConnector.Logger = value.CreateLogger<App>();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Communicate asynchronously from the main process to renderer processes.
|
||||
/// </summary>
|
||||
@@ -60,6 +101,11 @@
|
||||
/// </summary>
|
||||
public static Screen Screen { get { return Screen.Instance; } }
|
||||
|
||||
/// <summary>
|
||||
/// Access information about media sources that can be used to capture audio and video from the desktop using the navigator.mediaDevices.getUserMedia API.
|
||||
/// </summary>
|
||||
public static DesktopCapturer DesktopCapturer { get { return DesktopCapturer.Instance; } }
|
||||
|
||||
/// <summary>
|
||||
/// Perform copy and paste operations on the system clipboard.
|
||||
/// </summary>
|
||||
@@ -87,11 +133,7 @@
|
||||
/// <summary>
|
||||
/// Control your app in the macOS dock.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public static Dock Dock { get { return Dock.Instance; } }
|
||||
|
||||
/// <summary>
|
||||
/// Electeon extensions to the Nodejs process object.
|
||||
/// </summary>
|
||||
public static Process Process { get { return Process.Instance; } }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageOutputPath>..\artifacts</PackageOutputPath>
|
||||
<PackageId>ElectronNET.API</PackageId>
|
||||
@@ -38,13 +39,16 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="SocketIOClient" Version="2.3.1" />
|
||||
<PackageReference Include="SocketIOClient.Newtonsoft.Json" Version="2.3.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageReference Include="SocketIOClient" Version="3.0.6" />
|
||||
<PackageReference Include="System.Collections" Version="4.3.0" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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 @@ namespace ElectronNET.API.Entities
|
||||
/// window's size will include window frame's size and be slightly larger. Default
|
||||
/// is false.
|
||||
/// </summary>
|
||||
public bool UseContentSize { get; set; }
|
||||
public bool? UseContentSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show window in the center of the screen.
|
||||
/// </summary>
|
||||
public bool Center { get; set; }
|
||||
public bool? Center { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Window's minimum width. Default is 0.
|
||||
/// </summary>
|
||||
public int MinWidth { get; set; }
|
||||
public int? MinWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Window's minimum height. Default is 0.
|
||||
/// </summary>
|
||||
public int MinHeight { get; set; }
|
||||
public int? MinHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Window's maximum width. Default is no limit.
|
||||
/// </summary>
|
||||
public int MaxWidth { get; set; }
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Window's maximum height. Default is no limit.
|
||||
/// </summary>
|
||||
public int MaxHeight { get; set; }
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether window is resizable. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Resizable { get; set; } = true;
|
||||
public bool? Resizable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether window is movable. This is not implemented on Linux. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Movable { get; set; } = true;
|
||||
public bool? Movable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether window is minimizable. This is not implemented on Linux. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Minimizable { get; set; } = true;
|
||||
public bool? Minimizable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether window is maximizable. This is not implemented on Linux. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Maximizable { get; set; } = true;
|
||||
public bool? Maximizable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether window is closable. This is not implemented on Linux. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Closable { get; set; } = true;
|
||||
public bool? Closable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window can be focused. Default is true. On Windows setting
|
||||
@@ -100,35 +101,35 @@ namespace ElectronNET.API.Entities
|
||||
/// always stay on top in all workspaces.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Focusable { get; set; } = true;
|
||||
public bool? Focusable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window should always stay on top of other windows. Default is false.
|
||||
/// </summary>
|
||||
public bool AlwaysOnTop { get; set; }
|
||||
public bool? AlwaysOnTop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool Fullscreen { get; set; }
|
||||
public bool? Fullscreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool Fullscreenable { get; set; }
|
||||
public bool? Fullscreenable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show the window in taskbar. Default is false.
|
||||
/// </summary>
|
||||
public bool SkipTaskbar { get; set; }
|
||||
public bool? SkipTaskbar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The kiosk mode. Default is false.
|
||||
/// </summary>
|
||||
public bool Kiosk { get; set; }
|
||||
public bool? Kiosk { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default window title. Default is "Electron.NET".
|
||||
@@ -145,40 +146,40 @@ namespace ElectronNET.API.Entities
|
||||
/// Whether window should be shown when created. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Show { get; set; } = true;
|
||||
public bool? Show { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Specify false to create a . Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Frame { get; set; } = true;
|
||||
public bool? Frame { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a modal window. This only works when <see cref="Parent"/> is
|
||||
/// also specified. Default is false.
|
||||
/// </summary>
|
||||
public bool Modal { get; set; }
|
||||
public bool? Modal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the web view accepts a single mouse-down event that simultaneously
|
||||
/// activates the window. Default is false.
|
||||
/// </summary>
|
||||
public bool AcceptFirstMouse { get; set; }
|
||||
public bool? AcceptFirstMouse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to hide cursor when typing. Default is false.
|
||||
/// </summary>
|
||||
public bool DisableAutoHideCursor { get; set; }
|
||||
public bool? DisableAutoHideCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Auto hide the menu bar unless the Alt key is pressed. Default is false.
|
||||
/// </summary>
|
||||
public bool AutoHideMenuBar { get; set; }
|
||||
public bool? AutoHideMenuBar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable the window to be resized larger than screen. Default is false.
|
||||
/// </summary>
|
||||
public bool EnableLargerThanScreen { get; set; }
|
||||
public bool? EnableLargerThanScreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Window's background color as Hexadecimal value, like #66CD00 or #FFF or
|
||||
@@ -190,18 +191,18 @@ namespace ElectronNET.API.Entities
|
||||
/// Whether window should have a shadow. This is only implemented on macOS. Default
|
||||
/// is true.
|
||||
/// </summary>
|
||||
public bool HasShadow { get; set; }
|
||||
public bool? HasShadow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces using dark theme for the window, only works on some GTK+3 desktop
|
||||
/// environments.Default is false.
|
||||
/// </summary>
|
||||
public bool DarkTheme { get; set; }
|
||||
public bool? DarkTheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Makes the window . Default is false.
|
||||
/// </summary>
|
||||
public bool Transparent { get; set; }
|
||||
public bool? Transparent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of window, default is normal window.
|
||||
@@ -213,13 +214,21 @@ namespace ElectronNET.API.Entities
|
||||
/// 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover'
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public TitleBarStyle TitleBarStyle { get; set; }
|
||||
public TitleBarStyle? TitleBarStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows the title in the tile bar in full screen mode on macOS for all
|
||||
/// titleBarStyle options.Default is false.
|
||||
/// </summary>
|
||||
public bool FullscreenWindowTitle { get; set; }
|
||||
public bool? FullscreenWindowTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Activate the Window Controls Overlay on Windows, when combined with <see cref="TitleBarStyle"/> = <see cref="TitleBarStyle.hidden"/>
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("win")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
[DefaultValue(null)]
|
||||
public TitleBarOverlayConfig TitleBarOverlay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Use WS_THICKFRAME style for frameless windows on Windows, which adds standard
|
||||
@@ -227,7 +236,7 @@ namespace ElectronNET.API.Entities
|
||||
/// animations. Default is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool ThickFrame { get; set; } = true;
|
||||
public bool? ThickFrame { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Add a type of vibrancy effect to the window, only on macOS. Can be
|
||||
@@ -235,7 +244,7 @@ namespace ElectronNET.API.Entities
|
||||
/// medium-light or ultra-dark.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public Vibrancy Vibrancy { get; set; }
|
||||
public Vibrancy? Vibrancy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls the behavior on macOS when option-clicking the green stoplight button
|
||||
@@ -244,7 +253,7 @@ namespace ElectronNET.API.Entities
|
||||
/// it to zoom to the width of the screen.This will also affect the behavior when
|
||||
/// calling maximize() directly.Default is false.
|
||||
/// </summary>
|
||||
public bool ZoomToPageWidth { get; set; }
|
||||
public bool? ZoomToPageWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tab group name, allows opening the window as a native tab on macOS 10.12+.
|
||||
@@ -276,5 +285,13 @@ namespace ElectronNET.API.Entities
|
||||
/// </summary>
|
||||
[DefaultValue(null)]
|
||||
public BrowserWindow Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a custom position for the traffic light buttons in frameless windows.
|
||||
/// </summary>
|
||||
|
||||
[DefaultValue(null)]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Point TrafficLightPosition { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,7 @@
|
||||
/// The title of the url at text.
|
||||
/// </summary>
|
||||
public string Bookmark { get; set; }
|
||||
|
||||
public NativeImage? Image { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
ElectronNET.API/Entities/DesktopCapturerSource.cs
Normal file
15
ElectronNET.API/Entities/DesktopCapturerSource.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,12 @@
|
||||
/// <summary>
|
||||
/// Can be 0, 90, 180, 270, represents screen rotation in clock-wise degrees.
|
||||
/// </summary>
|
||||
public int Rotation { get; set; }
|
||||
public float Rotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Output device's pixel scale factor.
|
||||
/// </summary>
|
||||
public int ScaleFactor { get; set; }
|
||||
public float ScaleFactor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace ElectronNET.API.Entities
|
||||
using System;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
public class JumpListSettings
|
||||
{
|
||||
@@ -13,6 +15,6 @@
|
||||
/// in the Jump List. These items must not be re-added to the Jump List in the next call to <see cref="App.SetJumpList"/>, Windows will
|
||||
/// not display any custom category that contains any of the removed items.
|
||||
/// </summary>
|
||||
public JumpListItem[] RemovedItems { get; set; } = new JumpListItem[0];
|
||||
public JumpListItem[] RemovedItems { get; set; } = Array.Empty<JumpListItem>();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -20,11 +23,11 @@ namespace ElectronNET.API.Entities
|
||||
///
|
||||
/// </summary>
|
||||
public const float DefaultScaleFactor = 1.0f;
|
||||
|
||||
private readonly Dictionary<float, Image> _images = new Dictionary<float, Image>();
|
||||
|
||||
private bool _isTemplateImage;
|
||||
|
||||
private static readonly Dictionary<string, float> ScaleFactorPairs = new Dictionary<string, float>
|
||||
private static readonly Dictionary<string, float> 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},
|
||||
@@ -41,8 +44,7 @@ namespace ElectronNET.API.Entities
|
||||
}
|
||||
private static Image BytesToImage(byte[] bytes)
|
||||
{
|
||||
var ms = new MemoryStream(bytes);
|
||||
return Image.Load(ms);
|
||||
return Image.Load(new MemoryStream(bytes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,11 +58,14 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Obsolete("System.Drawing.Common is no longer supported. Use NativeImage.CreateFromImage(image, options);", true)]
|
||||
public static NativeImage CreateFromBitmap(object bitmap, CreateOptions options = null)
|
||||
public static NativeImage CreateFromImage(Image image, CreateFromBitmapOptions options = null)
|
||||
{
|
||||
throw new NotImplementedException(
|
||||
"System.Drawing.Common is no longer supported. Use NativeImage.CreateFromImage(image, options);");
|
||||
if (options is null)
|
||||
{
|
||||
options = new CreateFromBitmapOptions();
|
||||
}
|
||||
|
||||
return new NativeImage(image, options.ScaleFactor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,8 +79,12 @@ namespace ElectronNET.API.Entities
|
||||
/// </summary>
|
||||
public static NativeImage CreateFromBuffer(byte[] buffer, CreateOptions options = null)
|
||||
{
|
||||
var ms = new MemoryStream(buffer);
|
||||
var image = Image.Load(ms);
|
||||
if (options is null)
|
||||
{
|
||||
options = new CreateFromBufferOptions();
|
||||
}
|
||||
|
||||
var image = Image.Load(new MemoryStream(buffer));
|
||||
|
||||
return new NativeImage(image, options?.ScaleFactor ?? DefaultScaleFactor);
|
||||
}
|
||||
@@ -167,7 +176,7 @@ namespace ElectronNET.API.Entities
|
||||
/// </summary>
|
||||
public NativeImage Crop(Rectangle rect)
|
||||
{
|
||||
var images = new Dictionary<float,Image>();
|
||||
var images = new Dictionary<float, Image>();
|
||||
foreach (var image in _images)
|
||||
{
|
||||
images.Add(image.Key, Crop(rect.X, rect.Y, rect.Width, rect.Height, image.Key));
|
||||
@@ -217,7 +226,7 @@ namespace ElectronNET.API.Entities
|
||||
var image = GetScale(scaleFactor);
|
||||
if (image != null)
|
||||
{
|
||||
return Convert.ToSingle(image.Width) / image.Height;
|
||||
return (float)image.Width / image.Height;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
@@ -226,9 +235,9 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
/// Returns a byte array that contains the image's raw bitmap pixel data.
|
||||
/// </summary>
|
||||
public byte[] GetBitmap(ImageOptions options)
|
||||
public byte[] GetBitmap(float scaleFactor)
|
||||
{
|
||||
return ToBitmap(options);
|
||||
return ToBitmap(scaleFactor).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,7 +245,7 @@ namespace ElectronNET.API.Entities
|
||||
/// </summary>
|
||||
public byte[] GetNativeHandle()
|
||||
{
|
||||
return ToBitmap(new ImageOptions());
|
||||
return ToBitmap().ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -269,75 +278,86 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
/// Outputs a bitmap based on the scale factor
|
||||
/// </summary>
|
||||
public byte[] ToBitmap(ImageOptions options)
|
||||
public MemoryStream ToBitmap(float scaleFactor = 1.0f)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
_images[options.ScaleFactor].SaveAsBmp(ms);
|
||||
return ms.ToArray();
|
||||
_images[scaleFactor].SaveAsBmp(ms);
|
||||
return ms;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a data URL based on the scale factor
|
||||
/// Outputs a PNG based on the scale factor
|
||||
/// </summary>
|
||||
public string ToDataURL(ImageOptions options)
|
||||
=> _images.TryGetValue(options.ScaleFactor, out var image)
|
||||
? $"data:image/png;base64,{image.ToBase64String(PngFormat.Instance)}"
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a JPEG for the default scale factor
|
||||
/// </summary>
|
||||
public byte[] ToJPEG(int quality)
|
||||
public MemoryStream ToPng(float scaleFactor = 1.0f)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
_images[1.0f].SaveAsJpeg(ms);
|
||||
return ms.ToArray();
|
||||
_images[scaleFactor].SaveAsPng(ms);
|
||||
return ms;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a PNG for the specified scale factor
|
||||
/// Outputs a JPEG for the default scale factor
|
||||
/// </summary>
|
||||
public byte[] ToPNG(ImageOptions options)
|
||||
public MemoryStream ToJpeg(int quality, float scaleFactor = 1.0f)
|
||||
{
|
||||
if (_images.TryGetValue(options.ScaleFactor, out var image))
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
image.SaveAsPng(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
return null;
|
||||
var ms = new MemoryStream();
|
||||
_images[scaleFactor].SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder() { Quality = quality });
|
||||
return ms;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a data URL based on the scale factor
|
||||
/// </summary>
|
||||
public string ToDataURL(float scaleFactor = 1.0f)
|
||||
{
|
||||
if (!_images.TryGetValue(scaleFactor, out var image))
|
||||
{
|
||||
throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}");
|
||||
}
|
||||
|
||||
return image.ToBase64String(PngFormat.Instance);
|
||||
}
|
||||
|
||||
|
||||
private Image Resize(int? width, int? height, float scaleFactor = 1.0f)
|
||||
{
|
||||
if (!_images.TryGetValue(scaleFactor, out var image) || (width is null && height is null))
|
||||
{
|
||||
return null;
|
||||
throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}");
|
||||
}
|
||||
|
||||
if (width is null && height is null)
|
||||
{
|
||||
throw new ArgumentNullException("Missing width or height");
|
||||
}
|
||||
|
||||
var aspect = GetAspectRatio(scaleFactor);
|
||||
|
||||
width ??= Convert.ToInt32(image.Width * aspect);
|
||||
width ??= Convert.ToInt32(image.Width * aspect);
|
||||
height ??= Convert.ToInt32(image.Height * aspect);
|
||||
|
||||
width = Convert.ToInt32(width * scaleFactor);
|
||||
height = Convert.ToInt32(height * scaleFactor);
|
||||
width = Convert.ToInt32(width * scaleFactor);
|
||||
height = Convert.ToInt32(height * scaleFactor);
|
||||
|
||||
return image.Clone(c => c.Resize(new SixLabors.ImageSharp.Processing.ResizeOptions
|
||||
{
|
||||
Size = new (width.Value, height.Value),
|
||||
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))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var image = _images[scaleFactor];
|
||||
|
||||
|
||||
if (!_images.TryGetValue(scaleFactor, out image))
|
||||
{
|
||||
throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}");
|
||||
}
|
||||
|
||||
x ??= 0;
|
||||
y ??= 0;
|
||||
|
||||
@@ -350,8 +370,7 @@ namespace ElectronNET.API.Entities
|
||||
width = Convert.ToInt32(width * scaleFactor);
|
||||
height = Convert.ToInt32(height * scaleFactor);
|
||||
|
||||
return image.Clone(c =>
|
||||
c.Crop(new SixLabors.ImageSharp.Rectangle(x.Value, y.Value, width.Value, height.Value)));
|
||||
return image.Clone(c => c.Crop(new SixLabors.ImageSharp.Rectangle(x.Value, y.Value, width.Value, height.Value)));
|
||||
}
|
||||
|
||||
internal Dictionary<float,string> GetAllScaledImages()
|
||||
@@ -366,7 +385,7 @@ namespace ElectronNET.API.Entities
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
BridgeConnector.LogError(ex, "Error getting scaled images");
|
||||
}
|
||||
|
||||
return dict;
|
||||
@@ -374,17 +393,12 @@ namespace ElectronNET.API.Entities
|
||||
|
||||
internal Image GetScale(float scaleFactor)
|
||||
{
|
||||
if (_images.ContainsKey(scaleFactor))
|
||||
if (_images.TryGetValue(scaleFactor, out var image))
|
||||
{
|
||||
return _images[scaleFactor];
|
||||
return image;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility conversion operator
|
||||
/// </summary>
|
||||
public static implicit operator NativeImage(Image src) => CreateFromImage(src);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using SixLabors.ImageSharp;
|
||||
@@ -19,12 +22,15 @@ namespace ElectronNET.API.Entities
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var dict = serializer.Deserialize<Dictionary<float, string>>(reader);
|
||||
var dict = serializer.Deserialize<Dictionary<string, string>>(reader);
|
||||
var newDictionary = new Dictionary<float, Image>();
|
||||
foreach (var item in dict)
|
||||
{
|
||||
var bytes = Convert.FromBase64String(item.Value);
|
||||
newDictionary.Add(item.Key, Image.Load(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);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace ElectronNET.API.Entities
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public class NotificationAction
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
@@ -17,6 +18,8 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
/// A subtitle for the notification, which will be displayed below the title.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
|
||||
public string SubTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -38,38 +41,46 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
/// Whether or not to add an inline reply option to the notification.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public bool HasReply { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timeout duration of the notification. Can be 'default' or 'never'.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("linux")]
|
||||
public string TimeoutType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The placeholder to write in the inline reply input field.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public string ReplyPlaceholder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the sound file to play when the notification is shown.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public string Sound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The urgency level of the notification. Can be 'normal', 'critical', or 'low'.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("linux")]
|
||||
public string Urgency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actions to add to the notification. Please read the available actions and
|
||||
/// limitations in the NotificationAction documentation.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public NotificationAction Actions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A custom title for the close button of an alert. An empty string will cause the
|
||||
/// default localized text to be used.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public string CloseButtonText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -127,6 +138,7 @@ namespace ElectronNET.API.Entities
|
||||
/// The string the user entered into the inline reply field
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Action<string> OnReply { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -142,6 +154,7 @@ namespace ElectronNET.API.Entities
|
||||
/// macOS only - The index of the action that was activated
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Action<string> OnAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
@@ -39,6 +40,7 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
/// Message to display above input boxes.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace ElectronNET.API.Entities
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -28,21 +30,66 @@
|
||||
/// <summary>
|
||||
/// The create directory
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
createDirectory,
|
||||
|
||||
/// <summary>
|
||||
/// The prompt to create
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
promptToCreate,
|
||||
|
||||
/// <summary>
|
||||
/// The no resolve aliases
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
noResolveAliases,
|
||||
|
||||
/// <summary>
|
||||
/// The treat package as directory
|
||||
/// Treat packages, such as .app folders, as a directory instead of a file.
|
||||
/// </summary>
|
||||
treatPackageAsDirectory
|
||||
[SupportedOSPlatform("macos")]
|
||||
treatPackageAsDirectory,
|
||||
|
||||
/// <summary>
|
||||
/// Don't add the item being opened to recent documents list
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
dontAddToRecent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public enum SaveDialogProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// The show hidden files
|
||||
/// </summary>
|
||||
showHiddenFiles,
|
||||
|
||||
/// <summary>
|
||||
/// The create directory
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
createDirectory,
|
||||
|
||||
/// <summary>
|
||||
/// Treat packages, such as .app folders, as a directory instead of a file.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
treatPackageAsDirectory,
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the user will be presented a confirmation dialog if the user types a file name that already exists.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("linux")]
|
||||
showOverwriteConfirmation,
|
||||
|
||||
/// <summary>
|
||||
/// Don't add the item being opened to recent documents list
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
dontAddToRecent
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
@@ -12,11 +13,13 @@ namespace ElectronNET.API.Entities
|
||||
/// <see langword="true"/> to bring the opened application to the foreground. The default is <see langword="true"/>.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public bool Activate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The working directory.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public string WorkingDirectory { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Message to display above text fields.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom label for the text displayed in front of the filename text field.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public string NameFieldLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show the tags input box, defaults to true.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public bool ShowsTagField { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains which features the dialog should use. The following values are supported:
|
||||
/// 'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory'
|
||||
/// </summary>
|
||||
[JsonProperty("properties", ItemConverterType = typeof(StringEnumConverter))]
|
||||
public SaveDialogProperty[] Properties { get; set; }
|
||||
}
|
||||
}
|
||||
9
ElectronNET.API/Entities/SourcesOption.cs
Normal file
9
ElectronNET.API/Entities/SourcesOption.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,11 @@ namespace ElectronNET.API.Entities
|
||||
/// </summary>
|
||||
customButtonsOnHover
|
||||
}
|
||||
|
||||
public class TitleBarOverlayConfig
|
||||
{
|
||||
public string color { get; set; }
|
||||
public string symbolColor { get; set; }
|
||||
public int height { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace ElectronNET.API.Entities
|
||||
using System;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -13,7 +15,7 @@
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public UpdateFileInfo[] Files { get; set; } = new UpdateFileInfo[0];
|
||||
public UpdateFileInfo[] Files { get; set; } = Array.Empty<UpdateFileInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// The release name.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization;
|
||||
using System;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
@@ -11,16 +12,19 @@ namespace ElectronNET.API.Entities
|
||||
/// The appearance based
|
||||
/// </summary>
|
||||
[EnumMember(Value = "appearance-based")]
|
||||
[Obsolete("Removed in macOS Catalina (10.15).")]
|
||||
appearanceBased,
|
||||
|
||||
/// <summary>
|
||||
/// The light
|
||||
/// </summary>
|
||||
[Obsolete("Removed in macOS Catalina (10.15).")]
|
||||
light,
|
||||
|
||||
/// <summary>
|
||||
/// The dark
|
||||
/// </summary>
|
||||
[Obsolete("Removed in macOS Catalina (10.15).")]
|
||||
dark,
|
||||
|
||||
/// <summary>
|
||||
@@ -52,12 +56,38 @@ namespace ElectronNET.API.Entities
|
||||
/// The medium light
|
||||
/// </summary>
|
||||
[EnumMember(Value = "medium-light")]
|
||||
[Obsolete("Removed in macOS Catalina (10.15).")]
|
||||
mediumLight,
|
||||
|
||||
/// <summary>
|
||||
/// The ultra dark
|
||||
/// </summary>
|
||||
[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
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,12 @@ namespace ElectronNET.API.Entities
|
||||
/// </summary>
|
||||
public bool Offscreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable built-in spellcheck
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
public bool Spellcheck { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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 @@ namespace ElectronNET.API.Entities
|
||||
[DefaultValue(false)]
|
||||
public bool ContextIsolation { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use native window.open(). Defaults to false. This option is currently experimental.
|
||||
/// </summary>
|
||||
public bool NativeWindowOpen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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 @@ namespace ElectronNET.API.Entities
|
||||
public bool WebviewTag { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable the remote module. Defaults to false.
|
||||
/// Make the web view transparent
|
||||
/// </summary>
|
||||
[DefaultValue(false)]
|
||||
public bool EnableRemoteModule { get; set; } = false;
|
||||
public bool Transparent { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ElectronNET.API.Extensions
|
||||
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
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace ElectronNET.API.Extensions
|
||||
|
||||
public static MenuItem GetMenuItem(this List<MenuItem> menuItems, string id)
|
||||
{
|
||||
MenuItem result = new MenuItem();
|
||||
MenuItem result = new();
|
||||
|
||||
foreach (var item in menuItems)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace ElectronNET.API.Extensions
|
||||
|
||||
public static ThumbarButton GetThumbarButton(this List<ThumbarButton> thumbarButtons, string id)
|
||||
{
|
||||
ThumbarButton result = new ThumbarButton("");
|
||||
ThumbarButton result = new("");
|
||||
|
||||
foreach (var item in thumbarButtons)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ElectronNET.API
|
||||
public sealed class GlobalShortcut : IGlobalShortcut
|
||||
{
|
||||
private static GlobalShortcut _globalShortcut;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal GlobalShortcut() { }
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace ElectronNET.API
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, Action> _shortcuts = new Dictionary<string, Action>();
|
||||
private readonly Dictionary<string, Action> _shortcuts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global shortcut of accelerator.
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace ElectronNET.API
|
||||
public sealed class HostHook : IHostHook
|
||||
{
|
||||
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() { }
|
||||
|
||||
|
||||
@@ -95,8 +95,6 @@ namespace ElectronNET.API.Interfaces
|
||||
/// </summary>
|
||||
string Name
|
||||
{
|
||||
[Obsolete("Use the asynchronous version NameAsync instead")]
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
@@ -108,7 +106,7 @@ namespace ElectronNET.API.Interfaces
|
||||
/// should usually also specify a productName field, which is your application's full capitalized name, and
|
||||
/// which will be preferred over name by Electron.
|
||||
/// </summary>
|
||||
Task<string> NameAsync { get; }
|
||||
Task<string> GetNameAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="CommandLine"/> object that allows you to read and manipulate the command line arguments that Chromium uses.
|
||||
@@ -125,8 +123,6 @@ namespace ElectronNET.API.Interfaces
|
||||
/// </summary>
|
||||
string UserAgentFallback
|
||||
{
|
||||
[Obsolete("Use the asynchronous version UserAgentFallbackAsync instead")]
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
@@ -138,7 +134,7 @@ namespace ElectronNET.API.Interfaces
|
||||
/// custom value as early as possible in your app's initialization to ensure that your overridden value
|
||||
/// is used.
|
||||
/// </summary>
|
||||
Task<string> UserAgentFallbackAsync { get; }
|
||||
Task<string> GetUserAgentFallbackAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when a MacOS user wants to open a file with the application. The open-file event is usually emitted
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using Quobject.SocketIoClientDotNet.Client;
|
||||
|
||||
namespace ElectronNET.API.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper for the underlying Socket connection
|
||||
/// </summary>
|
||||
public interface IApplicationSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket used to communicate with main.js
|
||||
/// </summary>
|
||||
Socket Socket { get; }
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,26 @@ namespace ElectronNET.API.Interfaces
|
||||
/// <summary>
|
||||
/// Whether to automatically download an update when it is found. (Default is true)
|
||||
/// </summary>
|
||||
bool AutoDownload { get; set; }
|
||||
bool AutoDownload { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically download an update when it is found. (Default is true)
|
||||
/// </summary>
|
||||
Task<bool> IsAutoDownloadEnabledAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically install a downloaded update on app quit (if `QuitAndInstall` was not called before).
|
||||
///
|
||||
/// Applicable only on Windows and Linux.
|
||||
/// </summary>
|
||||
bool AutoInstallOnAppQuit { get; set; }
|
||||
bool AutoInstallOnAppQuit { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically install a downloaded update on app quit (if `QuitAndInstall` was not called before).
|
||||
///
|
||||
/// Applicable only on Windows and Linux.
|
||||
/// </summary>
|
||||
Task<bool> IsAutoInstallOnAppQuitEnabledAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// *GitHub provider only.* Whether to allow update to pre-release versions.
|
||||
@@ -28,47 +40,57 @@ namespace ElectronNET.API.Interfaces
|
||||
///
|
||||
/// If "true", downgrade will be allowed("allowDowngrade" will be set to "true").
|
||||
/// </summary>
|
||||
bool AllowPrerelease { get; set; }
|
||||
bool AllowPrerelease { set; }
|
||||
|
||||
/// <summary>
|
||||
/// *GitHub provider only.* Whether to allow update to pre-release versions.
|
||||
/// Defaults to "true" if application version contains prerelease components (e.g. "0.12.1-alpha.1", here "alpha" is a prerelease component), otherwise "false".
|
||||
///
|
||||
/// If "true", downgrade will be allowed("allowDowngrade" will be set to "true").
|
||||
/// </summary>
|
||||
Task<bool> IsAllowPrereleaseEnabledAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// *GitHub provider only.*
|
||||
/// Get all release notes (from current version to latest), not just the latest (Default is false).
|
||||
/// </summary>
|
||||
bool FullChangelog { get; set; }
|
||||
bool FullChangelog { set; }
|
||||
|
||||
/// <summary>
|
||||
/// *GitHub provider only.*
|
||||
/// Get all release notes (from current version to latest), not just the latest (Default is false).
|
||||
/// </summary>
|
||||
Task<bool> IsFullChangeLogEnabledAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow version downgrade (when a user from the beta channel wants to go back to the stable channel).
|
||||
/// Taken in account only if channel differs (pre-release version component in terms of semantic versioning).
|
||||
/// Default is false.
|
||||
/// </summary>
|
||||
bool AllowDowngrade { get; set; }
|
||||
bool AllowDowngrade { set; }
|
||||
|
||||
Task<bool> IsAllowDowngradeEnabledAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// For test only.
|
||||
/// </summary>
|
||||
string UpdateConfigPath { get; }
|
||||
Task<string> GetUpdateConfigPathAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current application version
|
||||
/// </summary>
|
||||
Task<SemVer> CurrentVersionAsync { get; }
|
||||
Task<SemVer> GetCurrentVersionAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the update channel. Not applicable for GitHub.
|
||||
/// Doesn’t return channel from the update configuration, only if was previously set.
|
||||
/// </summary>
|
||||
string Channel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the update channel. Not applicable for GitHub.
|
||||
/// Doesn’t return channel from the update configuration, only if was previously set.
|
||||
/// </summary>
|
||||
Task<string> ChannelAsync { get; }
|
||||
Task<string> GetChannelAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The request headers.
|
||||
/// </summary>
|
||||
Task<Dictionary<string, string>> RequestHeadersAsync { get; }
|
||||
Task<Dictionary<string, string>> GetRequestHeadersAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The request headers.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ElectronNET.API.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Electron's process object is extended from the Node.js process object. It adds the
|
||||
/// events, properties, and methods.
|
||||
/// </summary>
|
||||
public interface IProcess
|
||||
{
|
||||
/// <summary>
|
||||
/// The process.execPath property returns the absolute pathname of the executable that
|
||||
/// started the Node.js process. Symbolic links, if any, are resolved.
|
||||
/// </summary>
|
||||
Task<string> ExecPathAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The process.argv property returns an array containing the command-line arguments passed
|
||||
/// when the Node.js process was launched. The first element will be process.execPath. See
|
||||
/// process.argv0 if access to the original value of argv[0] is needed. The second element
|
||||
/// will be the path to the JavaScript file being executed. The remaining elements will be
|
||||
/// any additional command-line arguments
|
||||
/// </summary>
|
||||
Task<string[]> ArgvAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The process.execPath property returns the absolute pathname of the executable that
|
||||
/// started the Node.js process. Symbolic links, if any, are resolved.
|
||||
/// </summary>
|
||||
Task<string> TypeAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The process.versions property returns an object listing the version strings of
|
||||
/// chrome and electron.
|
||||
/// </summary>
|
||||
Task<ProcessVersions> VersionsAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A Boolean. When app is started by being passed as parameter to the default app, this
|
||||
/// property is true in the main process, otherwise it is false.
|
||||
/// </summary>
|
||||
Task<bool> DefaultAppAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A Boolean, true when the current renderer context is the "main" renderer frame. If you
|
||||
/// want the ID of the current frame you should use webFrame.routingId
|
||||
/// </summary>
|
||||
Task<bool> IsMainFrameAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A String representing the path to the resources directory.
|
||||
/// </summary>
|
||||
Task<string> ResourcesPathAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of seconds the current Node.js process has been running. The return value
|
||||
/// includes fractions of a second. Use Math.floor() to get whole seconds.
|
||||
/// </summary>
|
||||
Task<double> UpTimeAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The PID of the electron process
|
||||
/// </summary>
|
||||
Task<int> PidAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The operating system CPU architecture for which the Node.js binary was compiled
|
||||
/// </summary>
|
||||
Task<string> ArchAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A string identifying the operating system platform on which the Node.js process is running
|
||||
/// </summary>
|
||||
Task<string> PlatformAsync { get; }
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace ElectronNET.API
|
||||
public sealed class IpcMain : IIpcMain
|
||||
{
|
||||
private static IpcMain _ipcMain;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal IpcMain() { }
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace ElectronNET.API
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsConnected => BridgeConnector.IsConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Listens to channel, when a new message arrives listener would be called with
|
||||
/// listener(event, args...).
|
||||
@@ -48,11 +50,11 @@ namespace ElectronNET.API
|
||||
{
|
||||
BridgeConnector.Emit("registerIpcMainChannel", channel);
|
||||
BridgeConnector.Off(channel);
|
||||
BridgeConnector.On<object[]>(channel, (args) =>
|
||||
BridgeConnector.On<object[]>(channel, (args) =>
|
||||
{
|
||||
var objectArray = FormatArguments(args);
|
||||
|
||||
if(objectArray.Count == 1)
|
||||
if (objectArray.Count == 1)
|
||||
{
|
||||
listener(objectArray.First());
|
||||
}
|
||||
@@ -63,6 +65,38 @@ namespace ElectronNET.API
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="channel">Channelname.</param>
|
||||
/// <param name="listener">Callback Method.</param>
|
||||
public void OnWithId(string channel, Action<(int browserId, int webContentId, object arguments)> listener)
|
||||
{
|
||||
BridgeConnector.Emit("registerIpcMainChannelWithId", channel);
|
||||
BridgeConnector.Off(channel);
|
||||
BridgeConnector.On<ArgsAndIds>(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<object> FormatArguments(object[] objectArray)
|
||||
{
|
||||
return objectArray.Where(o => o is object).ToList();
|
||||
@@ -106,7 +140,7 @@ namespace ElectronNET.API
|
||||
public void Once(string channel, Action<object> listener)
|
||||
{
|
||||
BridgeConnector.Emit("registerOnceIpcMainChannel", channel);
|
||||
BridgeConnector.On<object[]>(channel, (args) =>
|
||||
BridgeConnector.Once<object[]>(channel, (args) =>
|
||||
{
|
||||
var objectArray = FormatArguments(args);
|
||||
|
||||
@@ -141,32 +175,29 @@ namespace ElectronNET.API
|
||||
/// <param name="data">Arguments data.</param>
|
||||
public void Send(BrowserWindow browserWindow, string channel, params object[] data)
|
||||
{
|
||||
List<JObject> jobjects = new List<JObject>();
|
||||
List<JArray> jarrays = new List<JArray>();
|
||||
List<object> objects = new List<object>();
|
||||
var objectsWithCorrectSerialization = new List<object>
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -180,9 +211,9 @@ namespace ElectronNET.API
|
||||
/// <param name="data">Arguments data.</param>
|
||||
public void Send(BrowserView browserView, string channel, params object[] data)
|
||||
{
|
||||
List<JObject> jobjects = new List<JObject>();
|
||||
List<JArray> jarrays = new List<JArray>();
|
||||
List<object> objects = new List<object>();
|
||||
List<JObject> jobjects = new();
|
||||
List<JArray> jarrays = new();
|
||||
List<object> objects = new();
|
||||
|
||||
foreach (var parameterObject in data)
|
||||
{
|
||||
@@ -229,7 +260,7 @@ namespace ElectronNET.API
|
||||
BridgeConnector.Emit("console-stderr", text);
|
||||
}
|
||||
|
||||
private JsonSerializer _jsonSerializer = new JsonSerializer()
|
||||
private readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Quobject.SocketIoClientDotNet.Client;
|
||||
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
@@ -13,15 +11,11 @@ namespace ElectronNET.API
|
||||
{
|
||||
public LifetimeServiceHost(IHostApplicationLifetime lifetime)
|
||||
{
|
||||
lifetime.ApplicationStarted.Register(async () =>
|
||||
lifetime.ApplicationStarted.Register(() =>
|
||||
{
|
||||
// wait till the socket is open before setting app to ready
|
||||
while(BridgeConnector.Socket.Io().ReadyState != Manager.ReadyStateEnum.OPEN) {
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
App.Instance.IsReady = true;
|
||||
Console.WriteLine("ASP.NET Core host has fully started.");
|
||||
|
||||
BridgeConnector.Log("ASP.NET Core host has fully started.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace ElectronNET.API
|
||||
public sealed class Menu : IMenu
|
||||
{
|
||||
private static Menu _menu;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Menu() { }
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace ElectronNET.API
|
||||
/// The menu items.
|
||||
/// </value>
|
||||
public IReadOnlyCollection<MenuItem> MenuItems { get { return _menuItems.AsReadOnly(); } }
|
||||
private List<MenuItem> _menuItems = new List<MenuItem>();
|
||||
private readonly List<MenuItem> _menuItems = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application menu.
|
||||
@@ -76,7 +76,7 @@ namespace ElectronNET.API
|
||||
/// The context menu items.
|
||||
/// </value>
|
||||
public IReadOnlyDictionary<int, ReadOnlyCollection<MenuItem>> ContextMenuItems { get; internal set; }
|
||||
private Dictionary<int, List<MenuItem>> _contextMenuItems = new Dictionary<int, List<MenuItem>>();
|
||||
private readonly Dictionary<int, List<MenuItem>> _contextMenuItems = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the context menu.
|
||||
@@ -114,7 +114,7 @@ namespace ElectronNET.API
|
||||
BridgeConnector.Emit("menu-contextMenuPopup", browserWindow.Id);
|
||||
}
|
||||
|
||||
private JsonSerializer _jsonSerializer = new JsonSerializer()
|
||||
private readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Entities;
|
||||
using ElectronNET.API.Extensions;
|
||||
@@ -12,7 +13,7 @@ namespace ElectronNET.API
|
||||
public sealed class NativeTheme : INativeTheme
|
||||
{
|
||||
private static NativeTheme _nativeTheme;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal NativeTheme() { }
|
||||
|
||||
@@ -115,12 +116,16 @@ namespace ElectronNET.API
|
||||
/// A <see cref="bool"/> for if the OS / Chromium currently has high-contrast mode enabled or is
|
||||
/// being instructed to show a high-contrast UI.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> ShouldUseHighContrastColorsAsync() => BridgeConnector.OnResult<bool>("nativeTheme-shouldUseHighContrastColors", "nativeTheme-shouldUseHighContrastColors-completed");
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="bool"/> for if the OS / Chromium currently has an inverted color scheme or is
|
||||
/// being instructed to use an inverted color scheme.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public Task<bool> ShouldUseInvertedColorSchemeAsync() => BridgeConnector.OnResult<bool>("nativeTheme-shouldUseInvertedColorScheme", "nativeTheme-shouldUseInvertedColorScheme-completed");
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace ElectronNET.API
|
||||
public sealed class Notification : INotification
|
||||
{
|
||||
private static Notification _notification;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Notification() { }
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace ElectronNET.API
|
||||
}
|
||||
}
|
||||
|
||||
private static List<NotificationOptions> _notificationOptions = new List<NotificationOptions>();
|
||||
private static readonly List<NotificationOptions> _notificationOptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Create OS desktop notifications
|
||||
@@ -49,7 +49,7 @@ namespace ElectronNET.API
|
||||
{
|
||||
GenerateIDsForDefinedActions(notificationOptions);
|
||||
|
||||
BridgeConnector.Emit("createNotification", notificationOptions);
|
||||
BridgeConnector.Emit("createNotification", JObject.FromObject(notificationOptions, _jsonSerializer));
|
||||
}
|
||||
|
||||
private static void GenerateIDsForDefinedActions(NotificationOptions notificationOptions)
|
||||
@@ -135,5 +135,12 @@ namespace ElectronNET.API
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Interfaces;
|
||||
|
||||
@@ -12,6 +13,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the system is about to lock the screen.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnLockScreen
|
||||
{
|
||||
add
|
||||
@@ -41,6 +44,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the system is about to unlock the screen.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnUnLockScreen
|
||||
{
|
||||
add
|
||||
@@ -70,6 +75,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the system is suspending.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnSuspend
|
||||
{
|
||||
add
|
||||
@@ -99,6 +106,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when system is resuming.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnResume
|
||||
{
|
||||
add
|
||||
@@ -128,6 +137,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when the system changes to AC power.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnAC
|
||||
{
|
||||
add
|
||||
@@ -157,6 +168,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Emitted when system changes to battery power.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action OnBattery
|
||||
{
|
||||
add
|
||||
@@ -190,6 +203,9 @@ namespace ElectronNET.API
|
||||
/// 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()`.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
|
||||
public event Action OnShutdown
|
||||
{
|
||||
add
|
||||
@@ -217,7 +233,7 @@ namespace ElectronNET.API
|
||||
private event Action _shutdown;
|
||||
|
||||
private static PowerMonitor _powerMonitor;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal PowerMonitor() { }
|
||||
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Electron's process object is extended from the Node.js process object. It adds the
|
||||
/// events, properties, and methods.
|
||||
/// </summary>
|
||||
public sealed class Process : IProcess
|
||||
{
|
||||
internal Process() { }
|
||||
|
||||
internal static Process Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_process == null)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_process == null)
|
||||
{
|
||||
_process = new Process();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _process;
|
||||
}
|
||||
}
|
||||
|
||||
private static Process _process;
|
||||
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// The process.execPath property returns the absolute pathname of the executable that
|
||||
/// started the Node.js process. Symbolic links, if any, are resolved.
|
||||
/// </summary>
|
||||
public Task<string> ExecPathAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<string>(
|
||||
"process-execPath", "process-execPath-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The process.argv property returns an array containing the command-line arguments passed
|
||||
/// when the Node.js process was launched. The first element will be process.execPath. See
|
||||
/// process.argv0 if access to the original value of argv[0] is needed. The second element
|
||||
/// will be the path to the JavaScript file being executed. The remaining elements will be
|
||||
/// any additional command-line arguments
|
||||
/// </summary>
|
||||
public Task<string[]> ArgvAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetArrayOverSocketAsync<string[]>(
|
||||
"process-argv", "process-argv-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The process.execPath property returns the absolute pathname of the executable that
|
||||
/// started the Node.js process. Symbolic links, if any, are resolved.
|
||||
/// </summary>
|
||||
public Task<string> TypeAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<string>(
|
||||
"process-type", "process-type-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The process.versions property returns an object listing the version strings of
|
||||
/// chrome and electron.
|
||||
/// </summary>
|
||||
public Task<ProcessVersions> VersionsAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetObjectOverSocketAsync<ProcessVersions>(
|
||||
"process-versions", "process-versions-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A Boolean. When app is started by being passed as parameter to the default app, this
|
||||
/// property is true in the main process, otherwise it is false.
|
||||
/// </summary>
|
||||
public Task<bool> DefaultAppAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<bool>(
|
||||
"process-defaultApp", "process-defaultApp-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Boolean, true when the current renderer context is the "main" renderer frame. If you
|
||||
/// want the ID of the current frame you should use webFrame.routingId
|
||||
/// </summary>
|
||||
public Task<bool> IsMainFrameAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<bool>(
|
||||
"process-isMainFrame", "process-isMainFrame-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A String representing the path to the resources directory.
|
||||
/// </summary>
|
||||
public Task<string> ResourcesPathAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<string>(
|
||||
"process-resourcesPath", "process-resourcesPath-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of seconds the current Node.js process has been running. The return value
|
||||
/// includes fractions of a second. Use Math.floor() to get whole seconds.
|
||||
/// </summary>
|
||||
public Task<double> UpTimeAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<double>(
|
||||
"process-uptime", "process-uptime-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The PID of the electron process
|
||||
/// </summary>
|
||||
public Task<int> PidAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<int>(
|
||||
"process-pid", "process-pid-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The operating system CPU architecture for which the Node.js binary was compiled
|
||||
/// </summary>
|
||||
public Task<string> ArchAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<string>(
|
||||
"process-arch", "process-arch-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A string identifying the operating system platform on which the Node.js process is running
|
||||
/// </summary>
|
||||
public Task<string> PlatformAsync
|
||||
{
|
||||
get
|
||||
{
|
||||
return BridgeConnector.GetValueOverSocketAsync<string>(
|
||||
"process-platform", "process-platform-Completed");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
37
ElectronNET.API/ProcessHelper.cs
Normal file
37
ElectronNET.API/ProcessHelper.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ namespace ElectronNET.API
|
||||
private event Action<Display, string[]> _onDisplayMetricsChanged;
|
||||
|
||||
private static Screen _screen;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Screen() { }
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace ElectronNET.API
|
||||
.AddSingleton(provider => PowerMonitor.Instance)
|
||||
.AddSingleton(provider => NativeTheme.Instance)
|
||||
.AddSingleton(provider => Dock.Instance)
|
||||
.AddSingleton(provider => new ApplicationSocket { Socket = BridgeConnector.Socket, })
|
||||
// this set for proper dependency injection
|
||||
.AddSingleton<IIpcMain>(_ => IpcMain.Instance)
|
||||
.AddSingleton<IApp>(_ => App.Instance)
|
||||
@@ -48,8 +47,6 @@ namespace ElectronNET.API
|
||||
.AddSingleton<IHostHook>(_ => HostHook.Instance)
|
||||
.AddSingleton<IPowerMonitor>(_ => PowerMonitor.Instance)
|
||||
.AddSingleton<INativeTheme>(_ => NativeTheme.Instance)
|
||||
.AddSingleton<IDock>(_ => Dock.Instance)
|
||||
.AddSingleton<IProcess>(_ => Process.Instance)
|
||||
.AddSingleton<IApplicationSocket>(provider => provider.GetService<ApplicationSocket>());
|
||||
.AddSingleton<IDock>(_ => Dock.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Newtonsoft.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Extensions;
|
||||
using ElectronNET.API.Interfaces;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
@@ -14,7 +15,7 @@ namespace ElectronNET.API
|
||||
public sealed class Shell : IShell
|
||||
{
|
||||
private static Shell _shell;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Shell() { }
|
||||
|
||||
@@ -94,6 +95,7 @@ namespace ElectronNET.API
|
||||
/// <param name="url">Max 2081 characters on windows.</param>
|
||||
/// <param name="options">Controls the behavior of OpenExternal.</param>
|
||||
/// <returns>The error message corresponding to the failure if a failure occurred, otherwise <see cref="string.Empty"/>.</returns>
|
||||
|
||||
public Task<string> OpenExternalAsync(string url, OpenExternalOptions options)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
@@ -142,6 +144,7 @@ namespace ElectronNET.API
|
||||
/// <param name="operation">Default is <see cref="ShortcutLinkOperation.Create"/></param>
|
||||
/// <param name="options">Structure of a shortcut.</param>
|
||||
/// <returns>Whether the shortcut was created successfully.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<bool> WriteShortcutLinkAsync(string shortcutPath, ShortcutLinkOperation operation, ShortcutDetails options)
|
||||
{
|
||||
return BridgeConnector.OnResult<bool>("shell-writeShortcutLink", "shell-writeShortcutLinkCompleted", shortcutPath, operation.GetDescription(), options);
|
||||
@@ -153,6 +156,7 @@ namespace ElectronNET.API
|
||||
/// </summary>
|
||||
/// <param name="shortcutPath">The path tot the shortcut.</param>
|
||||
/// <returns><see cref="ShortcutDetails"/> of the shortcut.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public Task<ShortcutDetails> ReadShortcutLinkAsync(string shortcutPath)
|
||||
{
|
||||
return BridgeConnector.OnResult<ShortcutDetails>("shell-readShortcutLink", "shell-readShortcutLinkCompleted", shortcutPath);
|
||||
|
||||
64
ElectronNET.API/SocketIO/ByteArrayConverter.cs
Normal file
64
ElectronNET.API/SocketIO/ByteArrayConverter.cs
Normal file
@@ -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<byte[]>();
|
||||
}
|
||||
|
||||
internal List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
ElectronNET.API/SocketIO/DisconnectReason.cs
Normal file
11
ElectronNET.API/SocketIO/DisconnectReason.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
19
ElectronNET.API/SocketIO/EventHandlers.cs
Normal file
19
ElectronNET.API/SocketIO/EventHandlers.cs
Normal file
@@ -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<JsonElement> array);
|
||||
public delegate void OnBinaryAck(int packetId, int totalCount, List<JsonElement> array);
|
||||
public delegate void OnBinaryReceived(int packetId, int totalCount, string eventName, List<JsonElement> array);
|
||||
public delegate void OnDisconnected();
|
||||
public delegate void OnError(string error);
|
||||
public delegate void OnEventReceived(int packetId, string eventName, List<JsonElement> array);
|
||||
public delegate void OnOpened(string sid, int pingInterval, int pingTimeout);
|
||||
public delegate void OnPing();
|
||||
public delegate void OnPong();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
ElectronNET.API/SocketIO/Extensions/DisposableExtensions.cs
Normal file
12
ElectronNET.API/SocketIO/Extensions/DisposableExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace SocketIOClient.Extensions
|
||||
{
|
||||
internal static class DisposableExtensions
|
||||
{
|
||||
public static void TryDispose(this IDisposable disposable)
|
||||
{
|
||||
disposable?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace SocketIOClient.Extensions
|
||||
{
|
||||
internal static class EventHandlerExtensions
|
||||
{
|
||||
public static void TryInvoke<T>(this EventHandler<T> handler, object sender, T args)
|
||||
{
|
||||
handler?.Invoke(sender, args);
|
||||
}
|
||||
|
||||
public static void TryInvoke(this EventHandler handler, object sender, EventArgs args)
|
||||
{
|
||||
handler?.Invoke(sender, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SocketIOResponse> handler, SocketIOResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler(response);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The exception is thrown by the user code, so it can be swallowed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<byte[]>
|
||||
{
|
||||
public ByteArrayConverter()
|
||||
{
|
||||
Bytes = new List<byte[]>();
|
||||
}
|
||||
|
||||
|
||||
public List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
ElectronNET.API/SocketIO/JsonSerializer/IJsonSerializer.cs
Normal file
11
ElectronNET.API/SocketIO/JsonSerializer/IJsonSerializer.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SocketIOClient.JsonSerializer
|
||||
{
|
||||
public interface IJsonSerializer
|
||||
{
|
||||
JsonSerializeResult Serialize(object[] data);
|
||||
T Deserialize<T>(string json);
|
||||
T Deserialize<T>(string json, IList<byte[]> incomingBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SocketIOClient.JsonSerializer
|
||||
{
|
||||
public class JsonSerializeResult
|
||||
{
|
||||
public string Json { get; set; }
|
||||
public IList<byte[]> Bytes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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<T>(string json)
|
||||
{
|
||||
var options = GetOptions();
|
||||
return System.Text.Json.JsonSerializer.Deserialize<T>(json, options);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string json, IList<byte[]> bytes)
|
||||
{
|
||||
var options = GetOptions();
|
||||
var converter = new ByteArrayConverter();
|
||||
options.Converters.Add(converter);
|
||||
converter.Bytes.AddRange(bytes);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<T>(json, options);
|
||||
}
|
||||
|
||||
private JsonSerializerOptions GetOptions()
|
||||
{
|
||||
JsonSerializerOptions options = null;
|
||||
if (OptionsProvider != null)
|
||||
{
|
||||
options = OptionsProvider();
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
options = new JsonSerializerOptions();
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
public Func<JsonSerializerOptions> OptionsProvider { get; set; }
|
||||
}
|
||||
}
|
||||
23
ElectronNET.API/SocketIO/LICENSE
Normal file
23
ElectronNET.API/SocketIO/LICENSE
Normal file
@@ -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.
|
||||
100
ElectronNET.API/SocketIO/Messages/BinaryMessage.cs
Normal file
100
ElectronNET.API/SocketIO/Messages/BinaryMessage.cs
Normal file
@@ -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<JsonElement> 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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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<JsonElement>();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
75
ElectronNET.API/SocketIO/Messages/ClientAckMessage.cs
Normal file
75
ElectronNET.API/SocketIO/Messages/ClientAckMessage.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using SocketIOClient.Transport;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SocketIOClient.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// The server calls the client's callback
|
||||
/// </summary>
|
||||
public class ClientAckMessage : IMessage
|
||||
{
|
||||
public MessageType Type => MessageType.AckMessage;
|
||||
|
||||
public string Namespace { get; set; }
|
||||
|
||||
public string Event { get; set; }
|
||||
|
||||
public List<JsonElement> JsonElements { get; set; }
|
||||
|
||||
public string Json { get; set; }
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public List<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
82
ElectronNET.API/SocketIO/Messages/ClientBinaryAckMessage.cs
Normal file
82
ElectronNET.API/SocketIO/Messages/ClientBinaryAckMessage.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using SocketIOClient.Transport;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SocketIOClient.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// The server calls the client's callback with binary
|
||||
/// </summary>
|
||||
public class ClientBinaryAckMessage : IMessage
|
||||
{
|
||||
public MessageType Type => MessageType.BinaryAckMessage;
|
||||
|
||||
public string Namespace { get; set; }
|
||||
|
||||
public string Event { get; set; }
|
||||
|
||||
public List<JsonElement> 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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
128
ElectronNET.API/SocketIO/Messages/ConnectedMessage.cs
Normal file
128
ElectronNET.API/SocketIO/Messages/ConnectedMessage.cs
Normal file
@@ -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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> IncomingBytes { get; set; }
|
||||
|
||||
public int BinaryCount { get; }
|
||||
|
||||
public int Eio { get; set; }
|
||||
|
||||
public TransportProtocol Protocol { get; set; }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, string>> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ElectronNET.API/SocketIO/Messages/DisconnectedMessage.cs
Normal file
36
ElectronNET.API/SocketIO/Messages/DisconnectedMessage.cs
Normal file
@@ -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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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 + ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
50
ElectronNET.API/SocketIO/Messages/ErrorMessage.cs
Normal file
50
ElectronNET.API/SocketIO/Messages/ErrorMessage.cs
Normal file
@@ -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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
97
ElectronNET.API/SocketIO/Messages/EventMessage.cs
Normal file
97
ElectronNET.API/SocketIO/Messages/EventMessage.cs
Normal file
@@ -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<JsonElement> JsonElements { get; set; }
|
||||
|
||||
public string Json { get; set; }
|
||||
|
||||
public List<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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<JsonElement>();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
ElectronNET.API/SocketIO/Messages/IMessage.cs
Normal file
30
ElectronNET.API/SocketIO/Messages/IMessage.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using SocketIOClient.Transport;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SocketIOClient.Messages
|
||||
{
|
||||
public interface IMessage
|
||||
{
|
||||
MessageType Type { get; }
|
||||
|
||||
List<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
75
ElectronNET.API/SocketIO/Messages/MessageFactory.cs
Normal file
75
ElectronNET.API/SocketIO/Messages/MessageFactory.cs
Normal file
@@ -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<string, MessageType> _messageTypes = Enum.GetValues<MessageType>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ElectronNET.API/SocketIO/Messages/MessageType.cs
Normal file
16
ElectronNET.API/SocketIO/Messages/MessageType.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SocketIOClient.Messages
|
||||
{
|
||||
public enum MessageType
|
||||
{
|
||||
Opened,
|
||||
Ping = 2,
|
||||
Pong,
|
||||
Connected = 40,
|
||||
Disconnected,
|
||||
EventMessage,
|
||||
AckMessage,
|
||||
ErrorMessage,
|
||||
BinaryMessage,
|
||||
BinaryAckMessage
|
||||
}
|
||||
}
|
||||
79
ElectronNET.API/SocketIO/Messages/OpenedMessage.cs
Normal file
79
ElectronNET.API/SocketIO/Messages/OpenedMessage.cs
Normal file
@@ -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<string> Upgrades { get; private set; }
|
||||
|
||||
public int PingInterval { get; private set; }
|
||||
|
||||
public int PingTimeout { get; private set; }
|
||||
|
||||
public List<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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<string>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
ElectronNET.API/SocketIO/Messages/PingMessage.cs
Normal file
26
ElectronNET.API/SocketIO/Messages/PingMessage.cs
Normal file
@@ -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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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";
|
||||
}
|
||||
}
|
||||
29
ElectronNET.API/SocketIO/Messages/PongMessage.cs
Normal file
29
ElectronNET.API/SocketIO/Messages/PongMessage.cs
Normal file
@@ -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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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";
|
||||
}
|
||||
}
|
||||
54
ElectronNET.API/SocketIO/Messages/ServerAckMessage.cs
Normal file
54
ElectronNET.API/SocketIO/Messages/ServerAckMessage.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using SocketIOClient.Transport;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SocketIOClient.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// The client calls the server's callback
|
||||
/// </summary>
|
||||
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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
ElectronNET.API/SocketIO/Messages/ServerBinaryAckMessage.cs
Normal file
60
ElectronNET.API/SocketIO/Messages/ServerBinaryAckMessage.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using SocketIOClient.Transport;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SocketIOClient.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// The client calls the server's callback with binary
|
||||
/// </summary>
|
||||
public class ServerBinaryAckMessage : IMessage
|
||||
{
|
||||
public MessageType Type => MessageType.BinaryAckMessage;
|
||||
|
||||
public string Namespace { get; set; }
|
||||
|
||||
public List<JsonElement> 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<byte[]> OutgoingBytes { get; set; }
|
||||
|
||||
public List<byte[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
56
ElectronNET.API/SocketIO/NewtonsoftJsonSerializer.cs
Normal file
56
ElectronNET.API/SocketIO/NewtonsoftJsonSerializer.cs
Normal file
@@ -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<JsonSerializerSettings> 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<T>(string json)
|
||||
{
|
||||
var settings = GetOptions();
|
||||
return JsonConvert.DeserializeObject<T>(json, settings);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string json, IList<byte[]> bytes)
|
||||
{
|
||||
var converter = new ByteArrayConverter();
|
||||
converter.Bytes.AddRange(bytes);
|
||||
var settings = GetOptions();
|
||||
settings.Converters.Add(converter);
|
||||
return JsonConvert.DeserializeObject<T>(json, settings);
|
||||
}
|
||||
|
||||
private JsonSerializerSettings GetOptions()
|
||||
{
|
||||
JsonSerializerSettings options = null;
|
||||
if (OptionsProvider != null)
|
||||
{
|
||||
options = OptionsProvider();
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
options = new JsonSerializerSettings();
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
public Func<JsonSerializerSettings> OptionsProvider { get; set; }
|
||||
}
|
||||
}
|
||||
769
ElectronNET.API/SocketIO/SocketIO.cs
Normal file
769
ElectronNET.API/SocketIO/SocketIO.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// socket.io client class
|
||||
/// </summary>
|
||||
public class SocketIO : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Create SocketIO object with default options
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
public SocketIO(string uri) : this(new Uri(uri)) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create SocketIO object with options
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
public SocketIO(Uri uri) : this(uri, new SocketIOOptions()) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create SocketIO object with options
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="options"></param>
|
||||
public SocketIO(string uri, SocketIOOptions options) : this(new Uri(uri), options) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create SocketIO object with options
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="options"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An unique identifier for the socket session. Set after the connect event is triggered, and updated after the reconnect event.
|
||||
/// </summary>
|
||||
public string Id { get; private set; }
|
||||
|
||||
string _namespace;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the socket is connected to the server.
|
||||
/// </summary>
|
||||
public bool Connected { get; private set; }
|
||||
|
||||
int _attempts;
|
||||
|
||||
[Obsolete]
|
||||
/// <summary>
|
||||
/// Whether or not the socket is disconnected from the server.
|
||||
/// </summary>
|
||||
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<SocketIO>();
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get; set; }
|
||||
|
||||
public Func<IClientWebSocket> ClientWebSocketProvider { get; set; }
|
||||
private IClientWebSocket _clientWebsocket;
|
||||
|
||||
BaseTransport _transport;
|
||||
|
||||
List<Type> _expectedExceptions;
|
||||
|
||||
int _packetId;
|
||||
bool _isConnectCoreRunning;
|
||||
Uri _realServerUri;
|
||||
Exception _connectCoreException;
|
||||
Dictionary<int, Action<SocketIOResponse>> _ackHandlers;
|
||||
List<OnAnyHandler> _onAnyHandlers;
|
||||
Dictionary<string, Action<SocketIOResponse>> _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<string> OnConnectError;
|
||||
//public event EventHandler<string> OnConnectTimeout;
|
||||
public event EventHandler<string> OnError;
|
||||
public event EventHandler<string> OnDisconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Fired upon a successful reconnection.
|
||||
/// </summary>
|
||||
public event EventHandler<int> OnReconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Fired upon an attempt to reconnect.
|
||||
/// </summary>
|
||||
public event EventHandler<int> OnReconnectAttempt;
|
||||
|
||||
/// <summary>
|
||||
/// Fired upon a reconnection attempt error.
|
||||
/// </summary>
|
||||
public event EventHandler<Exception> OnReconnectError;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when couldn’t reconnect within reconnectionAttempts
|
||||
/// </summary>
|
||||
public event EventHandler OnReconnectFailed;
|
||||
public event EventHandler OnPing;
|
||||
public event EventHandler<TimeSpan> OnPong;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Observable Event
|
||||
//Subject<Unit> _onConnected;
|
||||
//public IObservable<Unit> ConnectedObservable { get; private set; }
|
||||
#endregion
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
_packetId = -1;
|
||||
_ackHandlers = new Dictionary<int, Action<SocketIOResponse>>();
|
||||
_eventHandlers = new Dictionary<string, Action<SocketIOResponse>>();
|
||||
_onAnyHandlers = new List<OnAnyHandler>();
|
||||
|
||||
JsonSerializer = new SystemTextJsonSerializer();
|
||||
UriConverter = new UriConverter();
|
||||
|
||||
HttpClient = new HttpClient();
|
||||
ClientWebSocketProvider = () => new SystemNetWebSocketsClientWebSocket(Options.EIO);
|
||||
_expectedExceptions = new List<Type>
|
||||
{
|
||||
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<TransportProtocol> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new handler for the given event.
|
||||
/// </summary>
|
||||
/// <param name="eventName"></param>
|
||||
/// <param name="callback"></param>
|
||||
public void On(string eventName, Action<SocketIOResponse> callback)
|
||||
{
|
||||
if (_eventHandlers.ContainsKey(eventName))
|
||||
{
|
||||
_eventHandlers.Remove(eventName);
|
||||
}
|
||||
_eventHandlers.Add(eventName, callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a new handler for the given event.
|
||||
/// </summary>
|
||||
/// <param name="eventName"></param>
|
||||
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<byte[]>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits an event to the socket
|
||||
/// </summary>
|
||||
/// <param name="eventName"></param>
|
||||
/// <param name="data">Any other parameters can be included. All serializable datastructures are supported, including byte[]</param>
|
||||
/// <returns></returns>
|
||||
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<byte[]>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits an event to the socket
|
||||
/// </summary>
|
||||
/// <param name="eventName"></param>
|
||||
/// <param name="ack">will be called with the server answer.</param>
|
||||
/// <param name="data">Any other parameters can be included. All serializable datastructures are supported, including byte[]</param>
|
||||
/// <returns></returns>
|
||||
public async Task EmitAsync(string eventName, Action<SocketIOResponse> ack, params object[] data)
|
||||
{
|
||||
await EmitAsync(eventName, CancellationToken.None, ack, data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task EmitAsync(string eventName, CancellationToken cancellationToken, Action<SocketIOResponse> 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<byte[]>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
ElectronNET.API/SocketIO/SocketIOOptions.cs
Normal file
65
ElectronNET.API/SocketIO/SocketIOOptions.cs
Normal file
@@ -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<KeyValuePair<string, string>> Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow reconnection if accidentally disconnected
|
||||
/// </summary>
|
||||
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<string, string> ExtraHeaders { get; set; }
|
||||
|
||||
public TransportProtocol Transport { get; set; }
|
||||
|
||||
public int EIO { get; set; }
|
||||
|
||||
public bool AutoUpgrade { get; set; }
|
||||
|
||||
public object Auth { get; set; }
|
||||
}
|
||||
}
|
||||
62
ElectronNET.API/SocketIO/SocketIOResponse.cs
Normal file
62
ElectronNET.API/SocketIO/SocketIOResponse.cs
Normal file
@@ -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<JsonElement> array, SocketIO socket)
|
||||
{
|
||||
_array = array;
|
||||
InComingBytes = new List<byte[]>();
|
||||
SocketIO = socket;
|
||||
PacketId = -1;
|
||||
}
|
||||
|
||||
readonly IList<JsonElement> _array;
|
||||
|
||||
public List<byte[]> InComingBytes { get; }
|
||||
public SocketIO SocketIO { get; }
|
||||
public int PacketId { get; set; }
|
||||
|
||||
public T GetValue<T>(int index = 0)
|
||||
{
|
||||
var element = GetValue(index);
|
||||
string json = element.GetRawText();
|
||||
return SocketIO.JsonSerializer.Deserialize<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
245
ElectronNET.API/SocketIO/Transport/BaseTransport.cs
Normal file
245
ElectronNET.API/SocketIO/Transport/BaseTransport.cs
Normal file
@@ -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<string>, IObserver<byte[]>, IObservable<IMessage>, IDisposable
|
||||
{
|
||||
public BaseTransport(SocketIOOptions options, IJsonSerializer jsonSerializer, ILogger logger)
|
||||
{
|
||||
Options = options;
|
||||
MessageSubject = new Subject<IMessage>();
|
||||
JsonSerializer = jsonSerializer;
|
||||
UriConverter = new UriConverter();
|
||||
_messageQueue = new Queue<IMessage>();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
DateTime _pingTime;
|
||||
readonly Queue<IMessage> _messageQueue;
|
||||
readonly ILogger _logger;
|
||||
|
||||
protected SocketIOOptions Options { get; }
|
||||
protected Subject<IMessage> MessageSubject { get; }
|
||||
|
||||
protected IJsonSerializer JsonSerializer { get; }
|
||||
protected CancellationTokenSource PingTokenSource { get; private set; }
|
||||
protected OpenedMessage OpenedMessage { get; private set; }
|
||||
|
||||
public string Namespace { get; set; }
|
||||
public IUriConverter UriConverter { get; set; }
|
||||
|
||||
public async Task SendAsync(IMessage msg, CancellationToken cancellationToken)
|
||||
{
|
||||
msg.Eio = Options.EIO;
|
||||
msg.Protocol = Options.Transport;
|
||||
var payload = new Payload
|
||||
{
|
||||
Text = msg.Write()
|
||||
};
|
||||
if (msg.OutgoingBytes != null)
|
||||
{
|
||||
payload.Bytes = msg.OutgoingBytes;
|
||||
}
|
||||
await SendAsync(payload, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected virtual async Task OpenAsync(OpenedMessage msg)
|
||||
{
|
||||
OpenedMessage = msg;
|
||||
if (Options.EIO == 3 && string.IsNullOrEmpty(Namespace))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var connectMsg = new ConnectedMessage
|
||||
{
|
||||
Namespace = Namespace,
|
||||
Eio = Options.EIO,
|
||||
Query = Options.Query,
|
||||
};
|
||||
if (Options.EIO == 4)
|
||||
{
|
||||
if (Options.Auth != null)
|
||||
{
|
||||
connectMsg.AuthJsonStr = JsonSerializer.Serialize(new[] { Options.Auth }).Json.TrimStart('[').TrimEnd(']');
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i <= 3; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendAsync(connectMsg, CancellationToken.None).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (i == 3)
|
||||
OnError(e);
|
||||
else
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, i) * 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Eio3 ping is sent by the client</para>
|
||||
/// <para>Eio4 ping is sent by the server</para>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
private void StartPing(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug($"[Ping] Interval: {OpenedMessage.PingInterval}");
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(OpenedMessage.PingInterval);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
try
|
||||
{
|
||||
var ping = new PingMessage();
|
||||
_logger.LogDebug($"[Ping] Sending");
|
||||
await SendAsync(ping, CancellationToken.None).ConfigureAwait(false);
|
||||
_logger.LogDebug($"[Ping] Has been sent");
|
||||
_pingTime = DateTime.Now;
|
||||
MessageSubject.OnNext(ping);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogDebug($"[Ping] Failed to send, {e.Message}");
|
||||
MessageSubject.OnError(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public abstract Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
|
||||
|
||||
public abstract Task DisconnectAsync(CancellationToken cancellationToken);
|
||||
|
||||
public abstract void AddHeader(string key, string val);
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
MessageSubject.Dispose();
|
||||
_messageQueue.Clear();
|
||||
if (PingTokenSource != null)
|
||||
{
|
||||
PingTokenSource.Cancel();
|
||||
PingTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task SendAsync(Payload payload, CancellationToken cancellationToken);
|
||||
|
||||
public void OnCompleted()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnError(Exception error)
|
||||
{
|
||||
MessageSubject.OnError(error);
|
||||
}
|
||||
|
||||
public void OnNext(string text)
|
||||
{
|
||||
_logger.LogDebug($"[Receive] {text}");
|
||||
var msg = MessageFactory.CreateMessage(Options.EIO, text);
|
||||
if (msg == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (msg.BinaryCount > 0)
|
||||
{
|
||||
msg.IncomingBytes = new List<byte[]>(msg.BinaryCount);
|
||||
_messageQueue.Enqueue(msg);
|
||||
return;
|
||||
}
|
||||
if (msg.Type == MessageType.Opened)
|
||||
{
|
||||
OpenAsync(msg as OpenedMessage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (Options.EIO == 3)
|
||||
{
|
||||
if (msg.Type == MessageType.Connected)
|
||||
{
|
||||
var connectMsg = msg as ConnectedMessage;
|
||||
connectMsg.Sid = OpenedMessage.Sid;
|
||||
if ((string.IsNullOrEmpty(Namespace) && string.IsNullOrEmpty(connectMsg.Namespace)) || connectMsg.Namespace == Namespace)
|
||||
{
|
||||
if (PingTokenSource != null)
|
||||
{
|
||||
PingTokenSource.Cancel();
|
||||
}
|
||||
PingTokenSource = new CancellationTokenSource();
|
||||
StartPing(PingTokenSource.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (msg.Type == MessageType.Pong)
|
||||
{
|
||||
var pong = msg as PongMessage;
|
||||
pong.Duration = DateTime.Now - _pingTime;
|
||||
}
|
||||
}
|
||||
|
||||
MessageSubject.OnNext(msg);
|
||||
|
||||
if (msg.Type == MessageType.Ping)
|
||||
{
|
||||
_pingTime = DateTime.Now;
|
||||
try
|
||||
{
|
||||
SendAsync(new PongMessage(), CancellationToken.None).ConfigureAwait(false);
|
||||
MessageSubject.OnNext(new PongMessage
|
||||
{
|
||||
Eio = Options.EIO,
|
||||
Protocol = Options.Transport,
|
||||
Duration = DateTime.Now - _pingTime
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
OnError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnNext(byte[] bytes)
|
||||
{
|
||||
_logger.LogDebug($"[Receive] binary message");
|
||||
if (_messageQueue.Count > 0)
|
||||
{
|
||||
var msg = _messageQueue.Peek();
|
||||
msg.IncomingBytes.Add(bytes);
|
||||
if (msg.IncomingBytes.Count == msg.BinaryCount)
|
||||
{
|
||||
MessageSubject.OnNext(msg);
|
||||
_messageQueue.Dequeue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Subscribe(IObserver<IMessage> observer)
|
||||
{
|
||||
return MessageSubject.Subscribe(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
ElectronNET.API/SocketIO/Transport/Eio3HttpPollingHandler.cs
Normal file
76
ElectronNET.API/SocketIO/Transport/Eio3HttpPollingHandler.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public class Eio3HttpPollingHandler : HttpPollingHandler
|
||||
{
|
||||
public Eio3HttpPollingHandler(HttpClient httpClient) : base(httpClient) { }
|
||||
|
||||
public override async Task PostAsync(string uri, IEnumerable<byte[]> bytes, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<byte>();
|
||||
foreach (var item in bytes)
|
||||
{
|
||||
list.Add(1);
|
||||
var length = SplitInt(item.Length + 1).Select(x => (byte)x);
|
||||
list.AddRange(length);
|
||||
list.Add(byte.MaxValue);
|
||||
list.Add(4);
|
||||
list.AddRange(item);
|
||||
}
|
||||
var content = new ByteArrayContent(list.ToArray());
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
await HttpClient.PostAsync(AppendRandom(uri), content, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private List<int> SplitInt(int number)
|
||||
{
|
||||
List<int> list = new List<int>();
|
||||
while (number > 0)
|
||||
{
|
||||
list.Add(number % 10);
|
||||
number /= 10;
|
||||
}
|
||||
list.Reverse();
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override void ProduceText(string text)
|
||||
{
|
||||
int p = 0;
|
||||
while (true)
|
||||
{
|
||||
int index = text.IndexOf(':', p);
|
||||
if (index == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (int.TryParse(text.Substring(p, index - p), out int length))
|
||||
{
|
||||
string msg = text.Substring(index + 1, length);
|
||||
TextSubject.OnNext(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
p = index + length + 1;
|
||||
if (p >= text.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Task PostAsync(string uri, string content, CancellationToken cancellationToken)
|
||||
{
|
||||
content = content.Length + ":" + content;
|
||||
return base.PostAsync(uri, content, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
ElectronNET.API/SocketIO/Transport/Eio4HttpPollingHandler.cs
Normal file
48
ElectronNET.API/SocketIO/Transport/Eio4HttpPollingHandler.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public class Eio4HttpPollingHandler : HttpPollingHandler
|
||||
{
|
||||
public Eio4HttpPollingHandler(HttpClient httpClient) : base(httpClient) { }
|
||||
|
||||
const char Separator = '\u001E'; //1E
|
||||
|
||||
public override async Task PostAsync(string uri, IEnumerable<byte[]> bytes, CancellationToken cancellationToken)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var item in bytes)
|
||||
{
|
||||
builder.Append('b').Append(Convert.ToBase64String(item)).Append(Separator);
|
||||
}
|
||||
if (builder.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string text = builder.ToString().TrimEnd(Separator);
|
||||
await PostAsync(uri, text, cancellationToken);
|
||||
}
|
||||
|
||||
protected override void ProduceText(string text)
|
||||
{
|
||||
string[] items = text.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item[0] == 'b')
|
||||
{
|
||||
byte[] bytes = Convert.FromBase64String(item.Substring(1));
|
||||
BytesSubject.OnNext(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
TextSubject.OnNext(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
ElectronNET.API/SocketIO/Transport/HttpPollingHandler.cs
Normal file
118
ElectronNET.API/SocketIO/Transport/HttpPollingHandler.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public abstract class HttpPollingHandler : IHttpPollingHandler
|
||||
{
|
||||
public HttpPollingHandler(HttpClient httpClient)
|
||||
{
|
||||
HttpClient = httpClient;
|
||||
TextSubject = new Subject<string>();
|
||||
BytesSubject = new Subject<byte[]>();
|
||||
TextObservable = TextSubject.AsObservable();
|
||||
BytesObservable = BytesSubject.AsObservable();
|
||||
}
|
||||
|
||||
protected HttpClient HttpClient { get; }
|
||||
protected Subject<string> TextSubject{get;}
|
||||
protected Subject<byte[]> BytesSubject{get;}
|
||||
|
||||
public IObservable<string> TextObservable { get; }
|
||||
public IObservable<byte[]> BytesObservable { get; }
|
||||
|
||||
protected string AppendRandom(string uri)
|
||||
{
|
||||
return uri + "&t=" + DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public async Task GetAsync(string uri, CancellationToken cancellationToken)
|
||||
{
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, AppendRandom(uri));
|
||||
var resMsg = await HttpClient.SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||
if (!resMsg.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Response status code does not indicate success: {resMsg.StatusCode}");
|
||||
}
|
||||
await ProduceMessageAsync(resMsg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SendAsync(HttpRequestMessage req, CancellationToken cancellationToken)
|
||||
{
|
||||
var resMsg = await HttpClient.SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||
if (!resMsg.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Response status code does not indicate success: {resMsg.StatusCode}");
|
||||
}
|
||||
await ProduceMessageAsync(resMsg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async virtual Task PostAsync(string uri, string content, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpContent = new StringContent(content);
|
||||
var resMsg = await HttpClient.PostAsync(AppendRandom(uri), httpContent, cancellationToken).ConfigureAwait(false);
|
||||
await ProduceMessageAsync(resMsg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public abstract Task PostAsync(string uri, IEnumerable<byte[]> bytes, CancellationToken cancellationToken);
|
||||
|
||||
private async Task ProduceMessageAsync(HttpResponseMessage resMsg)
|
||||
{
|
||||
if (resMsg.Content.Headers.ContentType.MediaType == "application/octet-stream")
|
||||
{
|
||||
byte[] bytes = await resMsg.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
ProduceBytes(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
string text = await resMsg.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
ProduceText(text);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ProduceText(string text);
|
||||
|
||||
private void ProduceBytes(byte[] bytes)
|
||||
{
|
||||
int i = 0;
|
||||
while (bytes.Length > i + 4)
|
||||
{
|
||||
byte type = bytes[i];
|
||||
var builder = new StringBuilder();
|
||||
i++;
|
||||
while (bytes[i] != byte.MaxValue)
|
||||
{
|
||||
builder.Append(bytes[i]);
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
int length = int.Parse(builder.ToString());
|
||||
if (type == 0)
|
||||
{
|
||||
var buffer = new byte[length];
|
||||
Buffer.BlockCopy(bytes, i, buffer, 0, buffer.Length);
|
||||
TextSubject.OnNext(Encoding.UTF8.GetString(buffer));
|
||||
}
|
||||
else if (type == 1)
|
||||
{
|
||||
var buffer = new byte[length - 1];
|
||||
Buffer.BlockCopy(bytes, i + 1, buffer, 0, buffer.Length);
|
||||
BytesSubject.OnNext(buffer);
|
||||
}
|
||||
i += length;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TextSubject.Dispose();
|
||||
BytesSubject.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
121
ElectronNET.API/SocketIO/Transport/HttpTransport.cs
Normal file
121
ElectronNET.API/SocketIO/Transport/HttpTransport.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SocketIOClient.JsonSerializer;
|
||||
using SocketIOClient.Messages;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public class HttpTransport : BaseTransport
|
||||
{
|
||||
public HttpTransport(HttpClient http,
|
||||
IHttpPollingHandler pollingHandler,
|
||||
SocketIOOptions options,
|
||||
IJsonSerializer jsonSerializer,
|
||||
ILogger logger) : base(options, jsonSerializer, logger)
|
||||
{
|
||||
_http = http;
|
||||
_httpPollingHandler = pollingHandler;
|
||||
_httpPollingHandler.TextObservable.Subscribe(this);
|
||||
_httpPollingHandler.BytesObservable.Subscribe(this);
|
||||
}
|
||||
|
||||
string _httpUri;
|
||||
CancellationTokenSource _pollingTokenSource;
|
||||
|
||||
readonly HttpClient _http;
|
||||
readonly IHttpPollingHandler _httpPollingHandler;
|
||||
|
||||
private void StartPolling(CancellationToken cancellationToken)
|
||||
{
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
int retry = 0;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (!_httpUri.Contains("&sid="))
|
||||
{
|
||||
await Task.Delay(20);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _httpPollingHandler.GetAsync(_httpUri, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
retry++;
|
||||
if (retry >= 3)
|
||||
{
|
||||
MessageSubject.OnError(e);
|
||||
break;
|
||||
}
|
||||
await Task.Delay(100 * (int)Math.Pow(2, retry));
|
||||
}
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public override async Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
// if (_options.ExtraHeaders != null)
|
||||
// {
|
||||
// foreach (var item in _options.ExtraHeaders)
|
||||
// {
|
||||
// req.Headers.Add(item.Key, item.Value);
|
||||
// }
|
||||
// }
|
||||
|
||||
_httpUri = uri.ToString();
|
||||
await _httpPollingHandler.SendAsync(req, new CancellationTokenSource(Options.ConnectionTimeout).Token).ConfigureAwait(false);
|
||||
if (_pollingTokenSource != null)
|
||||
{
|
||||
_pollingTokenSource.Cancel();
|
||||
}
|
||||
_pollingTokenSource = new CancellationTokenSource();
|
||||
StartPolling(_pollingTokenSource.Token);
|
||||
}
|
||||
|
||||
public override Task DisconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_pollingTokenSource.Cancel();
|
||||
if (PingTokenSource != null)
|
||||
{
|
||||
PingTokenSource.Cancel();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override void AddHeader(string key, string val)
|
||||
{
|
||||
_http.DefaultRequestHeaders.Add(key, val);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_httpPollingHandler.Dispose();
|
||||
}
|
||||
|
||||
public override async Task SendAsync(Payload payload, CancellationToken cancellationToken)
|
||||
{
|
||||
await _httpPollingHandler.PostAsync(_httpUri, payload.Text, cancellationToken);
|
||||
if (payload.Bytes != null && payload.Bytes.Count > 0)
|
||||
{
|
||||
await _httpPollingHandler.PostAsync(_httpUri, payload.Bytes, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OpenAsync(OpenedMessage msg)
|
||||
{
|
||||
//if (!_httpUri.Contains("&sid="))
|
||||
//{
|
||||
//}
|
||||
_httpUri += "&sid=" + msg.Sid;
|
||||
await base.OpenAsync(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ElectronNET.API/SocketIO/Transport/IClientWebSocket.cs
Normal file
16
ElectronNET.API/SocketIO/Transport/IClientWebSocket.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public interface IClientWebSocket : IDisposable
|
||||
{
|
||||
IObservable<string> TextObservable { get; }
|
||||
IObservable<byte[]> BytesObservable { get; }
|
||||
Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
|
||||
Task DisconnectAsync(CancellationToken cancellationToken);
|
||||
Task SendAsync(byte[] bytes, TransportMessageType type, bool endOfMessage, CancellationToken cancellationToken);
|
||||
void AddHeader(string key, string val);
|
||||
}
|
||||
}
|
||||
18
ElectronNET.API/SocketIO/Transport/IHttpPollingHandler.cs
Normal file
18
ElectronNET.API/SocketIO/Transport/IHttpPollingHandler.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public interface IHttpPollingHandler : IDisposable
|
||||
{
|
||||
IObservable<string> TextObservable { get; }
|
||||
IObservable<byte[]> BytesObservable { get; }
|
||||
Task GetAsync(string uri, CancellationToken cancellationToken);
|
||||
Task SendAsync(HttpRequestMessage req, CancellationToken cancellationToken);
|
||||
Task PostAsync(string uri, string content, CancellationToken cancellationToken);
|
||||
Task PostAsync(string uri, IEnumerable<byte[]> bytes, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
10
ElectronNET.API/SocketIO/Transport/Payload.cs
Normal file
10
ElectronNET.API/SocketIO/Transport/Payload.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public class Payload
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public List<byte[]> Bytes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public class SystemNetWebSocketsClientWebSocket : IClientWebSocket
|
||||
{
|
||||
public SystemNetWebSocketsClientWebSocket(int eio)
|
||||
{
|
||||
_eio = eio;
|
||||
_textSubject = new Subject<string>();
|
||||
_bytesSubject = new Subject<byte[]>();
|
||||
TextObservable = _textSubject.AsObservable();
|
||||
BytesObservable = _bytesSubject.AsObservable();
|
||||
_ws = new ClientWebSocket();
|
||||
_listenCancellation = new CancellationTokenSource();
|
||||
_sendLock = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
|
||||
const int ReceiveChunkSize = 1024 * 8;
|
||||
|
||||
readonly int _eio;
|
||||
readonly ClientWebSocket _ws;
|
||||
readonly Subject<string> _textSubject;
|
||||
readonly Subject<byte[]> _bytesSubject;
|
||||
readonly CancellationTokenSource _listenCancellation;
|
||||
readonly SemaphoreSlim _sendLock;
|
||||
|
||||
public IObservable<string> TextObservable { get; }
|
||||
public IObservable<byte[]> BytesObservable { get; }
|
||||
|
||||
private void Listen()
|
||||
{
|
||||
Task.Factory.StartNew(async() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_listenCancellation.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var buffer = new byte[ReceiveChunkSize];
|
||||
int count = 0;
|
||||
WebSocketReceiveResult result = null;
|
||||
|
||||
while (_ws.State == WebSocketState.Open)
|
||||
{
|
||||
var subBuffer = new byte[ReceiveChunkSize];
|
||||
try
|
||||
{
|
||||
result = await _ws.ReceiveAsync(new ArraySegment<byte>(subBuffer), CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// resize
|
||||
if (buffer.Length - count < result.Count)
|
||||
{
|
||||
Array.Resize(ref buffer, buffer.Length + result.Count);
|
||||
}
|
||||
Buffer.BlockCopy(subBuffer, 0, buffer, count, result.Count);
|
||||
count += result.Count;
|
||||
if (result.EndOfMessage)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_textSubject.OnError(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
switch (result.MessageType)
|
||||
{
|
||||
case WebSocketMessageType.Text:
|
||||
string text = Encoding.UTF8.GetString(buffer, 0, count);
|
||||
_textSubject.OnNext(text);
|
||||
break;
|
||||
case WebSocketMessageType.Binary:
|
||||
byte[] bytes;
|
||||
if (_eio == 3)
|
||||
{
|
||||
bytes = new byte[count - 1];
|
||||
Buffer.BlockCopy(buffer, 1, bytes, 0, bytes.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytes = new byte[count];
|
||||
Buffer.BlockCopy(buffer, 0, bytes, 0, bytes.Length);
|
||||
}
|
||||
_bytesSubject.OnNext(bytes);
|
||||
break;
|
||||
case WebSocketMessageType.Close:
|
||||
_textSubject.OnError(new WebSocketException("Received a Close message"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
await _ws.ConnectAsync(uri, cancellationToken);
|
||||
Listen();
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] bytes, TransportMessageType type, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var msgType = WebSocketMessageType.Text;
|
||||
if (type == TransportMessageType.Binary)
|
||||
{
|
||||
msgType = WebSocketMessageType.Binary;
|
||||
}
|
||||
await _ws.SendAsync(new ArraySegment<byte>(bytes), msgType, endOfMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void AddHeader(string key, string val)
|
||||
{
|
||||
_ws.Options.SetRequestHeader(key, val);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_textSubject.Dispose();
|
||||
_bytesSubject.Dispose();
|
||||
_ws.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public enum TransportMessageType
|
||||
{
|
||||
Text = 0,
|
||||
Binary = 1
|
||||
}
|
||||
}
|
||||
8
ElectronNET.API/SocketIO/Transport/TransportProtocol.cs
Normal file
8
ElectronNET.API/SocketIO/Transport/TransportProtocol.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public enum TransportProtocol
|
||||
{
|
||||
Polling,
|
||||
WebSocket
|
||||
}
|
||||
}
|
||||
92
ElectronNET.API/SocketIO/Transport/WebSocketTransport.cs
Normal file
92
ElectronNET.API/SocketIO/Transport/WebSocketTransport.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SocketIOClient.JsonSerializer;
|
||||
|
||||
namespace SocketIOClient.Transport
|
||||
{
|
||||
public class WebSocketTransport : BaseTransport
|
||||
{
|
||||
public WebSocketTransport(IClientWebSocket ws, SocketIOOptions options, IJsonSerializer jsonSerializer, ILogger logger)
|
||||
: base(options, jsonSerializer, logger)
|
||||
{
|
||||
_ws = ws;
|
||||
_sendLock = new SemaphoreSlim(1, 1);
|
||||
_ws.TextObservable.Subscribe(this);
|
||||
_ws.BytesObservable.Subscribe(this);
|
||||
}
|
||||
|
||||
const int ReceiveChunkSize = 1024 * 8;
|
||||
const int SendChunkSize = 1024 * 8;
|
||||
|
||||
readonly IClientWebSocket _ws;
|
||||
readonly SemaphoreSlim _sendLock;
|
||||
|
||||
private async Task SendAsync(TransportMessageType type, byte[] bytes, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _sendLock.WaitAsync().ConfigureAwait(false);
|
||||
if (type == TransportMessageType.Binary && Options.EIO == 3)
|
||||
{
|
||||
byte[] buffer = new byte[bytes.Length + 1];
|
||||
buffer[0] = 4;
|
||||
Buffer.BlockCopy(bytes, 0, buffer, 1, bytes.Length);
|
||||
bytes = buffer;
|
||||
}
|
||||
int pages = (int)Math.Ceiling(bytes.Length * 1.0 / SendChunkSize);
|
||||
for (int i = 0; i < pages; i++)
|
||||
{
|
||||
int offset = i * SendChunkSize;
|
||||
int length = SendChunkSize;
|
||||
if (offset + length > bytes.Length)
|
||||
{
|
||||
length = bytes.Length - offset;
|
||||
}
|
||||
byte[] subBuffer = new byte[length];
|
||||
Buffer.BlockCopy(bytes, offset, subBuffer, 0, subBuffer.Length);
|
||||
bool endOfMessage = pages - 1 == i;
|
||||
await _ws.SendAsync(subBuffer, type, endOfMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
await _ws.ConnectAsync(uri, cancellationToken);
|
||||
}
|
||||
|
||||
public override async Task DisconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _ws.DisconnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async Task SendAsync(Payload payload, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(payload.Text);
|
||||
await SendAsync(TransportMessageType.Text, bytes, cancellationToken);
|
||||
if (payload.Bytes != null)
|
||||
{
|
||||
foreach (var item in payload.Bytes)
|
||||
{
|
||||
await SendAsync(TransportMessageType.Binary, item, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void AddHeader(string key, string val) => _ws.AddHeader(key, val);
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_sendLock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
ElectronNET.API/SocketIO/UriConverters/IUriConverter.cs
Normal file
10
ElectronNET.API/SocketIO/UriConverters/IUriConverter.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SocketIOClient.UriConverters
|
||||
{
|
||||
public interface IUriConverter
|
||||
{
|
||||
Uri GetServerUri(bool ws, Uri serverUri, int eio, string path, IEnumerable<KeyValuePair<string, string>> queryParams);
|
||||
}
|
||||
}
|
||||
54
ElectronNET.API/SocketIO/UriConverters/UriConverter.cs
Normal file
54
ElectronNET.API/SocketIO/UriConverters/UriConverter.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SocketIOClient.UriConverters
|
||||
{
|
||||
public class UriConverter : IUriConverter
|
||||
{
|
||||
public Uri GetServerUri(bool ws, Uri serverUri, int eio, string path, IEnumerable<KeyValuePair<string, string>> queryParams)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
if (serverUri.Scheme == "https" || serverUri.Scheme == "wss")
|
||||
{
|
||||
builder.Append(ws ? "wss://" : "https://");
|
||||
}
|
||||
else if (serverUri.Scheme == "http" || serverUri.Scheme == "ws")
|
||||
{
|
||||
builder.Append(ws ? "ws://" : "http://");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Only supports 'http, https, ws, wss' protocol");
|
||||
}
|
||||
builder.Append(serverUri.Host);
|
||||
if (!serverUri.IsDefaultPort)
|
||||
{
|
||||
builder.Append(":").Append(serverUri.Port);
|
||||
}
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
builder.Append("/socket.io");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(path);
|
||||
}
|
||||
builder
|
||||
.Append("/?EIO=")
|
||||
.Append(eio)
|
||||
.Append("&transport=")
|
||||
.Append(ws ? "websocket" : "polling");
|
||||
|
||||
if (queryParams != null)
|
||||
{
|
||||
foreach (var item in queryParams)
|
||||
{
|
||||
builder.Append('&').Append(item.Key).Append('=').Append(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return new Uri(builder.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Interfaces;
|
||||
|
||||
@@ -13,6 +14,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Add icons and context menus to the system's notification area.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class Tray : ITray
|
||||
{
|
||||
/// <summary>
|
||||
@@ -47,6 +50,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// macOS, Windows: Emitted when the tray icon is right clicked.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action<TrayClickEventArgs, Rectangle> OnRightClick
|
||||
{
|
||||
add
|
||||
@@ -76,6 +81,8 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// macOS, Windows: Emitted when the tray icon is double clicked.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
public event Action<TrayClickEventArgs, Rectangle> OnDoubleClick
|
||||
{
|
||||
add
|
||||
@@ -105,6 +112,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Windows: Emitted when the tray balloon shows.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnBalloonShow
|
||||
{
|
||||
add
|
||||
@@ -134,6 +142,7 @@ namespace ElectronNET.API
|
||||
/// <summary>
|
||||
/// Windows: Emitted when the tray balloon is clicked.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnBalloonClick
|
||||
{
|
||||
add
|
||||
@@ -164,6 +173,8 @@ namespace ElectronNET.API
|
||||
/// Windows: Emitted when the tray balloon is closed
|
||||
/// because of timeout or user manually closes it.
|
||||
/// </summary>
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public event Action OnBalloonClosed
|
||||
{
|
||||
add
|
||||
@@ -193,7 +204,7 @@ namespace ElectronNET.API
|
||||
// TODO: Implement macOS Events
|
||||
|
||||
private static Tray _tray;
|
||||
private static object _syncRoot = new object();
|
||||
private static readonly object _syncRoot = new();
|
||||
|
||||
internal Tray() { }
|
||||
|
||||
@@ -223,7 +234,7 @@ namespace ElectronNET.API
|
||||
/// The menu items.
|
||||
/// </value>
|
||||
public IReadOnlyCollection<MenuItem> MenuItems { get { return _items.AsReadOnly(); } }
|
||||
private List<MenuItem> _items = new List<MenuItem>();
|
||||
private readonly List<MenuItem> _items = new();
|
||||
|
||||
/// <summary>
|
||||
/// Shows the Traybar.
|
||||
@@ -286,6 +297,7 @@ namespace ElectronNET.API
|
||||
/// Sets the image associated with this tray icon when pressed on macOS.
|
||||
/// </summary>
|
||||
/// <param name="image"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetPressedImage(string image)
|
||||
{
|
||||
BridgeConnector.Emit("tray-setPressedImage", image);
|
||||
@@ -304,6 +316,7 @@ namespace ElectronNET.API
|
||||
/// macOS: Sets the title displayed aside of the tray icon in the status bar.
|
||||
/// </summary>
|
||||
/// <param name="title"></param>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
BridgeConnector.Emit("tray-setTitle", title);
|
||||
@@ -313,6 +326,7 @@ namespace ElectronNET.API
|
||||
/// Windows: Displays a tray balloon.
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void DisplayBalloon(DisplayBalloonOptions options)
|
||||
{
|
||||
BridgeConnector.Emit("tray-displayBalloon", options);
|
||||
@@ -354,7 +368,7 @@ namespace ElectronNET.API
|
||||
/// <param name="fn">The handler</param>
|
||||
public void Once(string eventName, Action<object> fn) => Events.Instance.Once(ModuleName, eventName, fn);
|
||||
|
||||
private JsonSerializer _jsonSerializer = new JsonSerializer()
|
||||
private readonly JsonSerializer _jsonSerializer = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user