const storage = require("Storage"); const heatshrink = require("heatshrink"); const STATE_PATH = "stlap.state.json"; g.setFont("Vector", 24); const BUTTON_ICONS = { play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")), pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")), reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI=")) }; let state = storage.readJSON(STATE_PATH, 1); const STATE_DEFAULT = { wasRunning: false, //If the stopwatch was ever running since being reset sessionStart: 0, //When the stopwatch was first started running: false, //Whether the stopwatch is currently running startTime: 0, //When the stopwatch was last started. pausedTime: 0, //When the stopwatch was last paused. elapsedTime: 0 //How much time was spent running before the current start time. Update on pause. }; if (!state) { state = STATE_DEFAULT; } let lapFile; let lapHistory; if (state.wasRunning) { lapFile = 'stlap-' + state.sessionStart + '.json'; lapHistory = storage.readJSON(lapFile); if (!lapHistory) lapHistory = { final: false, //Whether the stopwatch has been reset. It is expected that the stopwatch app will create a final split when reset. If this is false, it is expected that this hasn't been done, and that the current time should be used as the "final split" splits: [] //List of times when the Lap button was pressed }; } else lapHistory = { final: false, //Whether the stopwatch has been reset. It is expected that the stopwatch app will create a final split when reset. If this is false, it is expected that this hasn't been done, and that the current time should be used as the "final split" splits: [] //List of times when the Lap button was pressed }; //Get the number of milliseconds that stopwatch has run for function getTime() { if (!state.wasRunning) { //If the timer never ran, zero ms have passed return 0; } else if (state.running) { //If the timer is running, the time left is current time - start time + preexisting time return (new Date()).getTime() - state.startTime + state.elapsedTime; } else { //If the timer is not running, the same as above but use when the timer was paused instead of now. return state.pausedTime - state.startTime + state.elapsedTime; } } let gestureMode = false; function drawButtons() { //Draw the backdrop const BAR_TOP = g.getHeight() - 48; const BUTTON_Y = BAR_TOP + 12; const BUTTON_LEFT = g.getWidth() / 4 - 12; //For the buttons, we have to subtract 12 because images do not obey alignment, but their size is known in advance const TEXT_LEFT = g.getWidth() / 4; //For text, we do not have to subtract 12 because they do obey alignment. const BUTTON_MID = g.getWidth() / 2 - 12; const TEXT_MID = g.getWidth() / 2; const BUTTON_RIGHT = g.getHeight() * 3 / 4 - 12; g.setColor(0, 0, 1).setFontAlign(0, -1) .clearRect(0, BAR_TOP, g.getWidth(), g.getHeight()) .fillRect(0, BAR_TOP, g.getWidth(), g.getHeight()) .setColor(1, 1, 1); if (gestureMode) g.setFont('Vector', 16) .drawString('Button: Lap/Reset\nSwipe: Start/stop\nTap: Light', TEXT_MID, BAR_TOP); else { g.setFont('Vector', 24); if (!state.wasRunning) { //If the timer was never running: if (storage.read('stlapview.app.js') !== undefined) //If stlapview is installed, there should be a button to open it and a button to start the timer g.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight()) .drawString("Laps", TEXT_LEFT, BUTTON_Y) .drawImage(BUTTON_ICONS.play, BUTTON_RIGHT, BUTTON_Y); else g.drawImage(BUTTON_ICONS.play, BUTTON_MID, BUTTON_Y); //Otherwise, only a button to start the timer } else { //If the timer was running: g.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight()); if (state.running) { //If it is running now, have a lap button and a pause button g.drawString("LAP", TEXT_LEFT, BUTTON_Y) .drawImage(BUTTON_ICONS.pause, BUTTON_RIGHT, BUTTON_Y); } else { //If it is not running now, have a reset button and a g.drawImage(BUTTON_ICONS.reset, BUTTON_LEFT, BUTTON_Y) .drawImage(BUTTON_ICONS.play, BUTTON_RIGHT, BUTTON_Y); } } } } function drawTime() { function pad(number) { return ('00' + parseInt(number)).slice(-2); } let time = getTime(); g.reset(0, 0, 0) .setFontAlign(0, 0) .setFont("Vector", 36) .clearRect(0, 24, g.getWidth(), g.getHeight() - 48) //Draw the time .drawString((() => { let hours = Math.floor(time / 3600000); let minutes = Math.floor((time % 3600000) / 60000); let seconds = Math.floor((time % 60000) / 1000); let hundredths = Math.floor((time % 1000) / 10); if (hours >= 1) return `${hours}:${pad(minutes)}:${pad(seconds)}`; else return `${minutes}:${pad(seconds)}:${pad(hundredths)}`; })(), g.getWidth() / 2, g.getHeight() / 2); //Draw the lap labels if necessary if (lapHistory.splits.length >= 1) { let lastLap = lapHistory.splits.length; let curLap = lastLap + 1; g.setFont("Vector", 12) .drawString((() => { let lapTime = time - lapHistory.splits[lastLap - 1]; let hours = Math.floor(lapTime / 3600000); let minutes = Math.floor((lapTime % 3600000) / 60000); let seconds = Math.floor((lapTime % 60000) / 1000); let hundredths = Math.floor((lapTime % 1000) / 10); if (hours == 0) return `Lap ${curLap}: ${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; else return `Lap ${curLap}: ${hours}:${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; })(), g.getWidth() / 2, g.getHeight() / 2 + 18) .drawString((() => { let lapTime; if (lastLap == 1) lapTime = lapHistory.splits[lastLap - 1]; else lapTime = lapHistory.splits[lastLap - 1] - lapHistory.splits[lastLap - 2]; let hours = Math.floor(lapTime / 3600000); let minutes = Math.floor((lapTime % 3600000) / 60000); let seconds = Math.floor((lapTime % 60000) / 1000); let hundredths = Math.floor((lapTime % 1000) / 10); if (hours == 0) return `Lap ${lastLap}: ${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; else return `Lap ${lastLap}: ${hours}:${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; })(), g.getWidth() / 2, g.getHeight() / 2 + 30); } } drawButtons(); function firstTimeStart(now, time) { state = { wasRunning: true, sessionStart: Math.floor(now), running: true, startTime: now, pausedTime: 0, elapsedTime: 0, }; lapFile = 'stlap-' + state.sessionStart + '.json'; setupTimerIntervalFast(); Bangle.buzz(200); drawButtons(); } function split(now, time) { lapHistory.splits.push(time); Bangle.buzz(); } function pause(now, time) { //Record the exact moment that we paused state.pausedTime = now; //Stop the timer state.running = false; stopTimerInterval(); Bangle.buzz(200); drawTime(); drawButtons(); } function reset(now, time) { //Record the time lapHistory.splits.push(time); lapHistory.final = true; storage.writeJSON(lapFile, lapHistory); //Reset the timer state = STATE_DEFAULT; lapHistory = { final: false, splits: [] }; Bangle.buzz(500); drawTime(); drawButtons(); } function start(now, time) { //Start the timer and record when we started state.elapsedTime += (state.pausedTime - state.startTime); state.startTime = now; state.running = true; setupTimerIntervalFast(); Bangle.buzz(200); drawTime(); drawButtons(); } Bangle.on("touch", (button, xy) => { setupTimerIntervalFast(); //In gesture mode, just turn on the light and then return if (gestureMode) { Bangle.setLCDPower(true); return; } //If we support full touch and we're not touching the keys, ignore. //If we don't support full touch, we can't tell so just assume we are. if (xy !== undefined && xy.y <= g.getHeight() - 48) return; let now = (new Date()).getTime(); let time = getTime(); if (!state.wasRunning) { if (storage.read('stlapview.app.js') !== undefined) { //If we were never running and stlapview is installed, there are two buttons: open stlapview and start the timer if (button == 1) load('stlapview.app.js'); else firstTimeStart(now, time); } //If stlapview there is only one button: the start button else firstTimeStart(now, time); } else if (state.running) { //If we are running, there are two buttons: lap and pause if (button == 1) split(now, time); else pause(now, time); } else { //If we are stopped, there are two buttons: reset and continue if (button == 1) reset(now, time); else start(now, time); } }); Bangle.on('swipe', direction => { setupTimerIntervalFast(); let now = (new Date()).getTime(); let time = getTime(); if (gestureMode) { Bangle.setLCDPower(true); if (!state.wasRunning) firstTimeStart(now, time); else if (state.running) pause(now, time); else start(now, time); } else { gestureMode = true; Bangle.setOptions({ lockTimeout: 0 }); drawTime(); drawButtons(); } }); setWatch(() => { let now = (new Date()).getTime(); let time = getTime(); if (gestureMode) { Bangle.setLCDPower(true); if (state.running) split(now, time); else reset(now, time); } }, BTN1, { repeat: true }); let timerInterval; let userWatching = false; function setupTimerIntervalFast() { userWatching = true; setupTimerInterval(); setTimeout(() => { userWatching = false; setupTimerInterval(); }, 5000); } function setupTimerInterval() { if (timerInterval !== undefined) { clearInterval(timerInterval); } timerInterval = setInterval(drawTime, userWatching ? 10 : 1000); } function stopTimerInterval() { if (timerInterval !== undefined) { clearInterval(timerInterval); timerInterval = undefined; } } drawTime(); if (state.running) { setupTimerIntervalFast(); } //Save our state when the app is closed E.on('kill', () => { storage.writeJSON(STATE_PATH, state); if (state.wasRunning) { storage.writeJSON(lapFile, lapHistory); } }); // change interval depending of whether the user's looking Bangle.on("twist", setupTimerIntervalFast); Bangle.loadWidgets(); Bangle.drawWidgets();