mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-09-22 15:04:47 +00:00
Compare commits
11 Commits
0.6.0-pre.
...
0.6.0-pre.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c0523c7d | ||
|
|
6d94cf9538 | ||
|
|
521e7c935a | ||
|
|
730eacbd52 | ||
|
|
deca8166ec | ||
|
|
a04ca0b9bc | ||
|
|
fffd7dfb79 | ||
|
|
98c72af3d6 | ||
|
|
703cf8f880 | ||
|
|
248d4bd664 | ||
|
|
d5da1dbc9b |
13
Changelog.md
13
Changelog.md
@@ -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
|
||||
|
||||
|
||||
@@ -23,13 +23,14 @@ While the classic Electron.NET setup (using an ASP.NET host run by the Electron
|
||||
* ElectronNET.Core: [](https://www.nuget.org/packages/ElectronNET.Core.API/)
|
||||
* ElectronNET.Core.API: [](https://www.nuget.org/packages/ElectronNET.Core.API/)
|
||||
* ElectronNET.Core.AspNet: [](https://www.nuget.org/packages/ElectronNET.Core.AspNet/)
|
||||
* ElectronNET.Core.Templates: [](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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
88
docs/GettingStarted/Templates.md
Normal file
88
docs/GettingStarted/Templates.md
Normal 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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
18
src/ElectronNET.API/API/Entities/AdjustSelectionOptions.cs
Normal file
18
src/ElectronNET.API/API/Entities/AdjustSelectionOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
24
src/ElectronNET.API/API/Entities/FindInPageOptions.cs
Normal file
24
src/ElectronNET.API/API/Entities/FindInPageOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
33
src/ElectronNET.API/API/Entities/FoundInPageResult.cs
Normal file
33
src/ElectronNET.API/API/Entities/FoundInPageResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
25
src/ElectronNET.API/API/Entities/LoadFileOptions.cs
Normal file
25
src/ElectronNET.API/API/Entities/LoadFileOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
23
src/ElectronNET.API/API/Entities/StopFindInPageAction.cs
Normal file
23
src/ElectronNET.API/API/Entities/StopFindInPageAction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
18
src/ElectronNET.API/API/Entities/ZoomDirection.cs
Normal file
18
src/ElectronNET.API/API/Entities/ZoomDirection.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
65
src/ElectronNET.Templates/ElectronNET.Templates.csproj
Normal file
65
src/ElectronNET.Templates/ElectronNET.Templates.csproj
Normal 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="$(TemplatePlaceholderVersion)"'))"
|
||||
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>
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<NavMenu />
|
||||
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<nav class="nav-menu">
|
||||
<NavLink href="" Match="NavLinkMatch.All">Home</NavLink>
|
||||
<NavLink href="counter">Counter</NavLink>
|
||||
</nav>
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
48
src/ElectronNET.Templates/templates/blazor/Program.cs
Normal file
48
src/ElectronNET.Templates/templates/blazor/Program.cs
Normal 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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"ElectronBlazorApp": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:8001/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
49
src/ElectronNET.Templates/templates/blazor/wwwroot/app.css
Normal file
49
src/ElectronNET.Templates/templates/blazor/wwwroot/app.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)', '<WebPublishMethod>'))</HasWebPublishMethod>
|
||||
<HasProjectGuid>$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileContent)', '<ProjectGuid>'))</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->Count()) > 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:
|
||||
|
||||
{
|
||||
"name": "electron-host-hook",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": { "socket.io": "^4.8.1" },
|
||||
"devDependencies": { "typescript": "^5.9.3" }
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user