How to attach an Electron window to an application screen and resize dynamically - node.js

I am currently writing a program that uses a mix of Electron and React-Redux to create an overlay window on top of screens/applications. I managed to successfully create the transparent overlay window and list all the valid media streams. But I can't figure out how I can have this new overlay window match the size/location of the selected stream and dynamically resize. On top of that, I would like the overlay to be on top of the selected stream alone.
Any tips are welcome :)
// In MainProcess
ipcMain.on(ELECTRON_CREATE_OVERLAY_WINDOW, (event, windowSettings) => {
if (overlayWindow !== null) {
console.error('Trying to create an Overlay window when there is already one!')
return
}
console.log('Creating the Overlay window')
overlayWindow = new BrowserWindow({
width: windowSettings.width,
height: windowSettings.height,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
},
transparent: true,
frame: false,
alwaysOnTop: true,
});
overlayWindow.setIgnoreMouseEvents(true);
overlayWindow.loadURL("http://localhost:3000/overlay");
overlayWindow.on('closed', () => {
console.log('Overlay window closed')
overlayWindow = null
})
});
// In React page / RendererProcess
React.useEffect(async () => {
desktopCapturer
.getSources({
types: ["window", "screen"],
})
.then((inputSources) => {
for (let i = 0; i < inputSources.length; i++) {
let source = inputSources[i];
const constraints = {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: source.id,
},
},
};
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
inputSources[i].stream = stream;
console.log('stream', stream)
// When we got all streams, update the state
if (i == inputSources.length - 1) {
setSources([...inputSources]);
}
});
}
});
}, []);
...
const launchOverlay = (source) => {
const streamSettings = source.stream.getVideoTracks()[0].getSettings();
console.log(source)
console.log(source.stream)
console.log(streamSettings)
createOverlayWindow({ width: streamSettings.width, height: streamSettings.height })
};

You can use electron-overlay-window package for it.
Readme says it supports Windows and Linux
It tracks target windows by its title and keeps your app window right over it. It also re-attaches itself if you restart the target app/game. The only downside - it's not very documented. But the basic demo is simple.
// ...
import { overlayWindow as OW } from 'electron-overlay-window'
// ...
const win = new BrowserWindow({
...OW.WINDOW_OPTS, // pay attention here
width: 800,
height: 600,
resizable: false,
webPreferences: {
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
},
})
// ... when ready
OW.attachTo(window, 'Untitled - Notepad')
// listen for lifecycle events
OW.on('attach', ev => { console.log('WO: attach', ev) })
OW.on('detach', ev => { console.log('WO: detach', ev) })
OW.on('blur', ev => { console.log('WO: blur', ev)})
OW.on('focus', ev => { console.log('WO: focus', ev)})
OW.on('fullscreen', ev => console.log('WO: fullscreen', ev))
OW.on('moveresize', ev => console.log('WO: fullscreen', ev))
You can look up more examples here:
Ender-O for Elite Dangerous written in JS (demo video)
Awaken PoE Trade for Path of Exile written in TS
Simple demo from author written in TS (I recommend starting with this example)

Related

Electron | TypeError: "x" is not a constructor

I have a simple electron app that opens a browser window to a url of a webpage.
I import my stuff using
const { BrowserWindow, app, MessageChannelMain } = require('electron');
I create my window on app.on("ready") using window = new BrowserWindow({ ... }); and everything works fine.
When I try to create a MessageChannelMain using const {port1, port2} = new MessageChannelMain();, I get the error:
TypeError: MessageChannelMain is not a constructor.
Full code:
function init_window(){
if ( mainWindow == null ) {
mainWindow = create_window();
load_window(); // calls mainWindow.loadURL(myURL);
const {port1, port2} = new MessageChannelMain();
port2.postMessage({ test: 21 })
port2.on('message', (event) => {
console.log('from renderer main world:', event.data)
});
port2.start();
mainWindow.webContents.postMessage('main-world-port', null, [port1])
mainWindow.on('closed', function(event) {
mainWindow = null;
});
mainWindow.webContents.on("did-fail-load", function(event) {
})
}
}
function create_window() {
return new BrowserWindow({
width: 1200,
height: 800,
darkTheme: true,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});
}
When I console.log(MessageChannelMain); I get undefined.
At the same time, I get other errors when I try to use some electron stuff on the preload script:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing')
}
)
Here I am getting TypeError: Cannot read property 'exposeInMainWorld' of undefined

Kaspersky marks my Electron app as a Trojan

I recently made an Electron app that is essentially a web browser and it has 3 windows and they all get destroyed when the user closes them, leaving no excess process behind this is the only complex part of the app but Kaspersky flags it as a Trojan.
my index.js
const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron');
const path = require('path');
app.commandLine.appendSwitch("disable-http-cache");
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
// eslint-disable-next-line global-require
if (require('electron-squirrel-startup')) {
app.quit();
}
function createMacroWindow(macroUrl) {
let macroWindow = new BrowserWindow({
width:800,
height:500,
frame:false,
show:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences:{
devTools: false,
webviewTag: true,
nodeIntegration: true,
contextIsolation: false
}
})
macroWindow.loadFile(path.join(__dirname, './windows/macro-window.html'));
macroWindow.webContents.on('did-finish-load', () => {
macroWindow.webContents.send("loadURL", macroUrl)
})
macroWindow.center();
macroWindow.openDevTools();
macroWindow.show();
macroWindow.on("close", () => {
macroWindow = null;
})
require("#electron/remote/main").enable(macroWindow.webContents);
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
height: 700,
frame:false,
show:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences: {
webviewTag: true,
nodeIntegration: true,
devTools: true, // Geliştirici konsolunu kökten kısıtlayan kod (bu olmazsa CTRL+SHIFT+I yapınca yine açılır)
contextIsolation: false,
},
});
let splashScreen = new BrowserWindow({
width:1000,
height:700,
frame:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences: {
devTools:false
}
})
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
splashScreen.loadFile(path.join(__dirname, './windows/splash-window.html'));
splashScreen.center();
function destroySplashScreen() {
splashScreen.close();
splashScreen = null;
}
mainWindow.webContents.on('did-finish-load', function() {
mainWindow.show();
mainWindow.maximize();
splashScreen.isDestroyed() ? console.log("splash screen already destroyed") : destroySplashScreen();
});
require('#electron/remote/main').initialize()
require("#electron/remote/main").enable(mainWindow.webContents)
ipcMain.handle(
'DESKTOP_CAPTURER_GET_SOURCES',
(event, opts) => desktopCapturer.getSources(opts)
)
let macroUrl;
ipcMain.on("openMacroWindow", (e, urlToLoad) => {
macroUrl = urlToLoad;
createMacroWindow(macroUrl);
})
ipcMain.on("closeApp", () => {
BrowserWindow.getAllWindows().forEach(window => window.close());
})
ipcMain.on('clearCache', () => {
mainWindow.webContents.session.clearStorageData([], function (data) {
console.log(data);
})
mainWindow.webContents.session.clearCache();
})
mainWindow.on("close", () => {
app.quit();
})
// Open the DevTools.
// mainWindow.webContents.openDevTools(); // Geliştirici konsolunu kapatmak için bu satırı silebilirsiniz
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// app.disableHardwareAcceleration()
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
splashScreen is the window that opens while the mainWindow loads, it closes when the main loads and is set to null afterwards just like the macroWindow.
macroWindow is opened when the ipcMain is told to openMacroWindow
Fix
I downgraded to electron-forge version 5.2.4 from 6.x version, this fixed my issue but this is not an ideal solution.

Vue/Electron IPC between main and renderer

I'm trying to setup communication between my Vue browser app and the electron main process, but it is not working.
Before startBot() is even called, I get an error message that __Dirname is unknown. But this constant is nowhere in to be found in the code.
What am I doing wrong?
https://gist.github.com/Quenos/7d6dbe4f5410739499ea9e3b0b4f961a.js
This is the background.js where I open the browser window with a preload. This has the purpose of making window available to the browser process
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1300,
height: 1100,
title: "Hedgehog TRDR Bot",
icon: path.join(__static, "hedgehog.jpg"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// __static is set by webpack and will point to the public directory
preload: path.resolve(__static, "preload.js"),
},
});
This is said preload.js
const { contextBridge, ipcRenderer } = require("electron");
const validChannels = ["READ_FILE", "WRITE_FILE"];
contextBridge.exposeInMainWorld("ipc", {
send: (channel, data) => {
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on: (channel, func) => {
if (validChannels.includes(channel)) {
// Strip event as it includes `sender` and is a security risk
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
The main process which contains listeners that then will do file handling
const { ipcMain } = require("electron");
const fs = require("fs");
var file;
ipcMain.on("OPEN_FILE", (event, payload) => {
console.log("daaro");
file = fs.createWriteStream(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
ipcMain.on("TEST_FILE", (event, payload) => {
console.log("daaro");
file.write(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
And the browser process which send requests to do file handling:
async startBot() {
window.ipc.send("OPEN_FILE", { path: "./HH_trdr_bot.csv" });
}
In the meantime I've found this article that perfectly answers my question
https://medium.com/swlh/how-to-safely-set-up-an-electron-app-with-vue-and-webpack-556fb491b83
Docs of Vue CLI Plugin Electron Builder contain description of how to do it, but it's a little scattered.
First, see how to configure a preload script, i.e.:
vue.config.js
module.exports = {
// ...
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
}
}
}
and then, repeat the path in BrowserWindow's webPreferences.preload constructor option.
preload: path.join(__dirname, 'preload.js')
Finally, in src/preload.js expose the IPC Renderer as described in IPC Without Node Integration.
src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
// whitelist channels
let validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['fromMain']
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
It might be also a good idea to unsubscribe the event listener when the component that subscribed is about to be destroyed.
I made mine working by putting the preload.js file inside public folder, in the https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#preload-files docs, there is a little diagram under Folder Structure section that says ├── public/ # Files placed here will be available through __static or process.env.BASE_URL, so what I did is simply, use the __static variable as described in docs, and append it with \preload.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: __static + '/preload.js', // <-- simple solution
}
})

Lottie Animation in fabricjs canvas

Is it possible to load the Lottie animation in fabricjs canvas
I have tried the following samples
bodymovin.loadAnimation({
wrapper: animateElement, // div element
loop: true,
animType: 'canvas', // fabricjs canvas
animationData: dataValue, // AE json
rendererSettings: {
scaleMode: 'noScale',
clearCanvas: true,
progressiveLoad: false,
hideOnTransparent: true,
}
});
canvas.add(bodymovin);
canvas.renderAll();
I cant able to add the animation in the fabric js canvas. if any one overcome this issue kindly do comments on it
I might be late to answer this, but for anyone else looking, this pen could give you some pointers: https://codepen.io/shkaper/pen/oEKEgG
The idea here, first of all, is to extend fabric.Image class overriding its internal render method to render the contents of an arbitrary canvas that you yourself provide:
fabric.AEAnimation = fabric.util.createClass(fabric.Image, {
drawCacheOnCanvas: function(ctx) {
ctx.drawImage(this._AECanvas, -this.width / 2, -this.height / 2);
},
})
You can make this canvas a constructor argument, e.g.
initialize: function (AECanvas, options) {
options = options || {}
this.callSuper('initialize', AECanvas, options)
this._AECanvas = AECanvas
},
Then you'll just need to use lottie's canvas renderer to draw animation on a canvas and pass it to your new fabric.AEAnimation object.
I would assume so, by combining your code with something similar to https://itnext.io/video-element-serialization-and-deserialization-of-canvas-fc5dbf47666d. Depending on your scenario you might be able to get away with using something like http://fabricjs.com/interaction-with-objects-outside-canvas
If of any help, I've created this Lottie class with the support of exporting toObject/JSON
import { fabric } from 'fabric'
import lottie from 'lottie-web'
const Lottie = fabric.util.createClass(fabric.Image, {
type: 'lottie',
lockRotation: true,
lockSkewingX: true,
lockSkewingY: true,
srcFromAttribute: false,
initialize: function (path, options) {
if (!options.width) options.width = 480
if (!options.height) options.height = 480
this.path = path
this.tmpCanvasEl = fabric.util.createCanvasElement()
this.tmpCanvasEl.width = options.width
this.tmpCanvasEl.height = options.height
this.lottieItem = lottie.loadAnimation({
renderer: 'canvas',
loop: true,
autoplay: true,
path,
rendererSettings: {
context: this.tmpCanvasEl.getContext('2d'),
preserveAspectRatio: 'xMidYMid meet',
},
})
// this.lottieItem.addEventListener('DOMLoaded', () => {
// console.log('DOMLoaded')
// })
this.lottieItem.addEventListener('enterFrame', (e) => {
this.canvas?.requestRenderAll()
})
this.callSuper('initialize', this.tmpCanvasEl, options)
},
play: function () {
this.lottieItem.play()
},
stop: function () {
this.lottieItem.stop()
},
getSrc: function () {
return this.path
},
})
Lottie.fromObject = function (_object, callback) {
const object = fabric.util.object.clone(_object)
fabric.Image.prototype._initFilters.call(object, object.filters, function (filters) {
object.filters = filters || []
fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function (resizeFilters) {
object.resizeFilter = resizeFilters[0]
fabric.util.enlivenObjects([object.clipPath], function (enlivedProps) {
object.clipPath = enlivedProps[0]
const fabricLottie = new fabric.Lottie(object.src, object)
callback(fabricLottie, false)
})
})
})
}
Lottie.async = true
export default Lottie
To create Lottie element just pass JSON
const fabricImage = new fabric.Lottie('https://assets5.lottiefiles.com/private_files/lf30_rttpmsbc.json', {
scaleX: 0.5,
})
canvas.add(fabricImage)

Electron loading animation

Could someone please help me to implement a loading animation for my Electron app ?
I am learning this new technology and it would be nice to understand the correct way to do that.
I am thinking about something like :
app.on('ready', () => {
// show main content
})
app.on('not-ready', () => {
// show loading animation
})
As far as I know there is no event emitted from app before ready (only exception is will-finish-launching available only on macOS).
Furthermore, you cannot open any BrowserWindow before app is ready, so you should really wait it.
However, if your main application window loading very slow, you can still open a "loading window" before that and switch them when your main window is ready.
const { app, BrowserWindow } = require('electron')
app.on('ready', () => {
let main = null
let loading = new BrowserWindow({show: false, frame: false})
loading.once('show', () => {
main = new BrowserWindow({show: false})
main.webContents.once('dom-ready', () => {
console.log('main loaded')
main.show()
loading.hide()
loading.close()
})
// long loading html
main.loadURL('http://spacecrafts3d.org')
})
loading.loadURL('loding.html')
loading.show()
})
You can use win.on('ready-to-show') instead of win.webContents.on('dom-ready') everywhere if you want to eliminate visual flash (but losing some speed)
window.open()
If you want to do the same for BrowserWindow opened in renderer process by window.open(), you can use new-window event of webContents if nativeWindowOpen is true
main = new BrowserWindow({
webPreferences: {
nativeWindowOpen: true
}
})
main.webContents.on('new-window', (event, url) => {
// there are more args not used here
event.preventDefault()
const win = new BrowserWindow({show: false})
win.webContents.once('dom-ready', () => {
win.show()
loading.hide() // don't destroy in this case
})
win.loadURL(url)
loading.show()
event.newGuest = win
})
It can be done by displaying a new BrowserWindow displaying the activity loader , until the main app fully loads .
Let's define a createWindow funtion (as given in docs) which is responsible for loading the main app for the user as :
var loadingwindow = null; // Responsible for creating activity loader
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
win.loadFile('index.html')
loadingwindow.hide() // Used to hide the loading screen when the contents in main app are loaded
}
Now , in order to display the loadingwindow screen , we need to place it in app.on('ready' , callback_fn) as shows here :
app.on("ready" , () => {
loadingwindow = new BrowserWindow({
frame : false,
movable : false,
})
loadingwindow.loadFile('activity.html') // To load the activity loader html file
loadingwindow.show();
})
That's it ! ,
To check if it is working properly , wrap the setTimeout function over the app.whenReady().then(createWindow)

Resources