I've got an Electron app that shows a tray icon, which, when clicked, shows my main window. works great in development mode, but when I package it, (into a .app file), and double click the .app file, none of the menus show up, and more importantly, the icon doesn't show up, and so the user can never see my app.
I'm using the electron React/Redux Boilerplate (https://github.com/chentsulin/electron-react-boilerplate)
here's my main.dev.js file - any guesses are appreciated:
import { app, BrowserWindow, Tray } from 'electron';
import MenuBuilder from './menu';
let mainWindow = null;
let tray;
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
require('electron-debug')();
const path = require('path');
const p = path.join(__dirname, '..', 'app', 'node_modules');
require('module').globalPaths.push(p);
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
return Promise.all(
extensions.map(name => installer.default(installer[name], forceDownload))
).catch(console.error);
};
/**
* Add event listeners...
*/
app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (process.platform !== 'darwin') {
app.quit();
}
});
const getWindowPosition = () => {
const windowBounds = mainWindow.getBounds();
const trayBounds = tray.getBounds();
// Center window horizontally below the tray icon
const x = Math.round(
trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2
);
// Position window 4 pixels vertically below the tray icon
const y = Math.round(trayBounds.y + trayBounds.height + 4);
return { x, y };
};
function createTray() {
const path = require('path');
const iconPath = path.join(__dirname, 'confluence.png');
tray = new Tray(iconPath);
tray.setToolTip('Confluence Helper');
tray.on('click', event => {
toggleWindow();
// Show devtools when command clicked
if (mainWindow.isVisible() && process.defaultApp && event.metaKey) {
mainWindow.openDevTools({ mode: 'detach' });
}
});
}
const toggleWindow = () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
showWindow();
}
};
const showWindow = () => {
const position = getWindowPosition();
mainWindow.setPosition(position.x, position.y, false);
mainWindow.show();
mainWindow.focus();
};
app.on('ready', async () => {
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
await installExtensions();
}
mainWindow = new BrowserWindow({
show: false,
width: 500,
height: 728,
icon: `${__dirname}/confluence.icns`
});
mainWindow.loadURL(`file://${__dirname}/app.html`);
createTray();
// #TODO: Use 'ready-to-show' event
// https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event
mainWindow.webContents.on('did-finish-load', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
}
});
mainWindow.on('blur', () => {
mainWindow.hide();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
});
When i right clicked the .app file, and chose "Show Package Contents", I could see a Contents/Mac folder, and inside that was a unix executable file, which when I ran in the command line, showed me a rejected promised that had to do with my tray icon - I was doing a path.join(__dirname,'icon.png'), that ended up being the wrong path (console.log(path.join(__dirname,'icon.png')) to the rescue!
When I changed that to an absolute path ('users/myname/app/icon.png') and rebuilt, it worked!
However, this obviously won't work on OTHER people's computers - it did work on my computer(tm), but that's not good enough.
To REALLY fix it, I might have gone overboard - but this is what worked for me - by creating a NativeImage, using a path.join(__dirname,'resources','icon.png') in what I passed into that. I also added resources under build/files in package.json.
If you run into this kind of problem, i would recommend doing what I did (show package contents, etc) to see the issue in the packaged electron app.
I had the same problem on Windows (didn't tested in other OSes). Tray icon was working when running the app from VSCode, but it was invisible after packaged for Windows. Like the answer from #raingod, I was using the absolute path:
//Gets the icon to use in the tray
icon = nativeImage.createFromPath(path.join(__dirname, "build", "32x32.png"))
//Creates the tray
tray = new Tray(icon)
But what I found to be the problem was that I was pointing to a file in the "build" folder, where all the icons for my app were stored. Somehow it was causing this problem with the tray icon in the packaged app specifically. So I moved just the file used in the tray icon to another folder: "./src/icons/32x32.png". So the code looked like this:
//Gets the icon to use in the tray
icon = nativeImage.createFromPath(path.join(__dirname, "src", "icons", "32x32.png"))
//Creates the tray
tray = new Tray(icon)
Posting this here because another person might have the same problem. So use absolute path, and don't store the icon file in the "build" folder.
Related
My electron main.js code:
function createWindow () {
const { screen } = require('electron');
const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize;
let mainWindow = new BrowserWindow({
width,
height,
webPreferences: {
webSecurity: false,
},
});
return mainWindow;
}
function createServerProcess () {
serverProcess = fork(__dirname + '/index.js');
}
app.whenReady().then(async () => {
createServerProcess();
let mainWindow = createWindow();
let workspaceId = await localStorage.getItem('workspaceId');
if (!workspaceId) {
workspaceId = 'abc';
}
let url = `http://${workspaceId}.localhost:8443`;
setTimeout(() => {
mainWindow.loadURL(url);
}, 3000);
});
I am using electron-forge to build my electron app, using electron-forge start to start local app works fine. But when starting the app generated by electron-forge make command, it shows below error:
I have already set webSecurity to false, for turning off cross origin restriction. Just don't know why while local app works fine but generated app is not working. I am confused. I am new to Electron.
ok so I have been cracking my head over this problem for the last 2 days.
The default exit button of an electron.js app doesn't work.
Whenever I run my app and I click on the exit button, it doesn't show any error in the terminal and doesn't close the app either.
So far, these are the code snippets I have used:
// functions I used
app.on('window-all-closed') {
}
app.on('close') {
}
// the code inside the functions I used
_____________________________________
app.destroy()
_____________________________________
app.exit()
_____________________________________
app = null;
And I haven't used any HTML code since I am loading the URL of the page I want to make as an app like this:
function createWindow () {
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({
width: 720,
height: 600,
icon:'assets/icons/win/icon.png',
})
win.loadURL('https://insert-link')
win.removeMenu()
win.on("closed", () => mainWindow.destroy());
}
const { app } = require('electron')
app.whenReady().then(createWindow)
app.once('window-all-closed', function() {
app.quit().then(app.destroy())
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
Please help me on this.
I have built an Electron app and I'm trying to build it out. It works perfectly when I run it in the dev environment with "npm start", but once I build it out, it fails. I've tried this with electron forge and electron packager. Essentially, my app lives in the task tray and scans a folder every second. If the folder is not empty, it shows the window. While the window is shown, the loop stops. Once the main.js file receives a command that the user actions were completed, it hides the window and goes back to the tray to resume scanning. Here is my main.js file:
const { app, BrowserWindow, Tray, Menu } = require('electron')
var ipcMain = require('electron').ipcMain;
const Store = require('electron-store');
const store = new Store();
const fs = require('fs');
shouldScan = true;
// global window declaration function
var win;
async function createWindow () {
win = new BrowserWindow({
width: 500,
height: 250,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
}
})
win.setMenuBarVisibility(false)
win.loadFile('index.html')
tray = new Tray('spongebob.ico')
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show App', click: function () {
win.show()
}
},
{
label: 'Quit', click: function () {
app.isQuiting = true
app.quit()
}
}
])
tray.setToolTip('This is my application.')
tray.setContextMenu(contextMenu)
win.on('minimize', function (event) {
event.preventDefault()
shouldScan = true
scanning()
win.hide()
})
win.on('show', function (event) {
event.preventDefault()
shouldScan = false
})
await sleep(1000)
win.minimize()
}
// start the application
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
//allow delays
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//check the designated folder and stop the loop while showing the app window from the tray
async function scanning(){
while(shouldScan){
console.log('scanning')
if(store.get('default_path') != null){
files = fs.readdirSync(store.get('default_path'));
if(files.length > 0){
fs.rename(store.get('default_path') + "/" + files[0], store.get('default_path') + "/encounter", err => {
if (err) {
console.error(err)
return
}
})
console.log('should have shown')
win.show()
shouldScan = false
}
}
await sleep(1000)
}
}
//start the scanning funciton again when the signal is received
ipcMain.on('processInput', function(event, status) {
win.hide()
shouldScan = true
scanning()
});
The error I'm experiencing is that the window never goes to tray. Its even supposed to minimize 1 second after launching but it doesn't do that either. Actually, none of the scripts in the main file run other than creating the window. If the window is minimized and its target folder is not empty, it does not re-show the window. The dev tools within the window don't show any errors. I don't know how to have a command prompt running while the packaged .exe is running either, though. Does anyone have some advice?
for anyone in the future, it seems that electron does not like just local file paths. When I was creating the new Tray('spongebob.ico') that wasn't good. doing this seemed to fix the error:
new Tray(path.join(__dirname, 'asset','img', 'spongebob.png'));
obviously I had to create the correct path and file type.
How to create a single instance of an Electron app? If it's already running in the tray and user starts it again, how to open the running app from tray instead of starting a new one?
thank you!
I found this in docs, https://electronjs.org/docs/api/app#apprequestsingleinstancelock:
const { app } = require('electron')
let myWindow = null
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Кто-то пытался запустить второй экземпляр, мы должны сфокусировать наше окно.
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore()
myWindow.focus()
}
})
// Создать myWindow, загрузить остальную часть приложения, и т.д.
app.on('ready', () => {
})
}
Use app.makeSingleInstance(), to make sure the user does not open multiple instances of electron. Once you share your code I will make an edit to show you how to properly implement it.
var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) {
// Someone tried to run a second instance, we should focus our window.
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore();
myWindow.focus();
}
});
I'm working on a JHipster application that I'm trying to get functioning in Electron. I have Golden Layout for window/pane management and cross-pane communication. I am having several problems with the combination of technologies, including:
I can't pop out more than one pane at the same time into their own Electron windows. I instead get an Uncaught Error: Can't create config, layout not yet initialised error in the console.
Two thirds of the panes don't display anything when popped out into Electron windows, and I'm not sure what the reason is. Any ideas or suggestions for this? One example of content is a leaflet map, another is a "PowerPoint preview" that is really just divs that mock the appearance of slides.
I haven't made it this far yet, but I assume that I will have trouble communicating between popped-out Electron windows when I get more than one open. Right now, the panes communicate between each other using Golden Layout's glEventHub emissions. I have an avenue to explore when I cross that bridge, namely Electron ipcRenderer.
Some borrowed code is here (most of it I can't share because it's company confidential):
electron.js:
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require('path');
const isDev = require('electron-is-dev');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({width: 900, height: 680});
mainWindow.loadURL(isDev ? 'http://localhost:9000' : `file://${path.join(__dirname, '../build/index.html')}`);
if (isDev) {
// Open the DevTools.
//BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => mainWindow = null);
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
goldenLayoutComponent.tsx, a patch for Golden Layout:
import React from "react";
import ReactDOM from "react-dom";
// import "./goldenLayout-dependencies";
import GoldenLayout from "golden-layout";
import "golden-layout/src/css/goldenlayout-base.css";
import "golden-layout/src/css/goldenlayout-dark-theme.css";
import $ from "jquery";
interface IGoldenLayoutProps {
htmlAttrs: {},
config: any,
registerComponents: Function
}
interface IGoldenLayoutState {
renderPanels: Set<any>
}
interface IContainerRef {
current: any
}
export class GoldenLayoutComponent extends React.Component <IGoldenLayoutProps, IGoldenLayoutState> {
goldenLayoutInstance = undefined;
state = {
renderPanels: new Set<any>()
};
containerRef: IContainerRef = React.createRef();
render() {
const panels = Array.from(this.state.renderPanels || []);
return (
<div ref={this.containerRef as any} {...this.props.htmlAttrs}>
{panels.map((panel, index) => {
return ReactDOM.createPortal(
panel._getReactComponent(),
panel._container.getElement()[0]
);
})}
</div>
);
}
componentRender(reactComponentHandler) {
this.setState(state => {
const newRenderPanels = new Set(state.renderPanels);
newRenderPanels.add(reactComponentHandler);
return { renderPanels: newRenderPanels };
});
}
componentDestroy(reactComponentHandler) {
this.setState(state => {
const newRenderPanels = new Set(state.renderPanels);
newRenderPanels.delete(reactComponentHandler);
return { renderPanels: newRenderPanels };
});
}
componentDidMount() {
this.goldenLayoutInstance = new GoldenLayout(
this.props.config || {},
this.containerRef.current
);
if (this.props.registerComponents instanceof Function)
this.props.registerComponents(this.goldenLayoutInstance);
this.goldenLayoutInstance.reactContainer = this;
this.goldenLayoutInstance.init();
}
}
// Patching internal GoldenLayout.__lm.utils.ReactComponentHandler:
const ReactComponentHandler = GoldenLayout["__lm"].utils.ReactComponentHandler;
class ReactComponentHandlerPatched extends ReactComponentHandler {
_container: any;
_reactClass: any;
_render() {
const reactContainer = this._container.layoutManager.reactContainer; // Instance of GoldenLayoutComponent class
if (reactContainer && reactContainer.componentRender)
reactContainer.componentRender(this);
}
_destroy() {
// ReactDOM.unmountComponentAtNode( this._container.getElement()[ 0 ] );
this._container.off("open", this._render, this);
this._container.off("destroy", this._destroy, this);
}
_getReactComponent() {
// the following method is absolute copy of the original, provided to prevent depenency on window.React
const defaultProps = {
glEventHub: this._container.layoutManager.eventHub,
glContainer: this._container
};
const props = $.extend(defaultProps, this._container._config.props);
return React.createElement(this._reactClass, props);
}
}
GoldenLayout["__lm"].utils.ReactComponentHandler = ReactComponentHandlerPatched;
Any help or insight into these issues would be appreciated. Thanks in advance!
If you are still looking for a solutions, 1 and 2 I have solved, if you want to see my solution you could see in this repository.
But it was basically this:
1: The window that popups has a different path than the main window, so I just had to put a try catch in my requires, and you have to set
nativeWindowOpen = true
when creating the Browser window.
2: Solves it's self after 1 I think