From 70ab8ed89fcd1a823bb9d84c6e4e87f770e1a71b Mon Sep 17 00:00:00 2001 From: aouerfelli Date: Tue, 3 Jan 2017 14:46:25 -0500 Subject: [PATCH] Initial release (v1.0.0) Edited README with link to latest release and screenshots Added mute control for beep audio Removed external JSON string functionality Prevent form from submitting if there are errors Some refactoring and bug fixes --- README.md | 5 +- app/{res => assets}/audio/beep.wav | Bin app/{res => assets}/fonts/Roboto-Bold.woff2 | Bin .../fonts/Roboto-Regular.woff2 | Bin app/main.js | 13 ++- app/renderers/index.js | 75 ++++++++++-------- app/renderers/setup.js | 40 +++++++--- app/res/strings.json | 10 --- app/styles/{_base.css => base.css} | 15 ++-- app/styles/index.css | 21 +++-- app/views/index.html | 20 ++++- app/views/pause.html | 2 +- app/views/setup.html | 8 +- build/background.tiff | Bin 0 -> 638968 bytes build/icon.icns | Bin 145750 -> 155176 bytes build/icon.ico | Bin 370070 -> 370070 bytes build/icon.png | Bin 0 -> 59926 bytes package.json | 5 +- screenshots.gif | Bin 0 -> 37941 bytes 19 files changed, 121 insertions(+), 93 deletions(-) rename app/{res => assets}/audio/beep.wav (100%) rename app/{res => assets}/fonts/Roboto-Bold.woff2 (100%) rename app/{res => assets}/fonts/Roboto-Regular.woff2 (100%) delete mode 100644 app/res/strings.json rename app/styles/{_base.css => base.css} (93%) create mode 100644 build/background.tiff create mode 100644 build/icon.png create mode 100644 screenshots.gif diff --git a/README.md b/README.md index ff2431f..c3ca8ee 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Station Timer +[![GitHub release](https://img.shields.io/badge/download-latest-blue.svg)](https://github.com/aouerfelli/station-timer/releases/latest) A minimal timer application built with [Electron](http://electron.atom.io) that counts down and loops for a given number of stations. This was originally @@ -7,7 +8,9 @@ stations set up and would allocate a certain amount of time to stay at each station, and would then give some additional time to clean up and go to the next station. -## Releases (NOT AVAILABE YET) +![](screenshots.gif) + +## Releases There are builds available for Windows and macOS, since those are the only platforms I am able to use and test on at the moment. If you would like to build for your own platform, you can follow the steps [below](#Build). diff --git a/app/res/audio/beep.wav b/app/assets/audio/beep.wav similarity index 100% rename from app/res/audio/beep.wav rename to app/assets/audio/beep.wav diff --git a/app/res/fonts/Roboto-Bold.woff2 b/app/assets/fonts/Roboto-Bold.woff2 similarity index 100% rename from app/res/fonts/Roboto-Bold.woff2 rename to app/assets/fonts/Roboto-Bold.woff2 diff --git a/app/res/fonts/Roboto-Regular.woff2 b/app/assets/fonts/Roboto-Regular.woff2 similarity index 100% rename from app/res/fonts/Roboto-Regular.woff2 rename to app/assets/fonts/Roboto-Regular.woff2 diff --git a/app/main.js b/app/main.js index cec030d..aa9ddd0 100644 --- a/app/main.js +++ b/app/main.js @@ -2,7 +2,7 @@ const {app, BrowserWindow, ipcMain} = require('electron'); let mainWindow, setupWindow, pauseWindow; -function createMainWindow() { +function createMainWindow () { mainWindow = new BrowserWindow({ fullscreen: true, frame: false @@ -15,7 +15,7 @@ function createMainWindow() { mainWindow.on('closed', () => mainWindow = null); } -function createSetupModalWindow() { +function createSetupModalWindow () { setupWindow = new BrowserWindow({ parent: mainWindow, modal: true, @@ -33,7 +33,7 @@ function createSetupModalWindow() { setupWindow.on('closed', () => setupWindow = null); } -function createPauseModalWindow() { +function createPauseModalWindow () { mainWindow.webContents.executeJavaScript( 'document.body.classList.add(\'dim\')' ); @@ -62,16 +62,15 @@ function createPauseModalWindow() { }); } -function createStartWindows() { +function createStartWindows () { createMainWindow(); createSetupModalWindow(); } -function exitApp() { +function exitApp () { // Do not exit the program on macOS (standard OS-specific behaviour). - // Instead, close all open windows. + // Instead, lose app focus and close all open windows. if (process.platform === 'darwin') { - // Lose app and window focus before closing windows app.hide(); BrowserWindow.getAllWindows().forEach(win => win.close()); } else { diff --git a/app/renderers/index.js b/app/renderers/index.js index b90c653..e6d2532 100644 --- a/app/renderers/index.js +++ b/app/renderers/index.js @@ -1,33 +1,28 @@ const {ipcRenderer, remote} = require('electron'); +const webContents = remote.getCurrentWebContents(); const counterTextView = document.getElementById('counter'); const secondProgressBar = document.getElementById('progress'); const infoTextView = document.getElementById('info'); const pauseButton = document.getElementById('pause'); const restartButton = document.getElementById('restart'); +const muteOnButton = document.getElementById('mute-on'); +const muteOffButton = document.getElementById('mute-off'); const exitButton = document.getElementById('exit'); -const beepAudio = new Audio('../res/audio/beep.wav'); +const beepAudio = new Audio('../assets/audio/beep.wav'); // Object containing strings used in the counter -// I know it's pretty ugly, but I'm sure you've seen worse -const text = (() => { - try { - return require('./../res/strings.json'); - } catch (err) { - // If the JSON wasn't found or couldn't be parsed, then set default values - return { - counterText: { - end: '0' - }, - infoText: { - active: 'Complete your activity', - coolDown: 'Go to your next station', - complete: 'Return to your original station' - } - }; +const text = { + counterText: { + end: '0' + }, + infoText: { + active: 'Complete your activity', + coolDown: 'Go to your next station', + complete: 'Return to your original station' } -})(); +}; // Object containing values for duration, break duration and number of repeats let settings; @@ -41,9 +36,21 @@ pauseButton.addEventListener('click', () => { }); restartButton.addEventListener('click', () => - remote.getCurrentWebContents().send('start-timer', settings) + webContents.send('start-timer', settings) ); +muteOnButton.addEventListener('click', () => { + webContents.setAudioMuted(true); + muteOnButton.parentElement.style.display = 'none'; + muteOffButton.parentElement.style.display = ''; +}); + +muteOffButton.addEventListener('click', () => { + webContents.setAudioMuted(false); + muteOffButton.parentElement.style.display = 'none'; + muteOnButton.parentElement.style.display = ''; +}); + exitButton.addEventListener('click', () => ipcRenderer.send('exit') ); @@ -64,7 +71,7 @@ counterTextView.addEventListener('click', () => { * * @param generatorFn the generator function that will yield Promises. */ -function async(generatorFn) { +function async (generatorFn) { function continuer(verb, arg) { let result; try { @@ -83,7 +90,7 @@ function async(generatorFn) { return onResolved(); } -function skipTransition(elements, action) { +function skipTransition (elements, action) { // If a single element is given, place it in an array if (elements.constructor !== Array) { elements = [elements]; @@ -99,13 +106,13 @@ function skipTransition(elements, action) { }); } -function setProgressBar() { +function setProgressBar () { skipTransition(secondProgressBar, () => secondProgressBar.classList.remove('expand')); secondProgressBar.classList.add('expand'); } -function getFormattedTime(seconds) { +function getFormattedTime (seconds) { // Get units of time (from seconds up to hours) let hh = parseInt(seconds / 3600, 10); let mm = parseInt((seconds % 3600) / 60, 10); @@ -121,19 +128,19 @@ function getFormattedTime(seconds) { return hh + mm + ss; } -function sleep(ms) { +function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)); } -function waitPause() { +function pauseWait () { // The pause-wait channel will return a value of false when the pause modal is // closed, which we can set to the paused flag. When the flag is set, the // Promise will be resolved. return Promise.resolve(paused = ipcRenderer.sendSync('pause-wait')); } -function countdown(duration, view, onEachSecond) { - function action() { +function countdown (duration, view, onEachSecond) { + function action () { // Run the onEachSecond function if it is given if (typeof onEachSecond === 'function') { onEachSecond(); @@ -145,15 +152,15 @@ function countdown(duration, view, onEachSecond) { // We'll be decrementing duration each second in action() while (duration > 0) { if (paused) { - yield waitPause(); + yield pauseWait(); } else { action(); yield sleep(1000); } } - // Check before ending countdown + // Check for pause before ending countdown if (paused) { - yield waitPause(); + yield pauseWait(); } })); } @@ -161,7 +168,7 @@ function countdown(duration, view, onEachSecond) { ipcRenderer.on('start-timer', (evt, userSettings) => { let {duration, breakDuration, numRepeats} = settings = userSettings; - function resetTimer() { + function resetTimer () { // Reset elements to their intended initial visibility restartButton.parentElement.style.display = 'none'; pauseButton.parentElement.style.display = ''; @@ -172,7 +179,7 @@ ipcRenderer.on('start-timer', (evt, userSettings) => { infoTextView.classList = ''; } - function durationCountdown() { + function durationCountdown () { counterTextView.classList.remove('red'); counterTextView.classList.add('primary'); secondProgressBar.classList.remove('red'); @@ -180,7 +187,7 @@ ipcRenderer.on('start-timer', (evt, userSettings) => { return countdown(duration, counterTextView, setProgressBar); } - function breakDurationCountdown() { + function breakDurationCountdown () { counterTextView.classList.remove('primary'); counterTextView.classList.add('red'); secondProgressBar.classList.add('red'); @@ -191,7 +198,7 @@ ipcRenderer.on('start-timer', (evt, userSettings) => { }); } - function endTimer() { + function endTimer () { // Setting end classes secondProgressBar.classList.add('remove'); skipTransition(counterTextView, () => diff --git a/app/renderers/setup.js b/app/renderers/setup.js index 878f8da..bc0abff 100644 --- a/app/renderers/setup.js +++ b/app/renderers/setup.js @@ -1,20 +1,25 @@ const {ipcRenderer, remote} = require('electron'); const setupForm = document.forms.namedItem('setup'); +const setupFormInputs = Array.from( + setupForm.querySelectorAll('input[type="number"]') +); const exitButton = document.getElementById('exit'); -Array.from(document.querySelectorAll('input[type="number"]')).forEach(input => { - input.addEventListener('input', () => { - // The handling of empty inputs is done in CSS. If the input is not - // empty and the value is an empty string, that means the input is - // invalid. - let valid = input.value !== '' && - (Number.isInteger(Number(input.value)) - && (input.min === '' || - parseInt(input.value, 10) >= parseInt(input.min, 10)) - && (input.max === '' || - parseInt(input.value, 10) <= parseInt(input.max, 10))); +function checkValidInput (value, min, max) { + // The handling of empty inputs is done in CSS. If the input is not empty and + // the value is an empty string, that means the input is invalid. + return value !== '' && + (Number.isSafeInteger(Number(value)) + && (min === '' || + parseInt(value, 10) >= parseInt(min, 10)) + && (max === '' || + parseInt(value, 10) <= parseInt(max, 10))); +} +setupFormInputs.forEach(input => { + input.addEventListener('input', () => { + let valid = checkValidInput(input.value, input.min, input.max); input.classList.toggle('error', !valid); }); }); @@ -22,6 +27,19 @@ Array.from(document.querySelectorAll('input[type="number"]')).forEach(input => { setupForm.addEventListener('submit', evt => { evt.preventDefault(); + let valid = setupFormInputs.every(input => { + let invalid = input.classList.contains('error'); + if (invalid) { + // If any of the form inputs contain errors, focus on it + input.focus(); + } + return !invalid; + }); + // Return early (don't finish submitting the form) if not all inputs are valid + if (!valid) { + return; + } + ipcRenderer.send( 'setup-timer', Object.assign(...Array.from(new FormData(setupForm)) diff --git a/app/res/strings.json b/app/res/strings.json deleted file mode 100644 index 0e0a009..0000000 --- a/app/res/strings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "counterText": { - "end": "0" - }, - "infoText": { - "active": "Complete your activity", - "coolDown": "Go to your next station", - "complete": "Return to your original station" - } -} \ No newline at end of file diff --git a/app/styles/_base.css b/app/styles/base.css similarity index 93% rename from app/styles/_base.css rename to app/styles/base.css index f9025fd..704694e 100644 --- a/app/styles/_base.css +++ b/app/styles/base.css @@ -1,9 +1,9 @@ /* ================================================ * Accumulated styles common to all HTML documents * ================================================ */ - /* Note that this stylesheet will be loaded using HTML instead of being - * imported directly into the other stylesheets for performance reasons - * (importing does not load styles asynchronously). */ +/* Note that this stylesheet will be loaded using HTML instead of being + * imported directly into the other stylesheets for performance reasons + * (importing does not load styles asynchronously). */ /* ===================== * Local Fonts [*.html] @@ -15,7 +15,7 @@ font-style: normal; font-weight: 400; src: local('Roboto'), local('Roboto-Regular'), - url('../res/fonts/Roboto-Regular.woff2') format('woff2'); + url('../assets/fonts/Roboto-Regular.woff2') format('woff2'); } /* Roboto Bold (700) */ @@ -24,7 +24,7 @@ font-style: normal; font-weight: 700; src: local('Roboto Bold'), local('Roboto-Bold'), - url('../res/fonts/Roboto-Bold.woff2') format('woff2'); + url('../assets/fonts/Roboto-Bold.woff2') format('woff2'); } /* =================== @@ -33,8 +33,6 @@ :root { /* Colors */ - --color-statusbar: #E0E0E0; - --color-appbar: #F5F5F5; --color-background: #FAFAFA; --color-card: #FFF; --color-grey: #9E9E9E; @@ -62,7 +60,7 @@ --curve-sharp: cubic-bezier(0.4, 0.0, 0.6, 1); /* ease in out */ - /* Shadows */ + /* Shadows (from MaterializeCSS) */ --shadow-depth-1: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); --shadow-depth-2: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.3); --shadow-depth-3: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.3); @@ -252,7 +250,6 @@ button[type='submit'] svg { display: inline-block; padding: 0.25rem 0.5rem; font-size: 0.8em; - letter-spacing: 0.5px; border-radius: var(--corner-radius); color: var(--font-color-light); background-color: var(--font-color-dark); diff --git a/app/styles/index.css b/app/styles/index.css index a171e3b..0e534c7 100644 --- a/app/styles/index.css +++ b/app/styles/index.css @@ -10,25 +10,24 @@ body::after { left: 0; width: 100%; height: 100%; - opacity: 0; - transition: opacity 0.5s var(--curve-standard); - will-change: opacity; + background-color: transparent; + /* No transitions are added here to prioritize reliability */ + will-change: background-color; } body.hide::after { background-color: var(--color-background); - opacity: 1; } body.dim::after { - background-color: black; - opacity: 0.5; + background-color: rgba(0, 0, 0, 0.5); } #counter { --color-counter: var(--color-grey); color: var(--color-counter); - font-size: 25em; + /* Making sure font has same width on all screens since it will be changing constantly */ + font-size: 25vw; will-change: color; } @@ -51,15 +50,15 @@ body.dim::after { /* This will never trigger because a header is not focusable by default. It is * still declared just in case this behaviour changes. */ #counter.end:focus { - color: var(--color-primary-light); + --color-counter: var(--color-primary-light); } #counter.end:hover { - color: var(--color-primary); + --color-counter: var(--color-primary); } #counter.end:active { - color: var(--color-primary-dark); + --color-counter: var(--color-primary-dark); } #progress { @@ -95,5 +94,5 @@ body.dim::after { } #info { - font-size: 1.5em; + font-size: 2em; } diff --git a/app/views/index.html b/app/views/index.html index 759139f..a64c6ea 100644 --- a/app/views/index.html +++ b/app/views/index.html @@ -3,7 +3,7 @@ Station Timer - + @@ -39,6 +39,24 @@

Restart

+
+ +

Mute

+
+ +
+ +

Unmute

+
+