diff --git a/package.json b/package.json index 3a53e76b..9b8f4883 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "mob-timer", "version": "1.0.0", - "description": "", + "productName": "Mob Timer", + "description": "A timer for mob programming.", "main": "src/main.js", "scripts": { "start": "electron .", @@ -18,7 +19,7 @@ "type": "git", "url": "git+https://github.com/pluralsight/mob-timer.git" }, - "author": "", + "author": "Pluralsight", "license": "ISC", "bugs": { "url": "https://github.com/pluralsight/mob-timer/issues" diff --git a/src/main.js b/src/main.js old mode 100644 new mode 100755 index 773e5be2..051658d2 --- a/src/main.js +++ b/src/main.js @@ -15,6 +15,7 @@ app.on('ready', () => { if (timerState.getState().shuffleMobbersOnStartup) { timerState.shuffleMobbers() } + windows.createTrayIconAndMenu() }) function onTimerEvent(event, data) { @@ -26,15 +27,22 @@ function onTimerEvent(event, data) { ipc.on('timerWindowReady', () => timerState.initialize()) ipc.on('configWindowReady', () => timerState.publishConfig()) -ipc.on('fullscreenWindowReady', () => timerState.publishConfig()) +ipc.on('fullscreenWindowReady', () => { + timerState.stopAlerts() + timerState.publishConfig() +}) +ipc.on('reset', () => timerState.reset(true)) ipc.on('pause', () => timerState.pause()) ipc.on('unpause', () => timerState.start()) ipc.on('skip', () => timerState.rotate()) -ipc.on('startTurn', () => timerState.start()) +ipc.on('startTurn', () => { + windows.closeFullscreenWindow() + timerState.start() +}) ipc.on('configure', () => { - windows.showConfigWindow() windows.closeFullscreenWindow() + windows.showConfigWindow() }) ipc.on('shuffleMobbers', () => timerState.shuffleMobbers()) diff --git a/src/state/timer-state.js b/src/state/timer-state.js index 948e4454..f72adf06 100644 --- a/src/state/timer-state.js +++ b/src/state/timer-state.js @@ -52,8 +52,13 @@ class TimerState { }) } - reset() { + reset(stop = false) { + if (stop) { + this.mainTimer.pause() + this.callback('turnEnded') + } this.mainTimer.reset(this.secondsPerTurn) + this.stopAlerts() this.dispatchTimerChange(this.secondsPerTurn) } @@ -65,6 +70,7 @@ class TimerState { stopAlerts() { this.alertsTimer.pause() + this.alertsTimer.reset(0) this.callback('stopAlerts') } diff --git a/src/windows/img/trayIcon.png b/src/windows/img/trayIcon.png new file mode 100644 index 00000000..33698319 Binary files /dev/null and b/src/windows/img/trayIcon.png differ diff --git a/src/windows/img/trayIcon@2x.png b/src/windows/img/trayIcon@2x.png new file mode 100644 index 00000000..de53f9d0 Binary files /dev/null and b/src/windows/img/trayIcon@2x.png differ diff --git a/src/windows/img/trayIcon@3x.png b/src/windows/img/trayIcon@3x.png new file mode 100644 index 00000000..0bc51da6 Binary files /dev/null and b/src/windows/img/trayIcon@3x.png differ diff --git a/src/windows/menu-template.js b/src/windows/menu-template.js new file mode 100644 index 00000000..ff3ec98c --- /dev/null +++ b/src/windows/menu-template.js @@ -0,0 +1,119 @@ +const electron = require('electron') +let windows = require('./windows') +const isMac = process.platform === 'darwin' + +exports.appMenuTemplate = [ + ...(isMac ? [{ + role: 'appMenu', + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { + label: 'Preferences', + accelerator: 'CommandOrControl+,', + click() { windows.showConfigWindow() } + }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }] : []), + { + label: 'Timer', + submenu: [ + { + label: 'Start', + click() { windows.timerWindow.webContents.send('start') } + }, + { + label: 'Pause', + visible: false, + click() { windows.timerWindow.webContents.send('pause') } + }, + { + label: 'Reset', + visible: false, + click() { windows.timerWindow.webContents.send('reset') } + }, + { + label: 'Skip', + click() { windows.timerWindow.webContents.send('skip') } + } + ] + }, + ...(!isMac ? [ + { + label: 'Tools', + submenu: [ + { + label: 'Preferences', + accelerator: 'CommandOrControl+,', + click() { windows.showConfigWindow() } + } + ] + } + ] : []), + { + role: 'help', + submenu: [ + ...(!isMac ? [ + { role: 'about' }, + { type: 'separator' } + ] : []), + { + label: 'Learn More', + click() { electron.shell.openExternal('https://github.com/pluralsight/mob-timer') } + } + ] + } +] + +exports.trayMenuTemplate = [ + { role: 'about' }, + { + label: 'Timer', + submenu: [ + { + label: 'Start', + click() { windows.timerWindow.webContents.send('start') } + }, + { + label: 'Pause', + visible: false, + click() { windows.timerWindow.webContents.send('pause') } + }, + { + label: 'Reset', + visible: false, + click() { windows.timerWindow.webContents.send('reset') } + }, + { + label: 'Skip', + click() { windows.timerWindow.webContents.send('skip') } + } + ] + }, + { + label: 'Tools', + submenu: [ + { + label: 'Preferences', + accelerator: 'CommandOrControl+,', + click() { windows.showConfigWindow() } + } + ] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click() { electron.shell.openExternal('https://github.com/pluralsight/mob-timer') } + } + ] + }, + { role: 'quit' } +] diff --git a/src/windows/timer/index.js b/src/windows/timer/index.js old mode 100644 new mode 100755 index 8096c3dc..3c7cf595 --- a/src/windows/timer/index.js +++ b/src/windows/timer/index.js @@ -55,7 +55,13 @@ function drawTimerArc(seconds, maxSeconds) { drawArc(begin, end, theme.mobberBorderHighlightColor) } -ipc.on('rotated', (event, data) => { +function drawInitialState() { + drawOverlays(true) + containerEl.classList.remove('isPaused') + containerEl.classList.add('isTurnEnded') +} + +function drawMobbers(data) { if (!data.current) { data.current = { name: 'Add a mobber' } } @@ -67,29 +73,61 @@ ipc.on('rotated', (event, data) => { } nextPicEl.src = data.next.image || '../img/sad-cyclops.png' nextEl.innerHTML = data.next.name +} + +function drawOverlays(isPaused) { + if (!paused && isPaused) { + containerEl.classList.remove('isTurnEnded') + containerEl.classList.add('isPaused') + } + paused = isPaused + if (!isPaused) { + containerEl.classList.remove('isTurnEnded') + containerEl.classList.remove('isPaused') + toggleBtn.classList.remove('play') + toggleBtn.classList.add('pause') + } else { + toggleBtn.classList.add('play') + toggleBtn.classList.remove('pause') + } +} + +ipc.on('rotated', (event, data) => { + drawMobbers(data) + drawOverlays(true) +}) + +ipc.on('skip', () => { + skip() +}) + +ipc.on('reset', () => { + drawOverlays(true) + containerEl.classList.remove('isPaused') + containerEl.classList.add('isTurnEnded') + reset() +}) + +ipc.on('pause', () => { + drawOverlays(true) + pause() +}) + +ipc.on('start', () => { + drawOverlays(false) + start() }) ipc.on('paused', () => { - paused = true - containerEl.classList.add('isPaused') - toggleBtn.classList.add('play') - toggleBtn.classList.remove('pause') + drawOverlays(true) }) ipc.on('started', () => { - paused = false - containerEl.classList.remove('isPaused') - containerEl.classList.remove('isTurnEnded') - toggleBtn.classList.remove('play') - toggleBtn.classList.add('pause') + drawOverlays(false) }) ipc.on('turnEnded', () => { - paused = true - containerEl.classList.remove('isPaused') - containerEl.classList.add('isTurnEnded') - toggleBtn.classList.add('play') - toggleBtn.classList.remove('pause') + drawInitialState() }) ipc.on('configUpdated', (event, data) => { @@ -109,9 +147,25 @@ ipc.on('stopAlerts', () => { }) toggleBtn.addEventListener('click', () => { - paused ? ipc.send('unpause') : ipc.send('pause') + paused ? start() : pause() }) -nextBtn.addEventListener('click', () => ipc.send('skip')) +nextBtn.addEventListener('click', () => skip()) configureBtn.addEventListener('click', () => ipc.send('configure')) +function skip() { + ipc.send('skip') +} + +function reset() { + ipc.send('reset') +} + +function pause() { + ipc.send('pause') +} + +function start() { + ipc.send('unpause') +} + ipc.send('timerWindowReady') diff --git a/src/windows/windows.js b/src/windows/windows.js old mode 100644 new mode 100755 index acca3a71..f53c8a0c --- a/src/windows/windows.js +++ b/src/windows/windows.js @@ -1,9 +1,12 @@ const electron = require('electron') -const { app } = electron +const { app, BrowserWindow, Menu, Tray } = electron const windowSnapper = require('./window-snapper') const path = require('path') +const menuTemplate = require('./menu-template') +const isMac = process.platform === 'darwin' +const trayMenu = Menu.buildFromTemplate(menuTemplate.trayMenuTemplate) -let timerWindow, configWindow, fullscreenWindow +let tray, timerWindow, configWindow, fullscreenWindow let snapThreshold, secondsUntilFullscreen, timerAlwaysOnTop exports.createTimerWindow = () => { @@ -12,11 +15,13 @@ exports.createTimerWindow = () => { } let { width, height } = electron.screen.getPrimaryDisplay().workAreaSize - timerWindow = new electron.BrowserWindow({ - x: width - 220, - y: height - 90, - width: 220, - height: 90, + const timerWinWidth = 220 + const timerWinHeight = 90 + timerWindow = new BrowserWindow({ + x: width - timerWinWidth, + y: height - timerWinHeight, + width: timerWinWidth, + height: timerWinHeight, resizable: false, alwaysOnTop: timerAlwaysOnTop, frame: false, @@ -25,6 +30,7 @@ exports.createTimerWindow = () => { timerWindow.loadURL(`file://${__dirname}/timer/index.html`) timerWindow.on('closed', () => (timerWindow = null)) + createApplicationMenu() timerWindow.on('move', () => { if (snapThreshold <= 0) { @@ -46,6 +52,7 @@ exports.createTimerWindow = () => { timerWindow.setPosition(snapTo.x, snapTo.y) } }) + exports.timerWindow = timerWindow } exports.showConfigWindow = () => { @@ -61,7 +68,7 @@ exports.createConfigWindow = () => { return } - configWindow = new electron.BrowserWindow({ + configWindow = new BrowserWindow({ width: 420, height: 500, autoHideMenuBar: true @@ -101,8 +108,13 @@ exports.dispatchEvent = (event, data) => { if (event === 'alert' && data === secondsUntilFullscreen) { exports.createFullscreenWindow() } - if (event === 'stopAlerts') { - exports.closeFullscreenWindow() + if (isMac) { + if (event === 'started' || event === 'paused' || event === 'turnEnded') { + const menu = Menu.getApplicationMenu() + const timerMenu = isMac ? menu.items[1].submenu : menu.items[0].submenu + configureTimerMenu(event, timerMenu) + configureTimerMenu(event, trayMenu.items[1].submenu) + } } if (timerWindow) { @@ -129,9 +141,39 @@ exports.setConfigState = data => { } } +exports.createTrayIconAndMenu = () => { + if (isMac) { + if (!tray) { + tray = new Tray(path.join(__dirname, '/../windows/img/trayIcon.png')) + tray.setToolTip('Mob Timer') + } + tray.setContextMenu(trayMenu) + } +} + +function configureTimerMenu(event, timerMenu) { + switch (event) { + case 'started': + timerMenu.items[0].visible = false + timerMenu.items[1].visible = true + timerMenu.items[2].visible = true + break + case 'paused': + timerMenu.items[0].visible = true + timerMenu.items[1].visible = false + timerMenu.items[2].visible = true + break + case 'turnEnded': + timerMenu.items[0].visible = true + timerMenu.items[1].visible = false + timerMenu.items[2].visible = false + break + } +} + function createAlwaysOnTopFullscreenInterruptingWindow(options) { return whileAppDockHidden(() => { - const window = new electron.BrowserWindow(options) + const window = new BrowserWindow(options) window.setAlwaysOnTop(true, 'screen-saver') return window }) @@ -149,3 +191,11 @@ function whileAppDockHidden(work) { } return result } + +function createApplicationMenu() { + if (isMac) { + const template = menuTemplate.appMenuTemplate + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) + } +}