Skip to content

Instantly share code, notes, and snippets.

@vmatyagin
Last active March 16, 2024 12:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vmatyagin/2e453a22a0ca3e355350872bbfa96bca to your computer and use it in GitHub Desktop.
Save vmatyagin/2e453a22a0ca3e355350872bbfa96bca to your computer and use it in GitHub Desktop.
electron issue
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createRoot } from 'react-dom/client';
let windowMemory: Window | null = null;
declare global {
interface Window {
_common: {
restoreMedia: VoidFunction;
getMediaURL: () => Promise<string>;
hideMedia: VoidFunction;
};
}
}
const createMediaWindow = async (silent?: boolean): Promise<Window> => {
if (windowMemory) {
!silent && window._common.restoreMedia();
return windowMemory;
}
const location = await window._common.getMediaURL();
windowMemory = window.open(location, '_blank', silent ? 'silent' : undefined);
if (!windowMemory) {
throw new Error('Security restrictions check failed');
}
return windowMemory;
};
const MediaWindow = () => {
const [externalWindow, setExternalWindow] = useState<Window | null>(null);
useEffect(() => {
return () => {
window._common.hideMedia();
};
}, []);
const createWindow = async () => {
const win = await createMediaWindow();
setExternalWindow(win);
};
const close = () => {
window._common.hideMedia();
setExternalWindow(null);
};
const root = externalWindow?.document.body;
return (
<div>
{root ? (
<button onClick={close}>Close portal</button>
) : (
<button onClick={createWindow}>Open portal</button>
)}
{root &&
createPortal(
<div
onClick={close}
style={{ width: 500, height: 500, background: 'green' }}
></div>,
root,
)}
</div>
);
};
createRoot(document.getElementById('app')).render(<MediaWindow />);
// window warmup
createMediaWindow(true);
// Forge Configuration
const path = require('path');
const rootDir = process.cwd();
module.exports = {
// Forge Plugins
plugins: [
{
name: '@electron-forge/plugin-webpack',
config: {
// Fix content-security-policy error when image or video src isn't same origin
// Remove 'unsafe-eval' to get rid of console warning in development mode.
devContentSecurityPolicy: `default-src 'self' 'unsafe-inline' data:; script-src 'self' 'unsafe-inline' data:`,
// Webpack Dev Server port
port: 3000,
// Logger port
loggerPort: 9000,
// Main process webpack configuration
mainConfig: path.join(rootDir, 'tools/webpack/webpack.main.js'),
// Renderer process webpack configuration
renderer: {
// Configuration file path
config: path.join(rootDir, 'tools/webpack/webpack.renderer.js'),
// Entrypoints of the application
entryPoints: [
{
// Window process name
name: 'app_window',
// React Hot Module Replacement (HMR)
rhmr: 'react-hot-loader/patch',
// HTML index file template
html: path.join(rootDir, 'src/renderer/index.html'),
// App Renderer
js: path.join(rootDir, 'src/renderer/appRenderer.tsx'),
// App Preload
preload: {
js: path.join(rootDir, 'src/renderer/preload.ts'),
},
},
{
// Window process name
name: 'media_window',
// React Hot Module Replacement (HMR)
rhmr: 'react-hot-loader/patch',
// HTML index file template
html: path.join(rootDir, 'src/renderer/media.html'),
js: path.join(rootDir, 'src/renderer/empty.ts'),
},
],
},
devServer: {
liveReload: false,
},
},
},
],
};
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
</body>
</html>
import { app, BrowserWindow, ipcMain, screen } from 'electron';
declare const APP_WINDOW_WEBPACK_ENTRY: string;
declare const APP_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
declare const MEDIA_WINDOW_WEBPACK_ENTRY: string;
const recalcBounds = (() => {
let displayMemory: Electron.Display | null = null;
return async () => {
console.log('\nBounds recalc started');
const result = { isWindowChanged: false };
const bounds = appWindow.getBounds();
const display = screen.getDisplayMatching(bounds);
console.log('current main display', display.label);
console.log('current display bound', bounds);
if (display.id !== displayMemory?.id) {
displayMemory = display;
result.isWindowChanged = true;
mediaWindow.setBounds({
height: display.bounds.height,
width: display.bounds.width,
x: display.bounds.x,
y: display.bounds.y,
});
console.log('recalc success, display changed\n');
} else {
console.log('recalc skipped\n');
}
};
})();
let isMediaOpened = false;
const showMediaPage = async () => {
if (mediaWindow) {
await recalcBounds();
mediaWindow.show();
}
};
ipcMain.handle('GET_MEDIA_URL', () => MEDIA_WINDOW_WEBPACK_ENTRY);
ipcMain.handle('RESTORE_MEDIA', () => {
isMediaOpened = true;
showMediaPage();
});
ipcMain.handle('HIDE_MEDIA', () => {
isMediaOpened = false;
mediaWindow.hide();
});
let appWindow: BrowserWindow;
let mediaWindow: BrowserWindow;
function createAppWindow(): BrowserWindow {
appWindow = new BrowserWindow({
width: 500,
height: 500,
show: false,
movable: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: APP_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
appWindow.loadURL(APP_WINDOW_WEBPACK_ENTRY);
appWindow.on('ready-to-show', () => appWindow.show());
appWindow.on('close', () => {
appWindow = null;
app.quit();
});
return appWindow;
}
app.on('ready', createAppWindow);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createAppWindow();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
const getMainWindowDisplayBounds = (): Electron.Rectangle => {
if (!appWindow) {
throw new Error('Window is not defined');
}
const bounds = appWindow.getBounds();
const display = screen.getDisplayMatching(bounds);
return {
height: display.bounds.height,
width: display.bounds.width,
x: display.bounds.x,
y: display.bounds.y,
};
};
app.on('web-contents-created', (_, contents) => {
contents.setWindowOpenHandler(({ url, features }) => {
if (url === MEDIA_WINDOW_WEBPACK_ENTRY) {
app.once('browser-window-created', (_, window) => {
console.log('media window created');
mediaWindow = window;
// window.setAlwaysOnTop(true, 'screen-saver')
window.setHiddenInMissionControl(true);
window.on('show', () => {
console.log('show');
});
window.on('hide', () => {
console.log('hide');
if (!isMediaOpened) {
console.log('aborted');
return;
}
/**
* We dont know will user back after hide event that was occured by
* Mission Control opening, so hiding and will open it on parent focus
*/
window.hide();
appWindow?.blur();
appWindow?.once('focus', () => {
appWindow?.blur();
mediaWindow.focus();
console.log('parent focus');
showMediaPage();
});
});
appWindow.on('move', () => {
console.log('parent move');
recalcBounds();
});
window.on('close', (event) => {
console.log('close');
event.preventDefault();
mediaWindow.hide();
isMediaOpened = false;
});
});
const isSilent = features.includes('silent');
return {
action: 'allow',
overrideBrowserWindowOptions: {
...getMainWindowDisplayBounds(),
hasShadow: false,
// resizable: false,
// alwaysOnTop: true,
enableLargerThanScreen: true,
roundedCorners: false,
fullscreen: false,
fullscreenable: false,
hiddenInMissionControl: true,
// window warmup
show: !isSilent,
paintWhenInitiallyHidden: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webviewTag: false,
backgroundThrottling: false,
devTools: false,
},
},
};
}
return {
action: 'deny',
};
});
});
<!DOCTYPE html>
<html>
<head>
<title>MEDIA</title>
<style>
body,
html {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
body {
background: yellow;
}
</style>
</head>
<body></body>
</html>
{
"name": "example",
"main": ".webpack/main",
"version": "0.0.1",
"scripts": {
"start": "cross-env NODE_ENV=development electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint src/ --ext .ts,.js,.tsx,.jsx"
},
"config": {
"forge": "./tools/forge/forge.config.js"
},
"devDependencies": {
"@electron-forge/cli": "7.2.0",
"@electron-forge/maker-deb": "7.2.0",
"@electron-forge/maker-rpm": "7.2.0",
"@electron-forge/maker-squirrel": "7.2.0",
"@electron-forge/maker-zip": "7.2.0",
"@electron-forge/plugin-webpack": "7.2.0",
"@marshallofsound/webpack-asset-relocator-loader": "^0.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@types/node": "^20.10.8",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@types/webpack-env": "^1.18.4",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
"classnames": "^2.5.1",
"cross-env": "^7.0.3",
"css-loader": "^6.9.0",
"electron": "^29.1.4",
"eslint": "^8.56.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-react": "^7.33.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"node-loader": "^2.0.0",
"react-refresh": "^0.14.0",
"sass": "^1.69.7",
"sass-loader": "^13.3.3",
"style-loader": "^3.3.4",
"ts-loader": "9.5.1",
"typescript": "^5.3.3",
"webpack": "^5.89.0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('_common', {
restoreMedia: () => ipcRenderer.invoke('RESTORE_MEDIA'),
hideMedia: () => ipcRenderer.invoke('HIDE_MEDIA'),
getMediaURL: () => ipcRenderer.invoke('GET_MEDIA_URL'),
});
@fauzank339
Copy link

Hi Can u send the config.js file.

@fauzank339
Copy link

while trying to reproduce the issue I am getting an error

An unhandled rejection has occurred inside Forge:
Error: Expected packageJSON.config.forge to be an object or point to a requirable JS file
at exports.default (/Users/abdullahkidwai/Downloads/2e453a22a0ca3e355350872bbfa96bca-c14fcea0b9ae9de81b047b50216d3184912ea355/node_modules/@electron-forge/core/dist/util/forge-config.js:151:15)
at async /Users/abdullahkidwai/Downloads/2e453a22a0ca3e355350872bbfa96bca-c14fcea0b9ae9de81b047b50216d3184912ea355/node_modules/@electron-forge/core/dist/api/start.js:44:35
at async Task.task (/Users/abdullahkidwai/Downloads/2e453a22a0ca3e355350872bbfa96bca-c14fcea0b9ae9de81b047b50216d3184912ea355/node_modules/@electron-forge/tracer/dist/index.js:58:20)
at async Task.run (/Users/abdullahkidwai/Downloads/2e453a22a0ca3e355350872bbfa96bca-c14fcea0b9ae9de81b047b50216d3184912ea355/node_modules/listr2/dist/index.cjs:978:11)
at async /Users/abdullahkidwai/Downloads/2e453a22a0ca3e355350872bbfa96bca-c14fcea0b9ae9de81b047b50216d3184912ea355/node_modules/p-map/index.js:57:22
},
"config": {
"forge": "./tools/forge/forge.config.js"
},

@vmatyagin
Copy link
Author

@fauzank339 Hi. Thanks for your interest, I've added a config file. You may also need webpack config, you can get it there: https://github.com/codesbiome/electron-react-webpack-typescript-2024. i used this project for gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment