From 885790bc22dee6f77dd64ac01341c63523eba29d Mon Sep 17 00:00:00 2001 From: omouren Date: Fri, 6 Oct 2017 01:32:48 +0200 Subject: [PATCH] Fix #304 - Add --tray CLI flag to let app running in background on window close. Supports in-title counter. (#457) --- app/src/components/mainWindow/mainWindow.js | 7 +- app/src/components/trayIcon/trayIcon.js | 79 +++++++++++++++++++++ app/src/main.js | 2 + docs/api.md | 10 +++ src/build/buildApp.js | 1 + src/cli.js | 1 + src/options/optionsMain.js | 2 +- 7 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 app/src/components/trayIcon/trayIcon.js diff --git a/app/src/components/mainWindow/mainWindow.js b/app/src/components/mainWindow/mainWindow.js index fdc51b4..4bc5525 100644 --- a/app/src/components/mainWindow/mainWindow.js +++ b/app/src/components/mainWindow/mainWindow.js @@ -10,11 +10,14 @@ const { isOSX, linkIsInternal, getCssToInject, shouldInjectCss } = helpers; const ZOOM_INTERVAL = 0.1; -function maybeHideWindow(window, event, fastQuit) { +function maybeHideWindow(window, event, fastQuit, tray) { if (isOSX() && !fastQuit) { // this is called when exiting from clicking the cross button on the window event.preventDefault(); window.hide(); + } else if (!fastQuit && tray) { + event.preventDefault(); + window.hide(); } // will close the window on other platforms } @@ -211,7 +214,7 @@ function createMainWindow(inpOptions, onAppQuit, setDockBadge) { mainWindow.setFullScreen(false); mainWindow.once('leave-full-screen', maybeHideWindow.bind(this, mainWindow, event, options.fastQuit)); } - maybeHideWindow(mainWindow, event, options.fastQuit); + maybeHideWindow(mainWindow, event, options.fastQuit, options.tray); }); return mainWindow; diff --git a/app/src/components/trayIcon/trayIcon.js b/app/src/components/trayIcon/trayIcon.js new file mode 100644 index 0000000..4a507be --- /dev/null +++ b/app/src/components/trayIcon/trayIcon.js @@ -0,0 +1,79 @@ +import path from 'path'; + +const { app, Tray, Menu, ipcMain } = require('electron'); + +/** + * + * @param {{}} inpOptions AppArgs from nativefier.json + * @param {electron.BrowserWindow} mainWindow MainWindow created from main.js + * @returns {electron.Tray} + */ +function createTrayIcon(inpOptions, mainWindow) { + const options = Object.assign({}, inpOptions); + + if (options.tray) { + const iconPath = path.join(__dirname, '../', '/icon.png'); + const appIcon = new Tray(iconPath); + + const onClick = () => { + if (mainWindow.isVisible()) { + mainWindow.hide(); + } else { + mainWindow.show(); + } + }; + + const contextMenu = Menu.buildFromTemplate([ + { + label: options.name, + click: onClick, + }, + { + label: 'Quit', + click: app.exit, + }, + ]); + + appIcon.on('click', onClick); + + mainWindow.on('show', () => { + appIcon.setHighlightMode('always'); + }); + + mainWindow.on('hide', () => { + appIcon.setHighlightMode('never'); + }); + + if (options.counter) { + mainWindow.on('page-title-updated', (e, title) => { + const itemCountRegex = /[([{](\d*?)\+?[}\])]/; + const match = itemCountRegex.exec(title); + if (match) { + appIcon.setToolTip(`(${match[1]}) ${options.name}`); + } else { + appIcon.setToolTip(options.name); + } + }); + } else { + ipcMain.on('notification', () => { + if (mainWindow.isFocused()) { + return; + } + appIcon.setToolTip(`• ${options.name}`); + }); + + mainWindow.on('focus', () => { + appIcon.setToolTip(options.name); + }); + } + + appIcon.setToolTip(options.name); + appIcon.setContextMenu(contextMenu); + + return appIcon; + } + + return null; +} + +export default createTrayIcon; diff --git a/app/src/main.js b/app/src/main.js index 6475315..914478c 100644 --- a/app/src/main.js +++ b/app/src/main.js @@ -6,6 +6,7 @@ import electronDownload from 'electron-dl'; import createLoginWindow from './components/login/loginWindow'; import createMainWindow from './components/mainWindow/mainWindow'; +import createTrayIcon from './components/trayIcon/trayIcon'; import helpers from './helpers/helpers'; import inferFlash from './helpers/inferFlash'; @@ -103,6 +104,7 @@ if (appArgs.crashReporter) { app.on('ready', () => { mainWindow = createMainWindow(appArgs, app.quit, setDockBadge); + createTrayIcon(appArgs, mainWindow); }); app.on('login', (event, webContents, request, authInfo, callback) => { diff --git a/docs/api.md b/docs/api.md index 4606b5a..e22f861 100644 --- a/docs/api.md +++ b/docs/api.md @@ -40,6 +40,7 @@ - [[zoom]](#zoom) - [[crash-reporter]](#crash-reporter) - [[single-instance]](#single-instance) + - [[tray]](#tray) - [[basic-auth-username]](#basic-auth-username) - [[basic-auth-password]](#basic-auth-username) - [Programmatic API](#programmatic-api) @@ -410,6 +411,14 @@ Sets a default zoom factor to be used when the app is opened, defaults to `1.0`. Prevents application from being run multiple times. If such an attempt occurs the already running instance is brought to front. +#### [tray] + +``` +--tray +``` + +Application will stay as an icon in the system tray. Prevents application from being closed from clicking the window close button. + #### [basic-auth-username] ``` @@ -418,6 +427,7 @@ Prevents application from being run multiple times. If such an attempt occurs th Set basic http(s) auth via the command line to have the app automatically log you in to a protected site. Both fields are required if one is set. + #### [processEnvs] ``` diff --git a/src/build/buildApp.js b/src/build/buildApp.js index 4008db8..01b903d 100644 --- a/src/build/buildApp.js +++ b/src/build/buildApp.js @@ -46,6 +46,7 @@ function selectAppArgs(options) { win32metadata: options.win32metadata, versionString: options.versionString, processEnvs: options.processEnvs, + tray: options.tray, basicAuthUsername: options.basicAuthUsername, basicAuthPassword: options.basicAuthPassword, }; diff --git a/src/cli.js b/src/cli.js index 186f24d..e093700 100755 --- a/src/cli.js +++ b/src/cli.js @@ -82,6 +82,7 @@ if (require.main === module) { .option('--crash-reporter ', 'remote server URL to send crash reports') .option('--single-instance', 'allow only a single instance of the application') .option('--processEnvs ', 'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened.', getProcessEnvs) + .option('--tray', 'allow app to stay in system tray') .option('--basic-auth-username ', 'basic http(s) auth username') .option('--basic-auth-password ', 'basic http(s) auth password') .parse(process.argv); diff --git a/src/options/optionsMain.js b/src/options/optionsMain.js index 3574219..e6207a7 100644 --- a/src/options/optionsMain.js +++ b/src/options/optionsMain.js @@ -66,6 +66,7 @@ export default function (inpOptions) { FileDescription: inpOptions.name, }, processEnvs: inpOptions.processEnvs, + tray: inpOptions.tray || false, basicAuthUsername: inpOptions.basicAuthUsername || null, basicAuthPassword: inpOptions.basicAuthPassword || null, }; @@ -102,4 +103,3 @@ export default function (inpOptions) { return asyncConfig(options); } -