-
-
Save vmatyagin/2e453a22a0ca3e355350872bbfa96bca to your computer and use it in GitHub Desktop.
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'), | |
}); |
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"
},
@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
Hi Can u send the config.js file.