Compare commits

..

11 Commits

Author SHA1 Message Date
Florian Rappl
a0c0523c7d Change default framework version to net10.0
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-07 09:18:27 +02:00
Florian Rappl
6d94cf9538 Improved template 2026-09-07 08:52:56 +02:00
Florian Rappl
521e7c935a Removed references to electronize 2026-09-07 08:25:18 +02:00
Florian Rappl
730eacbd52 Improved templates for preview version 2026-09-04 21:06:47 +02:00
Florian Rappl
deca8166ec Added template #414 2026-09-04 20:44:55 +02:00
Florian Rappl
a04ca0b9bc Completed APIs from #956 2026-09-04 16:57:44 +02:00
Florian Rappl
fffd7dfb79 Added more missing APIs 2026-09-04 16:51:51 +02:00
Florian Rappl
98c72af3d6 Updated zoom level 2026-09-04 16:03:43 +02:00
Florian Rappl
703cf8f880 Updated to use nsis on Windows 2026-09-04 15:48:29 +02:00
Florian Rappl
248d4bd664 Updated and improved migration checks #946 2026-09-04 15:19:07 +02:00
Florian Rappl
d5da1dbc9b Fixed #1029 2026-09-04 15:05:55 +02:00
47 changed files with 1986 additions and 63 deletions

View File

@@ -3,14 +3,27 @@
## ElectronNET.Core
- Updated dependencies
- Updated `WebContents` zoom level APIs to use `double` instead of `int` (#956)
- Improved migration checks to honor custom output paths and to detect `ProjectGuid` in publish profiles (#946)
- Fixed single instance handling on macOS (#1040)
- Fixed slow socket bridge startup by binding to an explicit loopback address (#1103)
- Fixed socket bridge connection when a system proxy is configured (#1105)
- Fixed electron-builder using the host RID instead of the target RID (#1097)
- Fixed cross-compilation behavior on same platform (#1098) @epsnm
- Fixed resolution of unrelated target (#1099) @epsnm
- Fixed false alarm for `ELECTRON001` on a root `package-lock.json` (#946)
- Added `ElectronNET.Core.Templates` package with a `dotnet new electron-blazor` template (#414)
- Added `WebContents.OnZoomChanged` event (#956)
- Added `WebContents` page loading APIs `LoadFileAsync`, `IsLoadingAsync`, `IsLoadingMainFrameAsync`, `IsWaitingForResponseAsync`, `Reload`, `ReloadIgnoringCache` and `Stop` (#956)
- Added `WebContents.InsertCSSAsync` and `WebContents.RemoveInsertedCSSAsync` for dynamic CSS (#956)
- Added `WebContents` editing and selection APIs (undo, redo, cut, copy, paste, insert text, select, ...) (#956)
- Added `WebContents.FindInPageAsync`, `WebContents.StopFindInPage` and the `OnFoundInPage` event (#956)
- Added `WebContents` audio events `OnAudioStateChanged`, `OnMediaStartedPlaying` and `OnMediaPaused` (#956)
- Added `WebContents.ScrollToTop` and `WebContents.ScrollToBottom` (#956)
- Added target framework customization (#1095) @epsnm
- Added configurable Electron root directory for custom packaging layouts (#1106) @DYH1319
- Added ability for `custom_main.js` to modify command line switches (#1029) @AeonSake
- Added migration checks for incomplete `ElectronHostHook` folders (`ELECTRON010`, `ELECTRON011`) (#946)
- Added `ElectronExecutableName` to separate the product name from the executable name (#1003) @AeonSake
- Added build extensibility properties `ElectronSkipExecCommands` and `ElectronIntermediatePublishDir` (#1106) @DYH1319

View File

@@ -23,13 +23,14 @@ While the classic Electron.NET setup (using an ASP.NET host run by the Electron
* ElectronNET.Core: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.API/)
* ElectronNET.Core.API: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.API.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.API/)
* ElectronNET.Core.AspNet: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.AspNet.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.AspNet/)
* ElectronNET.Core.Templates: [![NuGet](https://img.shields.io/nuget/v/ElectronNET.Core.Templates.svg?style=flat-square)](https://www.nuget.org/packages/ElectronNET.Core.Templates/)
## 🛠 Requirements to Run
You should have installed:
* .NET 6/8 or later.
* The minimum base OS is the same as [.NET 6](https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md) / [.NET 8](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md).
* .NET 8/10 or later.
* The minimum base OS is the same as [.NET 8](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md) / [.NET 10](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md).
* Node.JS using at least [Version 22.x](https://nodejs.org).
## 👩‍🏫 Usage with ASP.NET
@@ -202,6 +203,8 @@ Alternatively, consider using a GitHub sponsorship for the core maintainers:
- [Gregor Biswanger](https://github.com/sponsors/GregorBiswanger)
- [Florian Rappl](https://github.com/sponsors/FlorianRappl)
Or just [buy Florian a coffee](https://buymeacoffee.com/florianrappl) :coffee:.
Any support appreciated! 🍻
## 🎉 License

View File

@@ -58,6 +58,23 @@ Works for both BrowserWindows and BrowserViews.
- `isBrowserWindow` - Whether the webContents belong to a BrowserWindow or not (the other option is a BrowserView)
- `path` - Absolute path to the CSS file location
#### 🧊 `Task<string> InsertCSSAsync(string css, string cssOrigin = null)`
Injects CSS into the current web page and returns a unique key for the inserted style sheet.
**Parameters:**
- `css` - The style sheet to inject
- `cssOrigin` - Can be either `user` or `author`, defaults to `author`
**Returns:**
The key of the inserted style sheet, to be used with `RemoveInsertedCSSAsync`.
#### 🧊 `Task RemoveInsertedCSSAsync(string key)`
Removes a previously inserted style sheet from the current web page.
**Parameters:**
- `key` - The key returned by `InsertCSSAsync`
#### 🧊 `Task LoadURLAsync(string url)`
Loads the url in the window. The url must contain the protocol prefix.
@@ -113,6 +130,106 @@ Prints window's web page as PDF with Chromium's preview printing custom settings
Whether the PDF generation succeeded.
#### 🧊 `Task<double> GetZoomFactorAsync()`
Returns the current zoom factor. A factor of `1.0` means 100%.
#### 🧊 `void SetZoomFactor(double factor)`
Changes the zoom factor to the specified factor. The zoom factor is the zoom percent divided by 100, so 300% = `3.0`. The factor must be greater than `0.0`.
**Parameters:**
- `factor` - The zoom factor
#### 🧊 `Task<double> GetZoomLevelAsync()`
Returns the current zoom level.
#### 🧊 `void SetZoomLevel(double level)`
Changes the zoom level to the specified level. The original size is `0` and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of the original size, respectively.
**Parameters:**
- `level` - The zoom level
#### 🧊 `Task SetVisualZoomLevelLimitsAsync(double minimumLevel, double maximumLevel)`
Sets the maximum and minimum pinch-to-zoom level.
**Parameters:**
- `minimumLevel` - The minimum pinch-to-zoom level
- `maximumLevel` - The maximum pinch-to-zoom level
#### 🧊 `Task LoadFileAsync(string filePath, LoadFileOptions options = null)`
Loads the given HTML file, relative to the root of the application.
**Parameters:**
- `filePath` - Path to the HTML file
- `options` - Optional `Query`, `Search` and `Hash` parts of the resulting URL
#### 🧊 `void SetAudioMuted(bool muted)`
Mutes or unmutes the audio on the current web page.
#### 🧊 `Task<bool> IsAudioMutedAsync()`
Whether this page has been muted.
#### 🧊 `Task<bool> IsCurrentlyAudibleAsync()`
Whether audio is currently playing.
#### 🧊 `Task<string> GetUserAgentAsync()` / `void SetUserAgent(string userAgent)`
Gets or overrides the user agent for this web page.
#### 🧊 `Task<bool> IsLoadingAsync()`
Whether the web page is still loading resources.
#### 🧊 `Task<bool> IsLoadingMainFrameAsync()`
Whether the main frame (and not just iframes or frames within it) is still loading.
#### 🧊 `Task<bool> IsWaitingForResponseAsync()`
Whether the web page is waiting for a first response from the main resource of the page.
#### 🧊 `void Reload()`
Reloads the current web page.
#### 🧊 `void ReloadIgnoringCache()`
Reloads the current web page and ignores the cache.
#### 🧊 `void Stop()`
Stops any pending navigation.
#### 🧊 `void Undo()` / `void Redo()`
Executes the `undo` / `redo` editing command in the web page.
#### 🧊 `void Cut()` / `void Copy()` / `void Paste()` / `void PasteAndMatchStyle()` / `void Delete()`
Executes the corresponding editing command in the web page.
#### 🧊 `void CopyImageAt(int x, int y)`
Copies the image at the given position to the clipboard.
#### 🧊 `void SelectAll()` / `void Unselect()` / `void CenterSelection()`
Selects all content, clears the selection, or scrolls to the current selection.
#### 🧊 `void ScrollToTop()` / `void ScrollToBottom()`
Scrolls to the top or the bottom of the current web page.
#### 🧊 `void AdjustSelection(AdjustSelectionOptions options)`
Adjusts the start and end points of the current text selection by the given amounts. Negative amounts move towards the beginning of the document.
#### 🧊 `Task InsertTextAsync(string text)`
Inserts text into the focused element.
#### 🧊 `void Replace(string text)` / `void ReplaceMisspelling(string text)`
Replaces the current selection, or the currently misspelled word, with the given text.
#### 🧊 `Task<int> FindInPageAsync(string text, FindInPageOptions options = null)`
Starts a request to find all matches for the text in the web page. Results are reported via the `OnFoundInPage` event.
**Parameters:**
- `text` - Content to be searched, must not be empty
- `options` - Optional `Forward`, `FindNext` and `MatchCase` flags
**Returns:**
The request id of the find request.
#### 🧊 `void StopFindInPage(StopFindInPageAction action)`
Stops any `FindInPageAsync` request with the given action (`ClearSelection`, `KeepSelection` or `ActivateSelection`).
## Events
#### ⚡ `InputEvent`
@@ -142,6 +259,21 @@ Emitted when the document in the top-level frame is loaded.
#### ⚡ `OnWillRedirect`
Emitted when a server side redirect occurs during navigation.
#### ⚡ `OnZoomChanged`
Emitted when the user changes the zoom level using the mouse wheel or the keyboard. The handler receives the `ZoomDirection` (`In` or `Out`).
#### ⚡ `OnFoundInPage`
Emitted when a result is available for a `FindInPageAsync` request. The handler receives a `FoundInPageResult`.
#### ⚡ `OnAudioStateChanged`
Emitted when media becomes audible or inaudible. The handler receives `true` if one or more frames or child web contents are emitting audio.
#### ⚡ `OnMediaStartedPlaying`
Emitted when media starts playing.
#### ⚡ `OnMediaPaused`
Emitted when media is paused or done playing.
## Usage Examples
### Page Loading
@@ -279,6 +411,82 @@ webContents.OnCrashed += (killed) =>
};
```
### Zoom Control
```csharp
// Set the zoom to 150%
webContents.SetZoomFactor(1.5);
var factor = await webContents.GetZoomFactorAsync();
Console.WriteLine($"Current zoom: {factor * 100}%");
// Zoom levels are relative: each step is 20% larger/smaller, 0 is the original size
webContents.SetZoomLevel(2);
// Restrict pinch-to-zoom
await webContents.SetVisualZoomLevelLimitsAsync(1, 3);
// React to zoom changes triggered by the user
webContents.OnZoomChanged += (direction) =>
{
Console.WriteLine($"User zoomed {direction}");
};
```
> **Note:** The zoom factor is shared by all windows using the same session partition. Assign a unique `WebPreferences.Partition` per window if each window should keep its own zoom.
### Dynamic CSS
```csharp
var key = await webContents.InsertCSSAsync("body { background-color: #202020; }");
// ... later
await webContents.RemoveInsertedCSSAsync(key);
```
### Editing and Selection
```csharp
await webContents.InsertTextAsync("Hello from .NET");
webContents.SelectAll();
webContents.Copy();
webContents.Unselect();
```
### Find in Page
```csharp
webContents.OnFoundInPage += (result) =>
{
Console.WriteLine($"{result.ActiveMatchOrdinal}/{result.Matches} matches");
if (result.FinalUpdate)
{
webContents.StopFindInPage(StopFindInPageAction.ClearSelection);
}
};
var requestId = await webContents.FindInPageAsync("electron", new FindInPageOptions { MatchCase = false });
```
### Audio
```csharp
webContents.SetAudioMuted(true);
var muted = await webContents.IsAudioMutedAsync();
var audible = await webContents.IsCurrentlyAudibleAsync();
webContents.OnAudioStateChanged += (isAudible) =>
{
Console.WriteLine(isAudible ? "Page started emitting audio" : "Page went silent");
};
webContents.OnMediaStartedPlaying += () => Console.WriteLine("Media playing");
webContents.OnMediaPaused += () => Console.WriteLine("Media paused");
```
## Related APIs
- [Electron.WindowManager](WindowManager.md) - Windows containing web contents

View File

@@ -17,6 +17,10 @@ When you build an Electron.NET project, the following validation checks are perf
| [ELECTRON005](#4-parent-paths-not-allowed-in-electron-builderjson) | Parent paths not allowed | Checks for `..` references in config |
| [ELECTRON006](#5-publish-profile-validation) | ASP.NET publish profile mismatch | Warns when ASP.NET projects have console-style profiles |
| [ELECTRON007](#5-publish-profile-validation) | Console publish profile mismatch | Warns when console projects have ASP.NET-style profiles |
| [ELECTRON010](#6-electronhosthook-folder-validation) | ElectronHostHook without package.json | Warns when hook TypeScript files exist without a `package.json` |
| [ELECTRON011](#6-electronhosthook-folder-validation) | ElectronHostHook without tsconfig.json | Warns when hook TypeScript files exist without a `tsconfig.json` |
All checks skip the build output folders (`bin`, `obj`, `publish`, `.electron`, `node_modules`) as well as any custom output paths configured via `BaseOutputPath` / `BaseIntermediateOutputPath` (for example the .NET `artifacts` output layout).
---
@@ -32,7 +36,7 @@ Rules:
- **ELECTRON001**: `package.json` / `package-lock.json` must not exist in the project directory or subdirectories
- Exception: `ElectronHostHook` folder is allowed
- Note: a **root** `package.json` is **excluded** from `ELECTRON001` and validated by `ELECTRON008` / `ELECTRON009`
- Note: a **root** `package.json` (and its `package-lock.json`) is **excluded** from `ELECTRON001` and validated by `ELECTRON008` / `ELECTRON009`
- **ELECTRON008**: If a root `package.json` exists, it must **not** contain electron-related dependencies or configuration.
@@ -113,6 +117,10 @@ The `electron.manifest.json` file format is deprecated. All configuration should
"mac": {
"icon": "Assets/app.icns",
"target": ["dmg", "zip"]
},
"nsis": {
"oneClick": true,
"perMachine": false
}
}
```
@@ -208,7 +216,7 @@ The build system examines `.pubxml` files in the `Properties/PublishProfiles` fo
- **ELECTRON006**: For **ASP.NET projects** (using `Microsoft.NET.Sdk.Web`), checks that publish profiles include `WebPublishMethod`. This property is required for proper ASP.NET publishing.
- **ELECTRON007**: For **console/other projects** (not using the Web SDK), checks that publish profiles do NOT include the `WebPublishMethod` property. These ASP.NET-specific properties are incorrect for non-web applications.
- **ELECTRON007**: For **console/other projects** (not using the Web SDK), checks that publish profiles do NOT include the `WebPublishMethod` or `ProjectGuid` property. These ASP.NET-specific properties are incorrect for non-web applications.
### Why this matters
@@ -233,9 +241,54 @@ For correct publish profile examples for both ASP.NET and Console applications,
---
## 6. ElectronHostHook Folder Validation
**Warning Codes:** `ELECTRON010`, `ELECTRON011`
### What is checked
If the project contains an `ElectronHostHook` folder with TypeScript files:
- **ELECTRON010**: The folder must contain a `package.json`
- **ELECTRON011**: The folder must contain a `tsconfig.json`
### Why this matters
Custom host hook code is only compiled and packaged when the `ElectronHostHook` folder declares its npm dependencies in a `package.json`. If that file is missing, the hook is silently dropped and the corresponding `Electron.HostHook` calls fail at runtime. The `tsconfig.json` is required so the hook sources are compiled with the expected settings.
### How to fix
Add the missing files to the `ElectronHostHook` folder:
```json
{
"name": "electron-host-hook",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"socket.io": "^4.8.1"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}
```
> **See also:** [HostHook API](../API/HostHook.md)
---
## Disabling Migration Checks
If you need to disable specific migration checks (not recommended), you can set the following properties in your `.csproj` file:
Individual checks can be downgraded to messages by their warning code:
```xml
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);ELECTRON005</MSBuildWarningsAsMessages>
</PropertyGroup>
```
If you need to disable all migration checks (not recommended), you can set the following property in your `.csproj` file:
```xml
<PropertyGroup>

View File

@@ -48,17 +48,19 @@ You can also manually edit `electron-builder.json`:
```json
{
"linux": {
"target": [
"tar.xz"
]
"target": ["tar.xz"]
},
"win": {
"target": [
{
"target": "portable",
"target": "nsis",
"arch": "x64"
}
]
},
"nsis": {
"oneClick": true,
"perMachine": false
}
}
```

View File

@@ -10,6 +10,11 @@ See [System Requirements](../GettingStarted/System-Requirements.md).
## 🚀 Quick Start
> [!Tip]
> To skip the manual setup below, scaffold a ready-to-run app with the
> [project templates](Templates.md): `dotnet new install ElectronNET.Core.Templates` followed by
> `dotnet new electron-blazor -n MyDesktopApp`.
### 1. Create ASP.NET Core Project
#### Visual Studio

View File

@@ -0,0 +1,88 @@
# Project Templates
Electron.NET ships a `dotnet new` template package so you can scaffold a ready-to-run desktop app instead of wiring up an ASP.NET project by hand.
## 🛠 System Requirements
See [System Requirements](../GettingStarted/System-Requirements.md).
## 📦 Install the Templates
```bash
dotnet new install ElectronNET.Core.Templates
```
To update to the latest version, run the same command again. To remove them:
```bash
dotnet new uninstall ElectronNET.Core.Templates
```
## 🚀 Available Templates
| Template | Short name | Description |
|----------|------------|-------------|
| Electron.NET Blazor App | `electron-blazor` | A Blazor Server app hosted in an Electron shell |
## 🧱 Create a Blazor App
```bash
dotnet new electron-blazor -n MyDesktopApp
cd MyDesktopApp
```
Then start it in the Electron shell:
```bash
dotnet build
dotnet run
```
### Options
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| `-f`, `--framework` | `net8.0`, `net10.0` | `net10.0` | The target framework of the generated project |
| `-e`, `--electron-version` | any Electron version | `38.2.2` | The Electron version the app is built against |
| `-p`, `--port` | port number | `8001` | The port the ASP.NET server uses during development |
| `--no-restore` | — | — | Skip the automatic `dotnet restore` |
Example:
```bash
dotnet new electron-blazor -n MyDesktopApp -f net10.0 -e 30.4.0 -p 8123
```
## 📁 What You Get
```
MyDesktopApp/
├── Components/
│ ├── Layout/ MainLayout.razor, NavMenu.razor
│ ├── Pages/ Home.razor, Counter.razor
│ ├── App.razor
│ ├── Routes.razor
│ └── _Imports.razor
├── Properties/
│ ├── PublishProfiles/ win-x64, linux-x64 and osx-arm64 folder profiles
│ ├── electron-builder.json
│ └── launchSettings.json
├── wwwroot/app.css
├── appsettings.json
├── Program.cs
└── MyDesktopApp.csproj
```
The project already references `ElectronNET.Core` and `ElectronNET.Core.AspNet`, calls `builder.UseElectron(...)` in `Program.cs`, and passes all [Migration Checks](../Core/Migration-Checks.md) out of the box.
> [!Note]
> `ElectronNET.API` also defines a type named `App`, which collides with the Blazor root
> component. That is why the template calls `app.MapRazorComponents<MyDesktopApp.Components.App>()`
> with a fully qualified type name.
## 🚀 Next Steps
- **[Configuration](../Using/Configuration.md)** - Adjust app metadata and Electron settings
- **[Debugging](../Using/Debugging.md)** - Debug the .NET and Electron sides
- **[Package Building](../Using/Package-Building.md)** - Create distributable packages

View File

@@ -7,6 +7,7 @@ This guide explains how to include and use a `custom_main.js` file in your Elect
- Register custom protocol handlers (e.g., `myapp://`) — protocols must be registered before the app is fully initialized
- Integrate Node.js modules (e.g., telemetry, OS APIs)
- Control startup logic (abort, environment checks)
- Modify Chromium / Electron.NET command line switches before they are evaluated
- Set up IPC messaging or preload scripts
## Step-by-Step Process
@@ -65,10 +66,29 @@ Use environment variables to control features:
if (env === 'Development') { /* enable dev features */ }
```
### Modifying Command Line Switches
`onStartup` is invoked before Electron.NET reads any of its own switches (`manifest`, `unpackedelectron`, `unpackeddotnet`, `dotnetpacked`, `electronforcedport`, `electronurl`), so the hook can append or override them:
```javascript
module.exports.onStartup = function (host) {
const { app } = require('electron');
// Force a fixed socket bridge port
app.commandLine.appendSwitch('electronforcedport', '8000');
// Chromium switches, e.g. to disable the GPU sandbox
app.commandLine.appendSwitch('disable-gpu-sandbox');
return true;
};
```
## Notes
- `custom_main.js` must use CommonJS syntax (`module.exports.onStartup = ...`).
- Place the file in your source directory and copy it to `.electron` using `.csproj`.
- Electron.NET will abort startup if `onStartup` returns `false`.
- `onStartup` runs before the Electron.NET command line switches are evaluated, so switches set there are picked up by the host.
### Complete example is available here [ElectronNetSampleApp](https://github.com/niteshsinghal85/ElectronNetSampleApp)

View File

@@ -15,6 +15,7 @@
# Getting Started
- [System Requirements](GettingStarted/System-Requirements.md)
- [Project Templates](GettingStarted/Templates.md)
- [With ASP.Net](GettingStarted/ASP.Net.md)
- [With a Console App](GettingStarted/Console-App.md)

View File

@@ -0,0 +1,18 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for <see cref="WebContents.AdjustSelection"/>.
/// </summary>
public class AdjustSelectionOptions
{
/// <summary>
/// Amount to shift the start index of the current selection.
/// </summary>
public int? Start { get; set; }
/// <summary>
/// Amount to shift the end index of the current selection.
/// </summary>
public int? End { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for <see cref="WebContents.FindInPageAsync"/>.
/// </summary>
public class FindInPageOptions
{
/// <summary>
/// Whether to search forward or backward, defaults to true.
/// </summary>
public bool? Forward { get; set; }
/// <summary>
/// Whether to begin a new text finding session with this request. Should be true
/// for initial requests, and false for subsequent requests. Defaults to false.
/// </summary>
public bool? FindNext { get; set; }
/// <summary>
/// Whether search should be case-sensitive, defaults to false.
/// </summary>
public bool? MatchCase { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// The result of a text finding session, reported by the found-in-page event.
/// </summary>
public class FoundInPageResult
{
/// <summary>
/// The identifier of the request returned by FindInPageAsync.
/// </summary>
public int RequestId { get; set; }
/// <summary>
/// Position of the active match.
/// </summary>
public int ActiveMatchOrdinal { get; set; }
/// <summary>
/// Number of matches.
/// </summary>
public int Matches { get; set; }
/// <summary>
/// Coordinates of the first match region.
/// </summary>
public Rectangle SelectionArea { get; set; }
/// <summary>
/// Indicates whether more responses are to follow.
/// </summary>
public bool FinalUpdate { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for loading a local file into a web page.
/// </summary>
public class LoadFileOptions
{
/// <summary>
/// Passed to url.format().
/// </summary>
public Dictionary<string, string> Query { get; set; }
/// <summary>
/// Passed to url.format().
/// </summary>
public string Search { get; set; }
/// <summary>
/// Passed to url.format().
/// </summary>
public string Hash { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Determines what happens to the selection when a text finding session is stopped.
/// </summary>
public enum StopFindInPageAction
{
/// <summary>
/// Clear the selection.
/// </summary>
ClearSelection,
/// <summary>
/// Translate the selection into a normal selection.
/// </summary>
KeepSelection,
/// <summary>
/// Focus and click the selection node.
/// </summary>
ActivateSelection
}
}

View File

@@ -0,0 +1,18 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// The direction of a zoom change triggered by the user.
/// </summary>
public enum ZoomDirection
{
/// <summary>
/// The user zoomed in.
/// </summary>
In,
/// <summary>
/// The user zoomed out.
/// </summary>
Out
}
}

View File

@@ -113,6 +113,52 @@ public class WebContents : ApiBase
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when the user is requesting to change the zoom level using the mouse wheel or the keyboard.
/// </summary>
public event Action<ZoomDirection> OnZoomChanged
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when a result is available for a <see cref="FindInPageAsync(string, FindInPageOptions)"/> request.
/// </summary>
public event Action<FoundInPageResult> OnFoundInPage
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when media becomes audible or inaudible. The parameter is true if one or more
/// frames or child web contents are emitting audio.
/// </summary>
public event Action<bool> OnAudioStateChanged
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when media starts playing.
/// </summary>
public event Action OnMediaStartedPlaying
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
/// <summary>
/// Emitted when media is paused or done playing.
/// </summary>
public event Action OnMediaPaused
{
add => AddEvent(value, Id);
remove => RemoveEvent(value, Id);
}
internal WebContents(int id)
{
Id = id;
@@ -304,6 +350,68 @@ public class WebContents : ApiBase
return tcs.Task;
}
/// <summary>
/// Loads the given file in the window. The file path must be a path to an HTML file
/// relative to the root of your application.
/// </summary>
/// <param name="filePath">Path to the HTML file.</param>
/// <param name="options">Optional query, search and hash parts of the resulting URL.</param>
public Task LoadFileAsync(string filePath, LoadFileOptions options = null)
{
var tcs = new TaskCompletionSource();
BridgeConnector.Socket.Once("webContents-loadFile-complete" + Id, () =>
{
BridgeConnector.Socket.Off("webContents-loadFile-error" + Id);
tcs.SetResult();
});
BridgeConnector.Socket.Once<string>("webContents-loadFile-error" + Id, (error) => { tcs.SetException(new InvalidOperationException(error)); });
BridgeConnector.Socket.Emit("webContents-loadFile", Id, filePath, options);
return tcs.Task;
}
/// <summary>
/// Returns boolean - Whether web page is still loading resources.
/// </summary>
public Task<bool> IsLoadingAsync() => InvokeAsync<bool>();
/// <summary>
/// Returns boolean - Whether the main frame (and not just iframes or frames within it) is still loading.
/// </summary>
public Task<bool> IsLoadingMainFrameAsync() => InvokeAsync<bool>();
/// <summary>
/// Returns boolean - Whether the web page is waiting for a first-response from the main resource of the page.
/// </summary>
public Task<bool> IsWaitingForResponseAsync() => InvokeAsync<bool>();
/// <summary>
/// Reloads the current web page.
/// </summary>
public void Reload()
{
BridgeConnector.Socket.Emit("webContents-reload", Id);
}
/// <summary>
/// Reloads the current page and ignores cache.
/// </summary>
public void ReloadIgnoringCache()
{
BridgeConnector.Socket.Emit("webContents-reloadIgnoringCache", Id);
}
/// <summary>
/// Stops any pending navigation.
/// </summary>
public void Stop()
{
BridgeConnector.Socket.Emit("webContents-stop", Id);
}
/// <summary>
/// Inserts CSS into the web page.
/// See: https://www.electronjs.org/docs/api/web-contents#contentsinsertcsscss-options
@@ -316,6 +424,37 @@ public class WebContents : ApiBase
BridgeConnector.Socket.Emit("webContents-insertCSS", Id, isBrowserWindow, path);
}
/// <summary>
/// Injects CSS into the current web page and returns a unique key for the inserted
/// stylesheet, which can be used with <see cref="RemoveInsertedCSSAsync"/>.
/// </summary>
/// <param name="css">The style sheet to inject.</param>
/// <param name="cssOrigin">Can be either 'user' or 'author'. Defaults to 'author'.</param>
/// <returns>The key of the inserted style sheet.</returns>
public Task<string> InsertCSSAsync(string css, string cssOrigin = null)
{
var tcs = new TaskCompletionSource<string>();
BridgeConnector.Socket.Once<string>("webContents-insertCSSText-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-insertCSSText", Id, css, cssOrigin);
return tcs.Task;
}
/// <summary>
/// Removes the inserted CSS from the current web page.
/// </summary>
/// <param name="key">The key returned by <see cref="InsertCSSAsync"/>.</param>
public Task RemoveInsertedCSSAsync(string key)
{
var tcs = new TaskCompletionSource();
BridgeConnector.Socket.Once("webContents-removeInsertedCSS-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-removeInsertedCSS", Id, key);
return tcs.Task;
}
/// <summary>
/// Returns number - The current zoom factor.
/// </summary>
@@ -337,14 +476,14 @@ public class WebContents : ApiBase
/// Returns number - The current zoom level.
/// </summary>
/// <returns></returns>
public Task<int> GetZoomLevelAsync() => InvokeAsync<int>();
public Task<double> GetZoomLevelAsync() => InvokeAsync<double>();
/// <summary>
/// Changes the zoom level to the specified level.
/// The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits of 300% and 50% of original size, respectively.
/// </summary>
/// <param name="level"></param>
public void SetZoomLevel(int level)
public void SetZoomLevel(double level)
{
BridgeConnector.Socket.Emit("webContents-setZoomLevel", Id, level);
}
@@ -354,7 +493,7 @@ public class WebContents : ApiBase
/// </summary>
/// <param name="minimumLevel"></param>
/// <param name="maximumLevel"></param>
public Task SetVisualZoomLevelLimitsAsync(int minimumLevel, int maximumLevel)
public Task SetVisualZoomLevelLimitsAsync(double minimumLevel, double maximumLevel)
{
var tcs = new TaskCompletionSource();
@@ -399,4 +538,178 @@ public class WebContents : ApiBase
{
BridgeConnector.Socket.Emit("webContents-setUserAgent", Id, userAgent);
}
/// <summary>
/// Executes the editing command undo in web page.
/// </summary>
public void Undo()
{
BridgeConnector.Socket.Emit("webContents-undo", Id);
}
/// <summary>
/// Executes the editing command redo in web page.
/// </summary>
public void Redo()
{
BridgeConnector.Socket.Emit("webContents-redo", Id);
}
/// <summary>
/// Executes the editing command cut in web page.
/// </summary>
public void Cut()
{
BridgeConnector.Socket.Emit("webContents-cut", Id);
}
/// <summary>
/// Executes the editing command copy in web page.
/// </summary>
public void Copy()
{
BridgeConnector.Socket.Emit("webContents-copy", Id);
}
/// <summary>
/// Copies the image at the given position to the clipboard.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public void CopyImageAt(int x, int y)
{
BridgeConnector.Socket.Emit("webContents-copyImageAt", Id, x, y);
}
/// <summary>
/// Executes the editing command paste in web page.
/// </summary>
public void Paste()
{
BridgeConnector.Socket.Emit("webContents-paste", Id);
}
/// <summary>
/// Executes the editing command pasteAndMatchStyle in web page.
/// </summary>
public void PasteAndMatchStyle()
{
BridgeConnector.Socket.Emit("webContents-pasteAndMatchStyle", Id);
}
/// <summary>
/// Executes the editing command delete in web page.
/// </summary>
public void Delete()
{
BridgeConnector.Socket.Emit("webContents-delete", Id);
}
/// <summary>
/// Executes the editing command selectAll in web page.
/// </summary>
public void SelectAll()
{
BridgeConnector.Socket.Emit("webContents-selectAll", Id);
}
/// <summary>
/// Executes the editing command unselect in web page.
/// </summary>
public void Unselect()
{
BridgeConnector.Socket.Emit("webContents-unselect", Id);
}
/// <summary>
/// Adjusts the current text selection starting and ending points by the given amounts.
/// A negative amount moves the selection towards the beginning of the document.
/// </summary>
/// <param name="options"></param>
public void AdjustSelection(AdjustSelectionOptions options)
{
BridgeConnector.Socket.Emit("webContents-adjustSelection", Id, options);
}
/// <summary>
/// Scrolls to the current text selection.
/// </summary>
public void CenterSelection()
{
BridgeConnector.Socket.Emit("webContents-centerSelection", Id);
}
/// <summary>
/// Scrolls to the top of the current web page.
/// </summary>
public void ScrollToTop()
{
BridgeConnector.Socket.Emit("webContents-scrollToTop", Id);
}
/// <summary>
/// Scrolls to the bottom of the current web page.
/// </summary>
public void ScrollToBottom()
{
BridgeConnector.Socket.Emit("webContents-scrollToBottom", Id);
}
/// <summary>
/// Inserts text to the focused element.
/// </summary>
/// <param name="text">The text to be inserted.</param>
public Task InsertTextAsync(string text)
{
var tcs = new TaskCompletionSource();
BridgeConnector.Socket.Once("webContents-insertText-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-insertText", Id, text);
return tcs.Task;
}
/// <summary>
/// Executes the editing command replace in web page.
/// </summary>
/// <param name="text"></param>
public void Replace(string text)
{
BridgeConnector.Socket.Emit("webContents-replace", Id, text);
}
/// <summary>
/// Executes the editing command replaceMisspelling in web page.
/// </summary>
/// <param name="text"></param>
public void ReplaceMisspelling(string text)
{
BridgeConnector.Socket.Emit("webContents-replaceMisspelling", Id, text);
}
/// <summary>
/// Starts a request to find all matches for the text in the web page.
/// The result of the request can be obtained by subscribing to the <see cref="OnFoundInPage"/> event.
/// </summary>
/// <param name="text">Content to be searched, must not be empty.</param>
/// <param name="options"></param>
/// <returns>The request id used for the request.</returns>
public Task<int> FindInPageAsync(string text, FindInPageOptions options = null)
{
var tcs = new TaskCompletionSource<int>();
BridgeConnector.Socket.Once<int>("webContents-findInPage-completed" + Id, tcs.SetResult);
BridgeConnector.Socket.Emit("webContents-findInPage", Id, text, options);
return tcs.Task;
}
/// <summary>
/// Stops any findInPage request for the web contents with the provided action.
/// </summary>
/// <param name="action"></param>
public void StopFindInPage(StopFindInPageAction action)
{
BridgeConnector.Socket.Emit("webContents-stopFindInPage", Id, action);
}
}

View File

@@ -111,6 +111,41 @@ module.exports = (socket) => {
electronSocket.emit("webContents-domReady" + id);
});
});
socket.on("register-webContents-zoomChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("zoom-changed");
browserWindow.webContents.on("zoom-changed", (_, zoomDirection) => {
electronSocket.emit("webContents-zoomChanged" + id, zoomDirection);
});
});
socket.on("register-webContents-foundInPage", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("found-in-page");
browserWindow.webContents.on("found-in-page", (_, result) => {
electronSocket.emit("webContents-foundInPage" + id, result);
});
});
socket.on("register-webContents-audioStateChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("audio-state-changed");
browserWindow.webContents.on("audio-state-changed", (event) => {
electronSocket.emit("webContents-audioStateChanged" + id, event.audible);
});
});
socket.on("register-webContents-mediaStartedPlaying", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-started-playing");
browserWindow.webContents.on("media-started-playing", () => {
electronSocket.emit("webContents-mediaStartedPlaying" + id);
});
});
socket.on("register-webContents-mediaPaused", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-paused");
browserWindow.webContents.on("media-paused", () => {
electronSocket.emit("webContents-mediaPaused" + id);
});
});
socket.on("webContents-openDevTools", (id, options) => {
if (options) {
getWindowById(id).webContents.openDevTools(options);
@@ -308,6 +343,36 @@ module.exports = (socket) => {
electronSocket.emit("webContents-loadURL-error" + id, error);
});
});
socket.on("webContents-loadFile", (id, filePath, options) => {
const browserWindow = getWindowById(id);
browserWindow.webContents
.loadFile(filePath, options ?? undefined)
.then(() => {
electronSocket.emit("webContents-loadFile-complete" + id);
})
.catch((error) => {
console.error(error);
electronSocket.emit("webContents-loadFile-error" + id, error);
});
});
socket.on("webContents-isLoading", (id) => {
electronSocket.emit("webContents-isLoading-completed", getWindowById(id).webContents.isLoading());
});
socket.on("webContents-isLoadingMainFrame", (id) => {
electronSocket.emit("webContents-isLoadingMainFrame-completed", getWindowById(id).webContents.isLoadingMainFrame());
});
socket.on("webContents-isWaitingForResponse", (id) => {
electronSocket.emit("webContents-isWaitingForResponse-completed", getWindowById(id).webContents.isWaitingForResponse());
});
socket.on("webContents-reload", (id) => {
getWindowById(id).webContents.reload();
});
socket.on("webContents-reloadIgnoringCache", (id) => {
getWindowById(id).webContents.reloadIgnoringCache();
});
socket.on("webContents-stop", (id) => {
getWindowById(id).webContents.stop();
});
socket.on("webContents-insertCSS", (id, isBrowserWindow, path) => {
if (isBrowserWindow) {
const browserWindow = getWindowById(id);
@@ -330,6 +395,14 @@ module.exports = (socket) => {
}
}
});
socket.on("webContents-insertCSSText", async (id, css, cssOrigin) => {
const key = await getWindowById(id).webContents.insertCSS(css, cssOrigin ? { cssOrigin } : undefined);
electronSocket.emit("webContents-insertCSSText-completed" + id, key);
});
socket.on("webContents-removeInsertedCSS", async (id, key) => {
await getWindowById(id).webContents.removeInsertedCSS(key);
electronSocket.emit("webContents-removeInsertedCSS-completed" + id);
});
socket.on("webContents-session-getAllExtensions", (id) => {
const browserWindow = getWindowById(id);
const extensionsList = browserWindow.webContents.session.getAllExtensions();
@@ -403,6 +476,65 @@ module.exports = (socket) => {
socket.on("webContents-setUserAgent", (id, userAgent) => {
getWindowById(id).webContents.setUserAgent(userAgent);
});
socket.on("webContents-undo", (id) => {
getWindowById(id).webContents.undo();
});
socket.on("webContents-redo", (id) => {
getWindowById(id).webContents.redo();
});
socket.on("webContents-cut", (id) => {
getWindowById(id).webContents.cut();
});
socket.on("webContents-copy", (id) => {
getWindowById(id).webContents.copy();
});
socket.on("webContents-copyImageAt", (id, x, y) => {
getWindowById(id).webContents.copyImageAt(x, y);
});
socket.on("webContents-paste", (id) => {
getWindowById(id).webContents.paste();
});
socket.on("webContents-pasteAndMatchStyle", (id) => {
getWindowById(id).webContents.pasteAndMatchStyle();
});
socket.on("webContents-delete", (id) => {
getWindowById(id).webContents.delete();
});
socket.on("webContents-selectAll", (id) => {
getWindowById(id).webContents.selectAll();
});
socket.on("webContents-unselect", (id) => {
getWindowById(id).webContents.unselect();
});
socket.on("webContents-adjustSelection", (id, options) => {
getWindowById(id).webContents.adjustSelection(options ?? {});
});
socket.on("webContents-centerSelection", (id) => {
getWindowById(id).webContents.centerSelection();
});
socket.on("webContents-scrollToTop", (id) => {
getWindowById(id).webContents.scrollToTop();
});
socket.on("webContents-scrollToBottom", (id) => {
getWindowById(id).webContents.scrollToBottom();
});
socket.on("webContents-insertText", async (id, text) => {
await getWindowById(id).webContents.insertText(text);
electronSocket.emit("webContents-insertText-completed" + id);
});
socket.on("webContents-replace", (id, text) => {
getWindowById(id).webContents.replace(text);
});
socket.on("webContents-replaceMisspelling", (id, text) => {
getWindowById(id).webContents.replaceMisspelling(text);
});
socket.on("webContents-findInPage", (id, text, options) => {
const requestId = getWindowById(id).webContents.findInPage(text, options ?? undefined);
electronSocket.emit("webContents-findInPage-completed" + id, requestId);
});
socket.on("webContents-stopFindInPage", (id, action) => {
getWindowById(id).webContents.stopFindInPage(action);
});
function getWindowById(id) {
if (id >= 1000) {
return (0, browserView_1.browserViewMediateService)(id - 1000);

File diff suppressed because one or more lines are too long

View File

@@ -103,6 +103,51 @@ export = (socket: Socket) => {
});
});
socket.on("register-webContents-zoomChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("zoom-changed");
browserWindow.webContents.on("zoom-changed", (_, zoomDirection) => {
electronSocket.emit("webContents-zoomChanged" + id, zoomDirection);
});
});
socket.on("register-webContents-foundInPage", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("found-in-page");
browserWindow.webContents.on("found-in-page", (_, result) => {
electronSocket.emit("webContents-foundInPage" + id, result);
});
});
socket.on("register-webContents-audioStateChanged", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("audio-state-changed");
browserWindow.webContents.on("audio-state-changed", (event) => {
electronSocket.emit("webContents-audioStateChanged" + id, event.audible);
});
});
socket.on("register-webContents-mediaStartedPlaying", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-started-playing");
browserWindow.webContents.on("media-started-playing", () => {
electronSocket.emit("webContents-mediaStartedPlaying" + id);
});
});
socket.on("register-webContents-mediaPaused", (id) => {
const browserWindow = getWindowById(id);
browserWindow.webContents.removeAllListeners("media-paused");
browserWindow.webContents.on("media-paused", () => {
electronSocket.emit("webContents-mediaPaused" + id);
});
});
socket.on("webContents-openDevTools", (id, options) => {
if (options) {
getWindowById(id).webContents.openDevTools(options);
@@ -409,6 +454,52 @@ export = (socket: Socket) => {
});
});
socket.on("webContents-loadFile", (id, filePath, options) => {
const browserWindow = getWindowById(id);
browserWindow.webContents
.loadFile(filePath, options ?? undefined)
.then(() => {
electronSocket.emit("webContents-loadFile-complete" + id);
})
.catch((error) => {
console.error(error);
electronSocket.emit("webContents-loadFile-error" + id, error);
});
});
socket.on("webContents-isLoading", (id) => {
electronSocket.emit(
"webContents-isLoading-completed",
getWindowById(id).webContents.isLoading(),
);
});
socket.on("webContents-isLoadingMainFrame", (id) => {
electronSocket.emit(
"webContents-isLoadingMainFrame-completed",
getWindowById(id).webContents.isLoadingMainFrame(),
);
});
socket.on("webContents-isWaitingForResponse", (id) => {
electronSocket.emit(
"webContents-isWaitingForResponse-completed",
getWindowById(id).webContents.isWaitingForResponse(),
);
});
socket.on("webContents-reload", (id) => {
getWindowById(id).webContents.reload();
});
socket.on("webContents-reloadIgnoringCache", (id) => {
getWindowById(id).webContents.reloadIgnoringCache();
});
socket.on("webContents-stop", (id) => {
getWindowById(id).webContents.stop();
});
socket.on("webContents-insertCSS", (id, isBrowserWindow, path) => {
if (isBrowserWindow) {
const browserWindow = getWindowById(id);
@@ -431,6 +522,19 @@ export = (socket: Socket) => {
}
});
socket.on("webContents-insertCSSText", async (id, css, cssOrigin) => {
const key = await getWindowById(id).webContents.insertCSS(
css,
cssOrigin ? { cssOrigin } : undefined,
);
electronSocket.emit("webContents-insertCSSText-completed" + id, key);
});
socket.on("webContents-removeInsertedCSS", async (id, key) => {
await getWindowById(id).webContents.removeInsertedCSS(key);
electronSocket.emit("webContents-removeInsertedCSS-completed" + id);
});
socket.on("webContents-session-getAllExtensions", (id) => {
const browserWindow = getWindowById(id);
const extensionsList = browserWindow.webContents.session.getAllExtensions();
@@ -557,6 +661,87 @@ export = (socket: Socket) => {
getWindowById(id).webContents.setUserAgent(userAgent);
});
socket.on("webContents-undo", (id) => {
getWindowById(id).webContents.undo();
});
socket.on("webContents-redo", (id) => {
getWindowById(id).webContents.redo();
});
socket.on("webContents-cut", (id) => {
getWindowById(id).webContents.cut();
});
socket.on("webContents-copy", (id) => {
getWindowById(id).webContents.copy();
});
socket.on("webContents-copyImageAt", (id, x, y) => {
getWindowById(id).webContents.copyImageAt(x, y);
});
socket.on("webContents-paste", (id) => {
getWindowById(id).webContents.paste();
});
socket.on("webContents-pasteAndMatchStyle", (id) => {
getWindowById(id).webContents.pasteAndMatchStyle();
});
socket.on("webContents-delete", (id) => {
getWindowById(id).webContents.delete();
});
socket.on("webContents-selectAll", (id) => {
getWindowById(id).webContents.selectAll();
});
socket.on("webContents-unselect", (id) => {
getWindowById(id).webContents.unselect();
});
socket.on("webContents-adjustSelection", (id, options) => {
getWindowById(id).webContents.adjustSelection(options ?? {});
});
socket.on("webContents-centerSelection", (id) => {
getWindowById(id).webContents.centerSelection();
});
socket.on("webContents-scrollToTop", (id) => {
getWindowById(id).webContents.scrollToTop();
});
socket.on("webContents-scrollToBottom", (id) => {
getWindowById(id).webContents.scrollToBottom();
});
socket.on("webContents-insertText", async (id, text) => {
await getWindowById(id).webContents.insertText(text);
electronSocket.emit("webContents-insertText-completed" + id);
});
socket.on("webContents-replace", (id, text) => {
getWindowById(id).webContents.replace(text);
});
socket.on("webContents-replaceMisspelling", (id, text) => {
getWindowById(id).webContents.replaceMisspelling(text);
});
socket.on("webContents-findInPage", (id, text, options) => {
const requestId = getWindowById(id).webContents.findInPage(
text,
options ?? undefined,
);
electronSocket.emit("webContents-findInPage-completed" + id, requestId);
});
socket.on("webContents-stopFindInPage", (id, action) => {
getWindowById(id).webContents.stopFindInPage(action);
});
function getWindowById(
id: number,
): Electron.BrowserWindow | Electron.BrowserView {

View File

@@ -29,32 +29,11 @@ let electronforcedport;
let electronUrl;
let authToken = randomUUID().split('-').join('');
if (app.commandLine.hasSwitch('manifest')) {
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
}
if (app.commandLine.hasSwitch('unpackedelectron')) {
unpackedelectron = true;
}
else if (app.commandLine.hasSwitch('unpackeddotnet')) {
unpackeddotnet = true;
}
else if (app.commandLine.hasSwitch('dotnetpacked')) {
dotnetpacked = true;
}
if (app.commandLine.hasSwitch('electronforcedport')) {
electronforcedport = +app.commandLine.getSwitchValue('electronforcedport');
}
// Store in global for access by browser windows
global.authToken = authToken;
if (app.commandLine.hasSwitch('electronurl')) {
electronUrl = app.commandLine.getSwitchValue('electronurl');
}
// Custom startup hook: look for custom_main.js and invoke its onStartup(host) if present.
// Runs before any command line switch is evaluated so the hook can still modify them.
// If the hook returns false, abort Electron startup.
try {
const fs = require('fs');
@@ -77,6 +56,28 @@ try {
console.error('Error while executing custom_main.js:', err);
}
if (app.commandLine.hasSwitch('manifest')) {
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
}
if (app.commandLine.hasSwitch('unpackedelectron')) {
unpackedelectron = true;
}
else if (app.commandLine.hasSwitch('unpackeddotnet')) {
unpackeddotnet = true;
}
else if (app.commandLine.hasSwitch('dotnetpacked')) {
dotnetpacked = true;
}
if (app.commandLine.hasSwitch('electronforcedport')) {
electronforcedport = +app.commandLine.getSwitchValue('electronforcedport');
}
if (app.commandLine.hasSwitch('electronurl')) {
electronUrl = app.commandLine.getSwitchValue('electronurl');
}
const currentPath = __dirname;
let currentBinPath = path.join(currentPath.replace('app.asar', ''), 'bin');
let manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);

View File

@@ -145,6 +145,111 @@ public class MigrationChecksTargetsTests
}
}
[Fact]
public async Task MigrationChecksTargets_BuildWithRootPackageLockJson_ShouldNotEmitELECTRON001Warning()
{
// A root package.json is validated by ELECTRON008/ELECTRON009 and therefore allowed.
// Its accompanying package-lock.json must not be reported by ELECTRON001.
var tempDir = CreateTempProjectDirectory();
try
{
await File.WriteAllTextAsync(
Path.Combine(tempDir, "package.json"),
"""{ "devDependencies": { "vite": "^5.0.0" } }""");
await File.WriteAllTextAsync(
Path.Combine(tempDir, "package-lock.json"),
"""{ "lockfileVersion": 3 }""");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0, $"Full build output:\n{output}");
output.Should().NotContain(
"ELECTRON001",
$"a root package-lock.json belongs to the allowed root package.json. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task MigrationChecksTargets_BuildWithIncompleteHostHookFolder_ShouldEmitELECTRON010AndELECTRON011Warnings()
{
// Host hook TypeScript sources are silently ignored when package.json/tsconfig.json
// are missing, so the migration checks must point that out.
var tempDir = CreateTempProjectDirectory();
try
{
var hookDir = Path.Combine(tempDir, "ElectronHostHook");
Directory.CreateDirectory(hookDir);
await File.WriteAllTextAsync(Path.Combine(hookDir, "index.ts"), "export class HookService {}");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0, $"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON010",
$"the ElectronHostHook folder has TypeScript files but no package.json. " +
$"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON011",
$"the ElectronHostHook folder has TypeScript files but no tsconfig.json. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task MigrationChecksTargets_BuildWithConsoleProfileContainingProjectGuid_ShouldEmitELECTRON007Warning()
{
// ProjectGuid is an ASP.NET publish profile property and must be reported for
// non-web projects, just like WebPublishMethod.
var tempDir = CreateTempProjectDirectory();
try
{
var profilesDir = Path.Combine(tempDir, "Properties", "PublishProfiles");
Directory.CreateDirectory(profilesDir);
await File.WriteAllTextAsync(
Path.Combine(profilesDir, "FolderProfile.pubxml"),
"""
<Project>
<PropertyGroup>
<ProjectGuid>00000000-0000-0000-0000-000000000000</ProjectGuid>
</PropertyGroup>
</Project>
""");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0, $"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON007",
$"a console project must not use an ASP.NET publish profile. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------

View File

@@ -108,13 +108,19 @@ namespace ElectronNET.IntegrationTests.Tests
await Task.Delay(500.ms());
var ok = await window.WebContents.GetZoomLevelAsync();
ok.Should().Be(0);
ok.Should().Be(0.0);
window.WebContents.SetZoomLevel(2);
await Task.Delay(500.ms());
ok = await window.WebContents.GetZoomLevelAsync();
ok.Should().Be(2);
ok.Should().Be(2.0);
window.WebContents.SetZoomLevel(0.5);
await Task.Delay(500.ms());
ok = await window.WebContents.GetZoomLevelAsync();
ok.Should().BeApproximately(0.5, 0.001);
}
finally
{
@@ -193,5 +199,94 @@ namespace ElectronNET.IntegrationTests.Tests
}
}
[IntegrationFact]
public async Task InsertAndRemoveCSS_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><p>CSS Test</p></body></html>");
var key = await wc.InsertCSSAsync("body { background-color: rgb(255, 0, 0); }");
key.Should().NotBeNullOrEmpty();
var color = await wc.ExecuteJavaScriptAsync<string>("getComputedStyle(document.body).backgroundColor");
color.Should().Be("rgb(255, 0, 0)");
await wc.RemoveInsertedCSSAsync(key);
color = await wc.ExecuteJavaScriptAsync<string>("getComputedStyle(document.body).backgroundColor");
color.Should().NotBe("rgb(255, 0, 0)");
}
[IntegrationFact]
public async Task EditCommands_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><input id='in' autofocus /></body></html>");
await Task.Delay(500.ms());
await wc.InsertTextAsync("Electron.NET");
await Task.Delay(500.ms());
var value = await wc.ExecuteJavaScriptAsync<string>("document.getElementById('in').value");
value.Should().Be("Electron.NET");
wc.SelectAll();
wc.Delete();
await Task.Delay(500.ms());
value = await wc.ExecuteJavaScriptAsync<string>("document.getElementById('in').value");
value.Should().BeEmpty();
}
[IntegrationFact]
public async Task FindInPage_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><p>needle in a haystack</p></body></html>");
await Task.Delay(500.ms());
var tcs = new TaskCompletionSource<FoundInPageResult>();
void OnFound(FoundInPageResult result) => tcs.TrySetResult(result);
wc.OnFoundInPage += OnFound;
try
{
var requestId = await wc.FindInPageAsync("needle");
requestId.Should().BeGreaterThan(0);
var completed = await Task.WhenAny(tcs.Task, Task.Delay(5.seconds()));
completed.Should().Be(tcs.Task);
var result = await tcs.Task;
result.RequestId.Should().Be(requestId);
result.Matches.Should().BeGreaterThan(0);
}
finally
{
wc.OnFoundInPage -= OnFound;
wc.StopFindInPage(StopFindInPageAction.ClearSelection);
}
}
[IntegrationFact]
public async Task Reload_and_loading_state_check()
{
var wc = this.MainWindow.WebContents;
await wc.LoadURLAsync("data:text/html,<html><body><h1>Reload Test</h1></body></html>");
await Task.Delay(500.ms());
(await wc.IsLoadingAsync()).Should().BeFalse();
(await wc.IsLoadingMainFrameAsync()).Should().BeFalse();
(await wc.IsWaitingForResponseAsync()).Should().BeFalse();
wc.Reload();
await Task.Delay(1.seconds());
var title = await wc.ExecuteJavaScriptAsync<string>("document.querySelector('h1').textContent");
title.Should().Be("Reload Test");
}
}
}

View File

@@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "!Config", "!Config", "{02EA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.AspNet", "ElectronNET.AspNet\ElectronNET.AspNet.csproj", "{DD10D21A-D131-1D9C-33F9-406046E0C5B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.Templates", "ElectronNET.Templates\ElectronNET.Templates.csproj", "{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{1BB6F634-2831-4496-83A6-BC6761DCEC8D}"
EndProject
Global
@@ -45,6 +47,10 @@ Global
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.Build.0 = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -54,6 +60,7 @@ Global
{829FC339-4785-4229-ABA5-53ADB544DA00} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{8860606D-6847-F22A-5AED-DF4E0984DD24} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{DD10D21A-D131-1D9C-33F9-406046E0C5B0} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {81A62E71-9E04-4EFE-AD5C-23165375F8EF}

View File

@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageOutputPath>..\..\artifacts</PackageOutputPath>
<PackageId>$(PackageNamePrefix).Templates</PackageId>
<Title>$(PackageId)</Title>
<Description>$(DescriptionFirstPart) This package contains the 'dotnet new' project templates.</Description>
<PackageTags>electron aspnetcore blazor dotnet-new templates</PackageTags>
<PackageType>Template</PackageType>
<IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<!-- Template content contains dot-prefixed folders (.template.config) that must be packed -->
<NoDefaultExcludes>true</NoDefaultExcludes>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<IncludeSymbols>false</IncludeSymbols>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<NoWarn>$(NoWarn);NU5128</NoWarn>
<Nullable>disable</Nullable>
<!-- The version pinned in the template project files on disk. It is replaced by the
actual package version while packing, so a template always scaffolds an app that
references the Electron.NET packages it was shipped with. -->
<TemplatePlaceholderVersion>0.6.0</TemplatePlaceholderVersion>
<TemplateProjectFile>templates\blazor\ElectronBlazorApp.csproj</TemplateProjectFile>
<TemplateProjectPackagePath>content\templates\blazor\ElectronBlazorApp.csproj</TemplateProjectPackagePath>
</PropertyGroup>
<ItemGroup>
<Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**;$(TemplateProjectFile)" />
<Compile Remove="**\*" />
</ItemGroup>
<ItemGroup>
<None Include="..\ElectronNET\PackageIcon.png" Pack="true" PackagePath="\" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<Target Name="RewriteTemplateProjectVersion" BeforeTargets="_GetPackageFiles">
<PropertyGroup>
<_TemplateProjectSource>$(MSBuildProjectDirectory)\$(TemplateProjectFile)</_TemplateProjectSource>
<_TemplateProjectTarget>$(IntermediateOutputPath)templates\blazor\ElectronBlazorApp.csproj</_TemplateProjectTarget>
<_TemplateProjectContent>$([System.IO.File]::ReadAllText('$(_TemplateProjectSource)'))</_TemplateProjectContent>
</PropertyGroup>
<Error Condition="!$(_TemplateProjectContent.Contains('Version=&quot;$(TemplatePlaceholderVersion)&quot;'))"
Text="'$(TemplateProjectFile)' contains no PackageReference pinned to $(TemplatePlaceholderVersion), so the package version cannot be injected. Align the template's PackageReference versions with the TemplatePlaceholderVersion property." />
<PropertyGroup>
<_TemplateProjectContent>$(_TemplateProjectContent.Replace('Version="$(TemplatePlaceholderVersion)"', 'Version="$(Version)"'))</_TemplateProjectContent>
</PropertyGroup>
<WriteLinesToFile File="$(_TemplateProjectTarget)"
Lines="$([MSBuild]::Escape($(_TemplateProjectContent)))"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<Content Include="$(_TemplateProjectTarget)" Pack="true" PackagePath="$(TemplateProjectPackagePath)" />
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,21 @@
{
"$schema": "http://json.schemastore.org/dotnetcli.host",
"symbolInfo": {
"Framework": {
"longName": "framework",
"shortName": "f"
},
"ElectronVersion": {
"longName": "electron-version",
"shortName": "e"
},
"Port": {
"longName": "port",
"shortName": "p"
},
"skipRestore": {
"longName": "no-restore",
"shortName": ""
}
}
}

View File

@@ -0,0 +1,75 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Electron.NET",
"classifications": [ "Blazor", "Desktop", "Electron", "Web" ],
"identity": "ElectronNET.Core.Templates.Blazor.CSharp",
"groupIdentity": "ElectronNET.Core.Templates.Blazor",
"name": "Electron.NET Blazor App",
"description": "A Blazor Server app hosted in an Electron shell, wired up with Electron.NET.",
"shortName": "electron-blazor",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "ElectronBlazorApp",
"preferNameDirectory": true,
"defaultName": "ElectronBlazorApp",
"symbols": {
"Framework": {
"type": "parameter",
"description": "The target framework for the project.",
"datatype": "choice",
"choices": [
{
"choice": "net10.0",
"description": "Target net10.0"
},
{
"choice": "net8.0",
"description": "Target net8.0"
}
],
"replaces": "net8.0",
"defaultValue": "net10.0"
},
"ElectronVersion": {
"type": "parameter",
"description": "The version of Electron to build the app against.",
"datatype": "text",
"replaces": "ELECTRON_VERSION",
"defaultValue": "38.2.2"
},
"Port": {
"type": "parameter",
"description": "Port number to use for the ASP.NET server during development.",
"datatype": "integer",
"replaces": "8001",
"defaultValue": "8001"
},
"skipRestore": {
"type": "parameter",
"datatype": "bool",
"description": "If specified, skips the automatic restore of the project on create.",
"defaultValue": "false"
}
},
"primaryOutputs": [
{
"path": "ElectronBlazorApp.csproj"
}
],
"postActions": [
{
"id": "restore",
"condition": "(!skipRestore)",
"description": "Restore NuGet packages required by this project.",
"manualInstructions": [
{
"text": "Run 'dotnet restore'"
}
],
"actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
"continueOnError": true
}
]
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="app.css" />
<ImportMap />
<title>ElectronBlazorApp</title>
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
@inherits LayoutComponentBase
<div class="page">
<NavMenu />
<main>
@Body
</main>
</div>

View File

@@ -0,0 +1,4 @@
<nav class="nav-menu">
<NavLink href="" Match="NavLinkMatch.All">Home</NavLink>
<NavLink href="counter">Counter</NavLink>
</nav>

View File

@@ -0,0 +1,19 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,34 @@
@page "/"
@rendermode InteractiveServer
@using ElectronNET.API
@using ElectronNET.API.Entities
<PageTitle>Home</PageTitle>
<h1>Hello from Electron.NET!</h1>
<p>
This Blazor app is served by ASP.NET Core and displayed in an Electron window.
Everything in the <code>ElectronNET.API</code> namespace is available from your components.
</p>
<button class="btn" @onclick="ShowNotification">Show a native notification</button>
<p role="status">@status</p>
@code {
private string? status;
private void ShowNotification()
{
if (HybridSupport.IsElectronActive)
{
Electron.Notification.Show(new NotificationOptions("ElectronBlazorApp", "Hello from Blazor!"));
status = "Notification sent.";
}
else
{
status = "Not running inside Electron.";
}
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,9 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.JSInterop
@using ElectronBlazorApp
@using ElectronBlazorApp.Components
@using ElectronBlazorApp.Components.Layout

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Electron hosts the app out of process -->
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
<PropertyGroup Label="ElectronNetCommon">
<ElectronPackageId>electron-blazor-app</ElectronPackageId>
<Title>ElectronBlazorApp</Title>
<Description>A Blazor app running in Electron.</Description>
<Version>1.0.0</Version>
<Company>My Company</Company>
<Copyright>Copyright © $(Company)</Copyright>
<ElectronVersion>ELECTRON_VERSION</ElectronVersion>
<ElectronSingleInstance>true</ElectronSingleInstance>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.6.0" />
<PackageReference Include="ElectronNET.Core.AspNet" Version="0.6.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
using ElectronNET.API;
using ElectronNET.API.Entities;
using ElectronNET.AspNet.Middleware;
using ElectronNET.AspNet.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSingleton<IElectronAuthenticationService, ElectronAuthenticationService>();
builder.Services.AddElectron();
// Starts the Electron shell and invokes ElectronAppReady once it is up.
builder.UseElectron(args, ElectronAppReady);
var app = builder.Build();
app.UseMiddleware<ElectronAuthenticationMiddleware>();
app.UseRouting();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
app.UseStaticFiles();
app.UseAntiforgery();
// Fully qualified because ElectronNET.API also defines an 'App' type.
app.MapRazorComponents<ElectronBlazorApp.Components.App>()
.AddInteractiveServerRenderMode();
app.Run();
static async Task ElectronAppReady()
{
var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions
{
IsRunningBlazor = true,
Width = 1152,
Height = 940,
Show = false,
});
window.OnReadyToShow += () => window.Show();
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>false</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>false</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>false</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,22 @@
{
"compression": "maximum",
"linux": {
"target": [ "AppImage" ],
"executableArgs": [ "--no-sandbox" ]
},
"win": {
"target": [
{
"target": "nsis",
"arch": "x64"
}
]
},
"mac": {
"target": [ "dmg" ]
},
"nsis": {
"oneClick": true,
"perMachine": false
}
}

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"ElectronBlazorApp": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:8001/"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,49 @@
:root {
color-scheme: light dark;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
}
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.nav-menu {
display: flex;
gap: 1rem;
padding: 0.75rem 1.5rem;
background-color: #1b1b1f;
}
.nav-menu a {
color: #d7d7d7;
text-decoration: none;
}
.nav-menu a.active {
color: #fff;
font-weight: 600;
}
main {
padding: 1.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
background-color: #1b6ec2;
color: #fff;
cursor: pointer;
}
.btn:hover {
background-color: #1861ac;
}

View File

@@ -29,9 +29,6 @@ namespace ElectronNET.WebApp.Controllers
Electron.IpcMain.On("auto-update", async (args) =>
{
// Electron.NET CLI Command for deploy:
// electronize build /target win /electron-params --publish=always
var currentVersion = await Electron.App.GetVersionAsync();
var updateCheckResult = await Electron.AutoUpdater.CheckForUpdatesAndNotifyAsync();
var availableVersion = updateCheckResult.UpdateInfo.Version;

View File

@@ -8,7 +8,7 @@
</h1>
<h3>The <code>HostHook</code> API allows you to execute your own JavaScript/TypeScript code on the host process.</h3>
<p>Create first an ElectronHostHook directory via the Electron.NET CLI, with the following command: <code>electronize add hosthook</code>.</p>
<p>Create first an ElectronHostHook directory with a package.json and a JavaScript file.</p>
<p>In this directory you can install any NPM packages and embed your own JavaScript/TypeScript code. It is also possible to respond to events from the host process.</p>
<p>You find the sample source code in <code>Controllers\HostHookController.cs</code> and in the <code>ElectronHostHook</code> folder.</p>
</div>

View File

@@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "!Config", "!Config", "{02EA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.AspNet", "ElectronNET.AspNet\ElectronNET.AspNet.csproj", "{DD10D21A-D131-1D9C-33F9-406046E0C5B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.Templates", "ElectronNET.Templates\ElectronNET.Templates.csproj", "{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.ConsoleApp", "ElectronNET.ConsoleApp\ElectronNET.ConsoleApp.csproj", "{EE38A326-5DE8-AF09-9EB9-DF0878938783}"
ProjectSection(ProjectDependencies) = postProject
{1C5FD66E-A1C6-C436-DF7C-3ECE4FEDDFE6} = {1C5FD66E-A1C6-C436-DF7C-3ECE4FEDDFE6}
@@ -99,6 +101,10 @@ Global
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD10D21A-D131-1D9C-33F9-406046E0C5B0}.Release|Any CPU.Build.0 = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11}.Release|Any CPU.Build.0 = Release|Any CPU
{EE38A326-5DE8-AF09-9EB9-DF0878938783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE38A326-5DE8-AF09-9EB9-DF0878938783}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE38A326-5DE8-AF09-9EB9-DF0878938783}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -131,6 +137,7 @@ Global
{829FC339-4785-4229-ABA5-53ADB544DA00} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{8860606D-6847-F22A-5AED-DF4E0984DD24} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{DD10D21A-D131-1D9C-33F9-406046E0C5B0} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{3F2A1D64-9C71-4E1B-9A2D-5C7B0E4F8A11} = {1BB6F634-2831-4496-83A6-BC6761DCEC8D}
{EE38A326-5DE8-AF09-9EB9-DF0878938783} = {EDCBFC49-2AEE-4BAF-9368-4409298C52FC}
{015CB06B-6CAE-209F-E050-21C3ACA5FE9F} = {985D39A7-5216-4945-8167-2FD0CB387BD8}
{06CAADC7-DE5B-47B4-AB2A-E9501459A2D1} = {D36CDFFD-3438-42E4-A7FF-88BA19AC4964}

View File

@@ -9,10 +9,22 @@
ElectronCheckNoManifestJson;
ElectronCheckElectronBuilderJson;
ElectronCheckNoParentPaths;
ElectronCheckPubxmlFiles
ElectronCheckPubxmlFiles;
ElectronCheckHostHookFolder
</ElectronMigrationChecksDependsOn>
</PropertyGroup>
<!--
Directories that are never scanned by the migration checks. Besides the well-known
bin/obj/publish/node_modules folders this also honors custom output paths (e.g. the
.NET 'artifacts' output layout) so that generated files never trigger false alarms.
-->
<PropertyGroup>
<_ElectronCheckExcludes>$(MSBuildProjectDirectory)\bin\**\*;$(MSBuildProjectDirectory)\obj\**\*;$(MSBuildProjectDirectory)\publish\**\*;$(MSBuildProjectDirectory)\node_modules\**\*;$(MSBuildProjectDirectory)\.electron\**\*</_ElectronCheckExcludes>
<_ElectronCheckExcludes Condition="'$(BaseOutputPath)' != ''">$(_ElectronCheckExcludes);$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(BaseOutputPath)'))**\*</_ElectronCheckExcludes>
<_ElectronCheckExcludes Condition="'$(BaseIntermediateOutputPath)' != ''">$(_ElectronCheckExcludes);$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)'))**\*</_ElectronCheckExcludes>
</PropertyGroup>
<!-- Main migration checks target that runs before build -->
<Target Name="ElectronMigrationChecks"
BeforeTargets="BeforeBuild"
@@ -32,16 +44,13 @@
<_InvalidPackageJson Include="$(MSBuildProjectDirectory)\**\package.json"
Exclude="$(MSBuildProjectDirectory)\package.json;
$(MSBuildProjectDirectory)\ElectronHostHook\**\package.json;
$(MSBuildProjectDirectory)\bin\**\package.json;
$(MSBuildProjectDirectory)\obj\**\package.json;
$(MSBuildProjectDirectory)\publish\**\package.json;
$(MSBuildProjectDirectory)\node_modules\**\package.json" />
$(_ElectronCheckExcludes)" />
<!-- The root package-lock.json belongs to the root package.json, which is validated
by ELECTRON008/ELECTRON009 instead of ELECTRON001. -->
<_InvalidPackageLockJson Include="$(MSBuildProjectDirectory)\**\package-lock.json"
Exclude="$(MSBuildProjectDirectory)\ElectronHostHook\**\package-lock.json;
$(MSBuildProjectDirectory)\bin\**\package-lock.json;
$(MSBuildProjectDirectory)\obj\**\package-lock.json;
$(MSBuildProjectDirectory)\publish\**\package-lock.json;
$(MSBuildProjectDirectory)\node_modules\**\package-lock.json" />
Exclude="$(MSBuildProjectDirectory)\package-lock.json;
$(MSBuildProjectDirectory)\ElectronHostHook\**\package-lock.json;
$(_ElectronCheckExcludes)" />
</ItemGroup>
<PropertyGroup>
@@ -141,10 +150,7 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<ItemGroup>
<_InvalidManifestJson Include="$(MSBuildProjectDirectory)\**\electron.manifest.json;$(MSBuildProjectDirectory)\**\electron-manifest.json"
Exclude="$(MSBuildProjectDirectory)\bin\**\*;
$(MSBuildProjectDirectory)\obj\**\*;
$(MSBuildProjectDirectory)\publish\**\*;
$(MSBuildProjectDirectory)\node_modules\**\*" />
Exclude="$(_ElectronCheckExcludes)" />
</ItemGroup>
<PropertyGroup>
@@ -187,10 +193,7 @@ MIGRATION REQUIRED:
<ItemGroup>
<_ElectronBuilderJsonWrongLocation Include="$(MSBuildProjectDirectory)\**\electron-builder.json"
Exclude="$(MSBuildProjectDirectory)\Properties\electron-builder.json;
$(MSBuildProjectDirectory)\bin\**\*;
$(MSBuildProjectDirectory)\obj\**\*;
$(MSBuildProjectDirectory)\publish\**\*;
$(MSBuildProjectDirectory)\node_modules\**\*" />
$(_ElectronCheckExcludes)" />
</ItemGroup>
<PropertyGroup>
@@ -296,10 +299,11 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
</_PubxmlFileInfo>
</ItemGroup>
<!-- Check each file for WebPublishMethod presence -->
<!-- Check each file for the ASP.NET specific properties (WebPublishMethod / ProjectGuid) -->
<ItemGroup Condition="'$(_HasPubxmlFiles)' == 'true'">
<_PubxmlFileInfoWithFlags Include="@(_PubxmlFileInfo)" Condition="'%(Identity)' != ''">
<HasWebPublishMethod>$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileContent)', '&lt;WebPublishMethod&gt;'))</HasWebPublishMethod>
<HasProjectGuid>$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileContent)', '&lt;ProjectGuid&gt;'))</HasProjectGuid>
</_PubxmlFileInfoWithFlags>
</ItemGroup>
@@ -309,10 +313,10 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
Condition="'$(_IsAspNetProject)' == 'true' AND '%(HasWebPublishMethod)' == 'False'" />
</ItemGroup>
<!-- For Console/Non-ASP.NET projects: find pubxml files that HAVE WebPublishMethod -->
<!-- For Console/Non-ASP.NET projects: find pubxml files that HAVE ASP.NET specific properties -->
<ItemGroup>
<_ConsolePubxmlWithAspNetProperties Include="@(_PubxmlFileInfoWithFlags)"
Condition="'$(_IsAspNetProject)' != 'true' AND '%(HasWebPublishMethod)' == 'True'" />
Condition="'$(_IsAspNetProject)' != 'true' AND ('%(HasWebPublishMethod)' == 'True' OR '%(HasProjectGuid)' == 'True')" />
</ItemGroup>
<!-- Warning for ASP.NET projects with incorrect pubxml files -->
@@ -336,7 +340,7 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
<Warning
Condition="'@(_ConsolePubxmlWithAspNetProperties)' != ''"
Code="ELECTRON007"
Text="The publish profile '%(_ConsolePubxmlWithAspNetProperties.Identity)' appears to be configured for ASP.NET publishing (containing the WebPublishMethod property), but this is a console application project.
Text="The publish profile '%(_ConsolePubxmlWithAspNetProperties.Identity)' appears to be configured for ASP.NET publishing (containing the WebPublishMethod and/or ProjectGuid property), but this is a console application project.
RECOMMENDED ACTION:
1. Delete the existing publish profiles
@@ -351,4 +355,59 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr
</Target>
<!--
Check 6: The ElectronHostHook folder must be set up correctly
Custom host hook TypeScript code is only compiled and packaged when the folder also
contains a package.json (and a tsconfig.json for the TypeScript compilation). Without
those files the hook code is silently ignored.
-->
<Target Name="ElectronCheckHostHookFolder"
Condition="Exists('$(MSBuildProjectDirectory)\ElectronHostHook')">
<ItemGroup>
<_HostHookTsFiles Include="$(MSBuildProjectDirectory)\ElectronHostHook\*.ts" />
</ItemGroup>
<PropertyGroup>
<_HasHostHookTsFiles>false</_HasHostHookTsFiles>
<_HasHostHookTsFiles Condition="@(_HostHookTsFiles-&gt;Count()) &gt; 0">true</_HasHostHookTsFiles>
</PropertyGroup>
<Warning Condition="'$(_HasHostHookTsFiles)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\ElectronHostHook\package.json')"
Code="ELECTRON010"
Text="The 'ElectronHostHook' folder contains TypeScript files but no package.json.
Folder: $(MSBuildProjectDirectory)\ElectronHostHook
The host hook code is only compiled and packaged when the folder contains a package.json declaring its npm dependencies. Without it the hook is silently ignored at runtime.
HOW TO FIX:
Add a package.json to the 'ElectronHostHook' folder, for example:
{
&quot;name&quot;: &quot;electron-host-hook&quot;,
&quot;version&quot;: &quot;1.0.0&quot;,
&quot;main&quot;: &quot;index.js&quot;,
&quot;dependencies&quot;: { &quot;socket.io&quot;: &quot;^4.8.1&quot; },
&quot;devDependencies&quot;: { &quot;typescript&quot;: &quot;^5.9.3&quot; }
}
For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migration-Checks#6-electronhosthook-folder-validation" />
<Warning Condition="'$(_HasHostHookTsFiles)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\ElectronHostHook\tsconfig.json')"
Code="ELECTRON011"
Text="The 'ElectronHostHook' folder contains TypeScript files but no tsconfig.json.
Folder: $(MSBuildProjectDirectory)\ElectronHostHook
Without a tsconfig.json the hook TypeScript sources are not compiled with the expected settings.
HOW TO FIX:
Add a tsconfig.json to the 'ElectronHostHook' folder.
For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migration-Checks#6-electronhosthook-folder-validation" />
</Target>
</Project>