diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c408a22
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# http://EditorConfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..30680eb
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,14 @@
+{
+ "root": true,
+ "extends": "airbnb-base",
+ "env": {
+ "browser": true,
+ "node": true
+ },
+ "rules": {
+ "import/extensions": 0,
+ "import/no-extraneous-dependencies": 0,
+ "import/no-unresolved": [2, { "ignore": ["electron"] }],
+ "no-param-reassign": ["error", { "props": false }]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 2909082..34a9938 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
-dist
node_modules
-npm-debug.log
-debug.log
+*.log
.DS_Store
+dist
diff --git a/app/elements/action-button/action-button.html b/app/elements/action-button/action-button.html
new file mode 100644
index 0000000..b7a4361
--- /dev/null
+++ b/app/elements/action-button/action-button.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/app/elements/action-button/action-button.js b/app/elements/action-button/action-button.js
new file mode 100644
index 0000000..8595ae5
--- /dev/null
+++ b/app/elements/action-button/action-button.js
@@ -0,0 +1,105 @@
+/* eslint-disable no-underscore-dangle */
+
+class ActionButton extends HTMLElement {
+
+ static get observedAttributes() {
+ return ['label', 'icon'];
+ }
+
+ constructor() {
+ super();
+
+ const ownerDocument = document.currentScript.ownerDocument;
+ const template = ownerDocument.querySelector('#action-button-template');
+ const shadowRoot = this.attachShadow({ mode: 'open' });
+ shadowRoot.appendChild(template.content.cloneNode(true));
+
+ this._components = {
+ container: shadowRoot.querySelector('.container'),
+ button: shadowRoot.querySelector('.button'),
+ label: shadowRoot.querySelector('.label'),
+ icon: shadowRoot.querySelector('.icon'),
+ };
+ this._action = undefined;
+ }
+
+ getAttributeValue(attributeName) {
+ return this.hasAttribute(attributeName) ? this.getAttribute(attributeName) : '';
+ }
+
+ get label() {
+ return this.getAttributeValue(this._components.label.className);
+ }
+
+ set label(label) {
+ if (!label) {
+ return;
+ }
+
+ this._components.label.textContent = label;
+ }
+
+ get icon() {
+ return this.getAttributeValue(this._components.icon.className);
+ }
+
+ set icon(icon) {
+ if (!icon) {
+ return;
+ }
+
+ this._components.icon.setAttribute('d', icon);
+ }
+
+ connectedCallback() {
+ // Action is a noop function by default
+ this.setAction(() => {});
+ }
+
+ disconnectedCallback() {
+ // Remove action from element on callback
+ this.removeAction();
+ }
+
+ attributeChangedCallback(attributeName, oldValue, newValue) {
+ if (attributeName === this._components.label.className) {
+ this.label = newValue;
+ // Must check with baseVal property for icon because the className is a SVGAnimatedString object
+ } else if (attributeName === this._components.icon.className.baseVal) {
+ this.icon = newValue;
+ }
+ }
+
+ setAction(action) {
+ if (typeof action !== 'function') {
+ return;
+ }
+
+ // Remove the current action and replace it with the new one
+ this.removeAction();
+ this._action = action;
+ this._components.button.addEventListener('click', this._action);
+ }
+
+ removeAction() {
+ this._components.button.removeEventListener('click', this._action);
+ }
+
+ hideButton() {
+ this.setHidden(true);
+ }
+
+ showButton() {
+ this.setHidden(false);
+ }
+
+ setHidden(hidden) {
+ if (typeof hidden !== 'boolean') {
+ return;
+ }
+
+ this._components.container.style.display = hidden ? 'none' : '';
+ }
+}
+
+window.customElements.define('action-button', ActionButton);
diff --git a/app/main.js b/app/main.js
index aa9ddd0..f593771 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,42 +1,65 @@
-const {app, BrowserWindow, ipcMain} = require('electron');
+const { app, BrowserWindow, ipcMain } = require('electron');
-let mainWindow, setupWindow, pauseWindow;
+/** @constant { boolean }
+ *
+ * Checks if the current environment is in development by looking for the asar package.
+ * If the package exists, then the app is in production mode.
+ * Otherwise (if it is not found), then the program is in development mode.
+ */
+const DEVELOPMENT = process.mainModule.filename.indexOf('app.asar') === -1;
+
+let mainWindow = null;
+let setupWindow = null;
+let pauseWindow = null;
+
+const initializeWindow = (windowType, windowName) => {
+ windowType.loadURL(`file://${__dirname}/views/${windowName}.html`);
+ windowType.setMenu(null);
+ if (DEVELOPMENT) {
+ windowType.webContents.openDevTools();
+ }
+
+ windowType.once('ready-to-show', windowType.show);
+};
+
+const exitApp = () => {
+ // Do not exit the program on macOS (standard OS-specific behaviour).
+ // Instead, lose app focus and close all open windows.
+ if (process.platform === 'darwin') {
+ app.hide();
+ BrowserWindow.getAllWindows().forEach(win => win.close());
+ } else {
+ app.quit();
+ }
+};
-function createMainWindow () {
+const createMainWindow = () => {
mainWindow = new BrowserWindow({
fullscreen: true,
- frame: false
+ frame: false,
});
- mainWindow.loadURL(`file://${__dirname}/views/index.html`);
- mainWindow.setMenu(null);
-
- mainWindow.once('ready-to-show', mainWindow.show);
+ initializeWindow(mainWindow, 'index');
- mainWindow.on('closed', () => mainWindow = null);
-}
+ mainWindow.on('closed', () => (mainWindow = null));
+};
-function createSetupModalWindow () {
+const createSetupModalWindow = () => {
setupWindow = new BrowserWindow({
parent: mainWindow,
modal: true,
minWidth: 400,
minHeight: 300,
- frame: false
+ frame: false,
});
- setupWindow.loadURL(`file://${__dirname}/views/setup.html`);
- setupWindow.setMenu(null);
-
- setupWindow.once('ready-to-show', setupWindow.show);
+ initializeWindow(setupWindow, 'setup');
setupWindow.on('close', exitApp);
- setupWindow.on('closed', () => setupWindow = null);
-}
+ setupWindow.on('closed', () => (setupWindow = null));
+};
-function createPauseModalWindow () {
- mainWindow.webContents.executeJavaScript(
- 'document.body.classList.add(\'dim\')'
- );
+const createPauseModalWindow = () => {
+ mainWindow.webContents.executeJavaScript('document.body.classList.add(\'dim\')');
pauseWindow = new BrowserWindow({
parent: mainWindow,
@@ -45,38 +68,22 @@ function createPauseModalWindow () {
height: 250,
resizable: false,
closable: false,
- frame: false
+ frame: false,
});
- pauseWindow.loadURL(`file://${__dirname}/views/pause.html`);
- pauseWindow.setMenu(null);
-
- pauseWindow.once('ready-to-show', pauseWindow.show);
+ initializeWindow(pauseWindow, 'pause');
pauseWindow.on('close', exitApp);
pauseWindow.on('closed', () => {
- mainWindow.webContents.executeJavaScript(
- 'document.body.classList.remove(\'dim\')'
- );
+ mainWindow.webContents.executeJavaScript('document.body.classList.remove(\'dim\')');
pauseWindow = null;
});
-}
+};
-function createStartWindows () {
+const createStartWindows = () => {
createMainWindow();
createSetupModalWindow();
-}
-
-function exitApp () {
- // Do not exit the program on macOS (standard OS-specific behaviour).
- // Instead, lose app focus and close all open windows.
- if (process.platform === 'darwin') {
- app.hide();
- BrowserWindow.getAllWindows().forEach(win => win.close());
- } else {
- app.quit();
- }
-}
+};
app.on('ready', createStartWindows);
@@ -84,9 +91,7 @@ app.on('window-all-closed', exitApp);
app.on('activate', createStartWindows);
-ipcMain.on('setup-timer', (evt, settings) =>
- mainWindow.webContents.send('start-timer', settings)
-);
+ipcMain.on('setup-timer', (evt, settings) => mainWindow.webContents.send('start-timer', settings));
ipcMain.on('pause', createPauseModalWindow);
@@ -94,15 +99,15 @@ ipcMain.on('pause', createPauseModalWindow);
* Called after the pause window has been opened and it is safe to wait for a
* synchronous reply before continuing the counter.
* There is undoubtedly a better way of handling pause, but this works for now.
- *
+ *
* @return the false boolean value for the paused flag in mainWindow
*/
-ipcMain.on('pause-wait', evt => {
+ipcMain.on('pause-wait', (evt) => {
// If it has already been closed before this channel, then return immediately
- if (pauseWindow == null) {
+ if (pauseWindow === null) {
evt.returnValue = false;
} else {
- pauseWindow.on('closed', () => evt.returnValue = false);
+ pauseWindow.on('closed', () => (evt.returnValue = false));
}
});
diff --git a/app/package.json b/app/package.json
deleted file mode 100644
index 04101f3..0000000
--- a/app/package.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "station-timer",
- "productName": "Station Timer",
- "version": "1.0.0",
- "description": "A simple timer application that repeatedly counts down for a given amount of times",
- "author": "Ahmad Ouerfelli ",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "https://github.com/aouerfelli/station-timer"
- },
- "bugs": "https://github.com/aouerfelli/station-timer/issues",
- "main": "./main.js"
-}
\ No newline at end of file
diff --git a/app/renderers/index.js b/app/renderers/index.js
index e6d2532..21e481e 100644
--- a/app/renderers/index.js
+++ b/app/renderers/index.js
@@ -1,224 +1,267 @@
-const {ipcRenderer, remote} = require('electron');
-const webContents = remote.getCurrentWebContents();
+const { ipcRenderer, remote } = require('electron');
+const numberToWords = require('number-to-words');
-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('../assets/audio/beep.wav');
+// Object that caches all the DOM elements used
+const domElements = {
+ remaining: document.querySelector('#remaining'),
+ counter: document.querySelector('#counter'),
+ progress: document.querySelector('#progress'),
+ info: document.querySelector('#info'),
+ buttons: {
+ pause: document.querySelector('#pause'),
+ restart: document.querySelector('#restart'),
+ muteOn: document.querySelector('#mute-on'),
+ muteOff: document.querySelector('#mute-off'),
+ exit: document.querySelector('#exit'),
+ },
+ audio: {
+ beep: document.querySelector('#beep'),
+ },
+};
// Object containing strings used in the counter
const text = {
- counterText: {
- end: '0'
+ remaining: {
+ multiple: ' stations remaining',
+ single: 'Last station',
+ none: 'No more stations',
},
- infoText: {
+ counter: {
+ end: '0',
+ },
+ info: {
active: 'Complete your activity',
coolDown: 'Go to your next station',
- complete: 'Return to your original station'
- }
+ complete: 'Return to your original station',
+ },
};
// Object containing values for duration, break duration and number of repeats
-let settings;
+let settings = null;
// Flag indicating whether or not the program is currently in a paused state
let paused = false;
-pauseButton.addEventListener('click', () => {
- paused = true;
- ipcRenderer.send('pause');
-});
-
-restartButton.addEventListener('click', () =>
- 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 = '';
-});
+const toSentenceCase = str =>
+ // If a string is given and it is not empty, convert it to a "Sentence case" string
+ ((typeof str === 'string' && str.length > 0) ?
+ str.charAt(0).toUpperCase() + str.substring(1).toLowerCase() :
+ '');
-exitButton.addEventListener('click', () =>
- ipcRenderer.send('exit')
-);
+const ensureArray = arr =>
+ // If a single element is given, place it in an array
+ (Array.isArray(arr) ? arr : [arr]);
-counterTextView.addEventListener('click', () => {
- // Since the counter has no pointer events when counting, this will only
- // trigger at the end when the end class is added to the counter, which
- // enables pointer events.
- ipcRenderer.send('exit');
-});
+const optionalCallback = func =>
+ // Executes a function if it is given and if not then a noop function is executed
+ (typeof func === 'function' ? func : () => {})();
-/**
- * This function allows us to use Promises with generator functions, much like
- * the async/await feature in ES7 (not supported in Electron v1.4.13). This
- * allows us to write asyncronous code that looks similar to syncronous code.
- * The basis for this function was derived from Jake Archibald's
- * [JavaScript Promises: an Introduction]{@link https://developers.google.com/web/fundamentals/getting-started/primers/promises#bonus_round_promises_and_generators}.
- *
- * @param generatorFn the generator function that will yield Promises.
- */
-function async (generatorFn) {
- function continuer(verb, arg) {
- let result;
- try {
- result = generator[verb](arg);
- } catch (err) {
- return Promise.reject(err);
- }
- return result.done ?
- result.value :
- Promise.resolve(result.value).then(onResolved, onRejected);
- }
+const skipTransition = (elements, action) => {
+ optionalCallback(action);
+ ensureArray(elements).forEach((el) => {
+ el.classList.add('skip-transition');
+ (() => el.offsetHeight)(); // Trigger CSS reflow to flush changes
+ el.classList.remove('skip-transition');
+ });
+};
- let generator = generatorFn();
- let onResolved = continuer.bind(continuer, 'next');
- let onRejected = continuer.bind(continuer, 'throw');
- return onResolved();
-}
+const mute = () => {
+ remote.getCurrentWebContents().setAudioMuted(true);
+ domElements.buttons.muteOn.hideButton();
+ domElements.buttons.muteOff.showButton();
+};
-function skipTransition (elements, action) {
- // If a single element is given, place it in an array
- if (elements.constructor !== Array) {
- elements = [elements];
- }
- // Run the action function if it is given
- if (typeof action === 'function') {
- action();
- }
- elements.forEach(element => {
- element.classList.add('skip-transition');
- element.offsetHeight; // Trigger CSS reflow to flush changes
- element.classList.remove('skip-transition');
- });
-}
+const unmute = () => {
+ remote.getCurrentWebContents().setAudioMuted(false);
+ domElements.buttons.muteOff.hideButton();
+ domElements.buttons.muteOn.showButton();
+};
-function setProgressBar () {
- skipTransition(secondProgressBar, () =>
- secondProgressBar.classList.remove('expand'));
- secondProgressBar.classList.add('expand');
-}
+const setProgressBar = () => {
+ skipTransition(domElements.progress, () =>
+ domElements.progress.classList.remove('expand'));
+ domElements.progress.classList.add('expand');
+};
-function getFormattedTime (seconds) {
- // Get units of time (from seconds up to hours)
+const getFormattedTime = (seconds) => {
+ // --- Get units of time (from seconds up to hours) ---
let hh = parseInt(seconds / 3600, 10);
let mm = parseInt((seconds % 3600) / 60, 10);
let ss = parseInt(seconds % 60, 10);
- // Displaying or hiding units based on length of time (up to hours)
- hh = hh > 0 ? hh + ':' : '';
- mm = hh === '' && mm <= 0 ? '' :
- hh !== '' && mm < 10 ? '0' + mm + ':' : mm + ':';
- ss = mm === '' ? ss :
- ss < 10 ? '0' + ss : ss;
+ // --- Displaying or hiding units based on length of time (up to hours) ---
+ // Hours
+ if (hh > 0) {
+ hh = `${hh}:`;
+ } else {
+ hh = '';
+ }
+ // Minutes
+ if (hh === '' && mm <= 0) {
+ mm = '';
+ } else if (hh !== '' && mm < 10) {
+ mm = `0${mm}:`;
+ } else {
+ mm = `${mm}:`;
+ }
+ // Seconds
+ if (mm !== '' && ss < 10) {
+ ss = `0${ss}`;
+ }
- return hh + mm + ss;
-}
+ return `${hh}${mm}${ss}`;
+};
-function sleep (ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
-}
+const sleep = ms =>
+ new Promise(resolve => setTimeout(resolve, ms));
-function pauseWait () {
+const 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 () {
- // Run the onEachSecond function if it is given
- if (typeof onEachSecond === 'function') {
- onEachSecond();
- }
- view.textContent = getFormattedTime(duration--);
- }
+ Promise.resolve(paused = ipcRenderer.sendSync('pause-wait'));
+
+const countdown = (duration, counterView, onEachSecond) => {
+ let currentSecond = duration;
- return Promise.resolve(async(function* () {
- // We'll be decrementing duration each second in action()
- while (duration > 0) {
+ const action = () => {
+ optionalCallback(onEachSecond);
+ counterView.textContent = getFormattedTime(currentSecond);
+ currentSecond -= 1;
+ };
+
+ return Promise.resolve((async () => {
+ // The current second will be decremented each second in the action function
+ while (currentSecond > 0) {
+ /* eslint-disable no-await-in-loop */
if (paused) {
- yield pauseWait();
+ // Since the loop should not continue when in a paused state,
+ // the loop is blocked until the pause promise is resolved
+ await pauseWait();
} else {
action();
- yield sleep(1000);
+ // After completing the countdown decrementing action,
+ // the loop must be blocked for a second (1000ms) before resuming the countdown
+ await sleep(1000);
}
+ /* eslint-enable no-await-in-loop */
}
- // Check for pause before ending countdown
+
+ // Check for pause request before ending the countdown
if (paused) {
- yield pauseWait();
+ await pauseWait();
}
- }));
-}
+ })());
+};
+
+domElements.buttons.pause.setAction(() => {
+ paused = true;
+ ipcRenderer.send('pause');
+});
+
+domElements.buttons.restart.setAction(() => remote.getCurrentWebContents().send('start-timer', settings));
+
+domElements.buttons.muteOn.setAction(() => {
+ mute();
+});
+
+domElements.buttons.muteOff.setAction(() => {
+ unmute();
+});
+
+domElements.buttons.exit.setAction(() => ipcRenderer.send('exit'));
+
+// Since the counter has no pointer events when counting, this will only
+// trigger at the end when the end class is added to the counter, which
+// enables pointer events.
+domElements.counter.addEventListener('click', () => ipcRenderer.send('exit'));
ipcRenderer.on('start-timer', (evt, userSettings) => {
- let {duration, breakDuration, numRepeats} = settings = userSettings;
+ const { duration, breakDuration, numRepeats } = userSettings;
+ settings = userSettings;
- function resetTimer () {
+ const resetTimer = () => {
+ // Set muted button based on whether or not the audio is mutedd
+ if (remote.getCurrentWebContents().isAudioMuted()) {
+ mute();
+ } else {
+ unmute();
+ }
// Reset elements to their intended initial visibility
- restartButton.parentElement.style.display = 'none';
- pauseButton.parentElement.style.display = '';
+ domElements.buttons.restart.hideButton();
+ domElements.buttons.pause.showButton();
// Remove all classes from the views
document.body.classList = '';
- counterTextView.classList = '';
- secondProgressBar.classList = '';
- infoTextView.classList = '';
- }
+ domElements.counter.classList = '';
+ domElements.progress.classList = '';
+ domElements.info.classList = '';
+ };
- function durationCountdown () {
- counterTextView.classList.remove('red');
- counterTextView.classList.add('primary');
- secondProgressBar.classList.remove('red');
- infoTextView.textContent = text.infoText.active;
- return countdown(duration, counterTextView, setProgressBar);
- }
+ const durationCountdown = () => {
+ domElements.counter.classList.remove('red');
+ domElements.counter.classList.add('primary');
+ domElements.progress.classList.remove('red');
+ domElements.info.textContent = text.info.active;
+ return countdown(duration, domElements.counter, setProgressBar);
+ };
- function breakDurationCountdown () {
- counterTextView.classList.remove('primary');
- counterTextView.classList.add('red');
- secondProgressBar.classList.add('red');
- infoTextView.textContent = text.infoText.coolDown;
- return countdown(breakDuration, counterTextView, () => {
- beepAudio.play();
+ const breakDurationCountdown = () => {
+ domElements.counter.classList.remove('primary');
+ domElements.counter.classList.add('red');
+ domElements.progress.classList.add('red');
+ domElements.info.textContent = text.info.coolDown;
+ return countdown(breakDuration, domElements.counter, () => {
+ domElements.audio.beep.play();
setProgressBar();
});
- }
+ };
- function endTimer () {
+ const endTimer = () => {
// Setting end classes
- secondProgressBar.classList.add('remove');
- skipTransition(counterTextView, () =>
- counterTextView.classList.remove('red'));
- counterTextView.classList.add('end');
+ domElements.progress.classList.add('remove');
+ skipTransition(domElements.counter, () =>
+ domElements.counter.classList.remove('red'));
+ domElements.counter.classList.add('end');
// Setting end text to views
- counterTextView.textContent = text.counterText.end;
- infoTextView.textContent = text.infoText.complete;
+ domElements.counter.textContent = text.counter.end;
+ domElements.info.textContent = text.info.complete;
// Setting end visibility for Action Buttons
- pauseButton.parentElement.style.display = 'none';
- restartButton.parentElement.style.display = '';
- }
+ domElements.buttons.pause.hideButton();
+ domElements.buttons.restart.showButton();
+ };
- async(function* () {
+ const setRemainingText = (stationsLeft) => {
+ let stationsLeftText = '';
+ // Determine text to be displayed on the stations remaining counter
+ // The text is based on the number of stations left
+ if (stationsLeft === 0) {
+ stationsLeftText = text.remaining.none;
+ } else if (stationsLeft === 1) {
+ stationsLeftText = text.remaining.single;
+ } else {
+ // Concatenate the word form of the number of stations left with the text
+ stationsLeftText = toSentenceCase(numberToWords.toWords(stationsLeft));
+ stationsLeftText += text.remaining.multiple;
+ }
+ // Set the calculated text to the stations remaining counter view
+ domElements.remaining.textContent = stationsLeftText;
+ };
+
+ const countdownAction = async () => {
+ await durationCountdown();
+ await breakDurationCountdown();
+ };
+
+ (async () => {
resetTimer();
- // Start the timer: repeat for however many stations there are
- for (let i = 0; i < numRepeats; i++) {
- yield durationCountdown();
- yield breakDurationCountdown();
+ for (let stationsLeft = numRepeats; stationsLeft > 0; stationsLeft -= 1) {
+ setRemainingText(stationsLeft);
+ // Since we do not want the loop to continue until the cooldown promise is resolved,
+ // we wait for the asynchronous code to complete before moving on to the next iteration
+ await countdownAction(); // eslint-disable-line no-await-in-loop
+ // This is called here to update the counter text before the loop is broken
+ setRemainingText(stationsLeft - 1);
}
endTimer();
- });
+ })();
});
diff --git a/app/renderers/pause.js b/app/renderers/pause.js
index bccf204..1ee9e07 100644
--- a/app/renderers/pause.js
+++ b/app/renderers/pause.js
@@ -1,11 +1,9 @@
-const {remote} = require('electron');
+const { remote } = require('electron');
-const resumeButton = document.getElementById('resume');
-
-resumeButton.addEventListener('click', () => {
- let win = remote.getCurrentWindow();
+document.querySelector('#resume').addEventListener('click', () => {
+ const win = remote.getCurrentWindow();
// Lose focus before closing setup modal to prevent screen flash
win.blur();
// Destroy it directly since this is a non-closeable window
win.destroy();
-});
\ No newline at end of file
+});
diff --git a/app/renderers/setup.js b/app/renderers/setup.js
index bc0abff..fae1d42 100644
--- a/app/renderers/setup.js
+++ b/app/renderers/setup.js
@@ -1,56 +1,50 @@
-const {ipcRenderer, remote} = require('electron');
+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');
+const setupFormInputs = Array.from(setupForm.querySelectorAll('input[type="number"]'));
-function checkValidInput (value, min, max) {
+const 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 !== '' &&
+ value !== '' &&
(Number.isSafeInteger(Number(value))
&& (min === '' ||
parseInt(value, 10) >= parseInt(min, 10))
&& (max === '' ||
parseInt(value, 10) <= parseInt(max, 10)));
-}
-setupFormInputs.forEach(input => {
+setupFormInputs.forEach((input) => {
input.addEventListener('input', () => {
- let valid = checkValidInput(input.value, input.min, input.max);
+ const valid = checkValidInput(input.value, input.min, input.max);
input.classList.toggle('error', !valid);
});
});
-setupForm.addEventListener('submit', evt => {
+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) {
+ if (setupFormInputs.every((input) => {
+ const invalid = input.classList.contains('error');
+ // If any of the form inputs contain errors, focus on it
+ if (invalid) input.focus();
+ return invalid;
+ })) {
return;
}
- ipcRenderer.send(
- 'setup-timer',
+ ipcRenderer.send('setup-timer',
+ // An object is created from the form data
+ // The names of the form fields are used as keys
+ // The values of the fields are parsed from the supplied string to an integer
Object.assign(...Array.from(new FormData(setupForm))
- .map(([k, v]) => ({ [k]: v })))
- );
+ .map(([k, v]) => ({ [k]: parseInt(v, 10) }))));
- let win = remote.getCurrentWindow();
+ const win = remote.getCurrentWindow();
// Lose focus before closing setup modal to prevent screen flash
win.blur();
// Destroy it directly to bypass close event if not closed
win.destroy();
});
-exitButton.addEventListener('click', () => ipcRenderer.send('exit'));
\ No newline at end of file
+document.querySelector('#exit').setAction(() => ipcRenderer.send('exit'));
diff --git a/app/styles/base.css b/app/styles/base.css
index 704694e..e8f59cb 100644
--- a/app/styles/base.css
+++ b/app/styles/base.css
@@ -177,86 +177,9 @@ button[type='submit'] svg {
margin-left: 0.5rem;
}
-/* Action Button styles for all pages (would preferably be a Custom Element, but v1 of the API is not currently supported (Electron v1.4.13)) */
-
+/* Action Button container positioning */
.action-buttons {
position: fixed;
bottom: 0;
right: 0;
}
-
-.action-buttons div {
- display: inline-flex;
- flex-direction: column-reverse;
- align-items: center;
- justify-content: center;
-}
-
-.action-buttons div button {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0.375rem;
- margin: 0.75rem;
- margin-top: 0;
- outline: none;
- border: none;
- background-color: transparent;
-}
-
-.action-buttons div button::before {
- content: '';
- position: absolute;
- z-index: -1;
- width: 0;
- height: 0;
- border-radius: 50%;
- background-color: var(--color-grey);
- opacity: 0;
- transition: all 0.3s var(--curve-standard);
- transition-property: width, height, opacity;
- /* Since there are not that many buttons on each page, it's fine to give each of them a layer initially instead of dynamically */
- will-change: width, height, opacity;
-}
-
-.action-buttons div button:active::before {
- width: 3rem;
- height: 3rem;
- opacity: 0.3;
-}
-
-.action-buttons div button svg {
- width: 2.25rem;
- height: 2.25rem;
- fill: var(--color-grey);
- transition: fill 0.3s var(--curve-standard);
- will-change: fill;
-}
-
-.action-buttons div button:focus svg {
- fill: var(--color-primary-light);
-}
-
-.action-buttons div button:hover svg {
- fill: var(--color-primary);
-}
-
-.action-buttons div button:active svg {
- fill: var(--color-primary-dark);
-}
-
-.action-buttons div p {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- font-size: 0.8em;
- border-radius: var(--corner-radius);
- color: var(--font-color-light);
- background-color: var(--font-color-dark);
- opacity: 0;
- transition: opacity 0.3s var(--curve-sharp);
-}
-
-.action-buttons div button:hover ~ p {
- opacity: 0.7;
-}
diff --git a/app/styles/index.css b/app/styles/index.css
index 0e534c7..2ee23f2 100644
--- a/app/styles/index.css
+++ b/app/styles/index.css
@@ -23,6 +23,11 @@ body.dim::after {
background-color: rgba(0, 0, 0, 0.5);
}
+#remaining {
+ font-size: 1.25em;
+ opacity: 0.75;
+}
+
#counter {
--color-counter: var(--color-grey);
color: var(--color-counter);
@@ -65,7 +70,7 @@ body.dim::after {
--color-progress: var(--color-primary);
background-color: var(--color-progress);
width: 0;
- height: 0.25rem;
+ height: 0.3125rem;
margin: auto;
margin-bottom: 1.5rem;
border-radius: var(--corner-radius);
diff --git a/app/views/index.html b/app/views/index.html
index a64c6ea..f8d5b8d 100644
--- a/app/views/index.html
+++ b/app/views/index.html
@@ -1,71 +1,31 @@
-
+
Station Timer
+
+
+
+
-
-