Merge of theolivenbaum PR

This commit is contained in:
Gregor Biswanger
2022-07-28 11:51:18 +02:00
185 changed files with 6155 additions and 6135 deletions

View File

@@ -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 Chromes 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()
};

View File

@@ -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; }
}
}

View File

@@ -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.
/// Doesnt 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);
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);

View File

@@ -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 windows 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
/// doesnt care where the extra width and height are within the content viewonly 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 windows 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 windows 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 windows 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

View File

@@ -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
};
}
}

View File

@@ -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.

View File

@@ -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
};
}
}

View 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);
}
}
}

View File

@@ -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
};
}
}

View File

@@ -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

View 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;
}
}
}
}

View File

@@ -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; } }
}
}

View File

@@ -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>

View File

@@ -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; }
}
}

View File

@@ -34,5 +34,7 @@
/// The title of the url at text.
/// </summary>
public string Bookmark { get; set; }
public NativeImage? Image { get; set; }
}
}
}

View 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; }
}
}

View File

@@ -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.

View File

@@ -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>();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -1,8 +1,11 @@
namespace ElectronNET.API.Entities
using System.Runtime.Versioning;
namespace ElectronNET.API.Entities
{
/// <summary>
///
/// </summary>
[SupportedOSPlatform("macos")]
public class NotificationAction
{
/// <summary>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View 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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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.

View File

@@ -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
}
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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.

View File

@@ -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() { }

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -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.
/// Doesnt return channel from the update configuration, only if was previously set.
/// </summary>
string Channel { get; }
/// <summary>
/// Get the update channel. Not applicable for GitHub.
/// Doesnt 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.

View File

@@ -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; }
}
}

View File

@@ -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,

View File

@@ -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.");
});
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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
};
}
}

View File

@@ -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() { }

View File

@@ -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");
}
}
}
}

View 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();
}
}
}
}

View File

@@ -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() { }

View File

@@ -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);
}
}

View File

@@ -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);

View 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();
}
}
}

View 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";
}
}

View 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();
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace SocketIOClient.Extensions
{
internal static class DisposableExtensions
{
public static void TryDispose(this IDisposable disposable)
{
disposable?.Dispose();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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();
}
}
}

View 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);
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View 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.

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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 + ",";
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,16 @@
namespace SocketIOClient.Messages
{
public enum MessageType
{
Opened,
Ping = 2,
Pong,
Connected = 40,
Disconnected,
EventMessage,
AckMessage,
ErrorMessage,
BinaryMessage,
BinaryAckMessage
}
}

View 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();
}
}
}

View 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";
}
}

View 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";
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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; }
}
}

View 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 couldnt 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();
}
}
}

View 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; }
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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; }
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,8 @@
namespace SocketIOClient.Transport
{
public enum TransportMessageType
{
Text = 0,
Binary = 1
}
}

View File

@@ -0,0 +1,8 @@
namespace SocketIOClient.Transport
{
public enum TransportProtocol
{
Polling,
WebSocket
}
}

View 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();
}
}
}

View 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);
}
}

View 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());
}
}
}

View File

@@ -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