diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/edgeclk/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md new file mode 100644 index 000000000..535a5e9df --- /dev/null +++ b/apps/edgeclk/README.md @@ -0,0 +1,24 @@ +# Edge Clock + +![Screenshot](screenshot.png) +![Screenshot](screenshot2.png) +![Screenshot](screenshot3.png) + +Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability. +It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging. +There are three progress bars that indicate day of the week, time of the day, and daily step goal. +The watch face is monochrome and allows for applying your favorite color scheme. + +The appearance is highly configurable. In the settings menu you can: +- De-/activate a buzz when the charger is connected while the watch face is active. +- Decide if month or day should be displayed first. +- Switch between 24h and 12h clock. +- Hide or display seconds.* +- Show AM/PM in place of the seconds. +- Set the daily step goal. +- En- or disable the individual progress bars. +- Set if your week should start with Monday or Sunday (for week progress bar). + +*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well. + +The clock implements Fast Loading for faster switching to and fro. diff --git a/apps/edgeclk/app-icon.js b/apps/edgeclk/app-icon.js new file mode 100644 index 000000000..301abc93f --- /dev/null +++ b/apps/edgeclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIJGtgEDkEAn0b3P8uF8kUK1P+sFclQFCuFMkULCIMgvk+GRH/ABQRF5feGwMH7/KjcAmUGqfLzYLBg1fBYU+s1f/QRDofbDog7Xj8P/EH4f+Ao/5w4FBg8MgOB4eOAo8AgIFCDoIFJhk4Aok5DogFEGoIFBGoIdDAozgLOKNwAovv33wg0f/AFFsGGmEGwf4seOmCnBuALCg/YqFv30wgGf7AFEA")) \ No newline at end of file diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js new file mode 100644 index 000000000..dfc847b33 --- /dev/null +++ b/apps/edgeclk/app.js @@ -0,0 +1,306 @@ +{ + /* Configuration + ------------------------------------------------------------------------------*/ + + const settings = Object.assign({ + buzzOnCharge: true, + monthFirst: true, + twentyFourH: true, + showAmPm: false, + showSeconds: true, + stepGoal: 10000, + stepBar: true, + weekBar: true, + mondayFirst: true, + dayBar: true, + }, require('Storage').readJSON('edgeclk.settings.json', true) || {}); + + + /* Runtime Variables + ------------------------------------------------------------------------------*/ + + let startTimeout; + let drawInterval; + + let lcdPower = true; + let charging = Bangle.isCharging(); + + const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA'); + + const iconSize = [19, 26]; + const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA='); + const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA='); + const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA='); + + + /* Draw Functions + ------------------------------------------------------------------------------*/ + + const drawAll = function () { + const date = new Date(); + + drawDate(date); + if (settings.showSeconds) drawSecs(date); + drawTime(date); + drawLower(); + }; + + const drawLower = function (stepsOnlyCount) { + if (charging) { + drawCharge(); + } else { + drawSteps(stepsOnlyCount); + } + }; + + const drawDate = function (date) { + const top = 30; + g.reset(); + + // weekday + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, -1); // left top + g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true); + + // date + g.setFontAlign(1, -1); // right top + // Note: to save space first and last two lines of ASCII are left out. + // That is why '-' is assigned to '\' and ' ' (space) to '_'. + if (settings.monthFirst) { + g.drawString((date.getMonth()+1).toString().padStart(2, '_') + + '\\' + + date.getDate().toString().padStart(2, 0), + g.getWidth(), top + 12, true); + } else { + g.drawString('_' + + date.getDate().toString().padStart(2, 0) + + '\\' + + (date.getMonth()+1).toString(), + g.getWidth(), top + 12, true); + } + + // line/progress bar + if (settings.weekBar) { + let weekday = date.getDay(); + if (settings.mondayFirst) { + if (weekday === 0) { weekday = 7; } + } else { + weekday += 1; + } + drawBar(top, weekday/7); + } else { + drawLine(top); + } + }; + + const drawTime = function (date) { + const top = 72; + g.reset(); + + const h = date.getHours(); + g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9) + g.setFontAlign(-1, -1); // left top + g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0), + 0, top+12, true); + g.setFontAlign(0, -1); // center top + g.drawString(':', g.getWidth()/2, top+12, false); + const m = date.getMinutes(); + g.setFontAlign(1, -1); // right top + g.drawString(m.toString().padStart(2, 0), + g.getWidth(), top+12, true); + + if (settings.showAmPm) { + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true); + } + + if (settings.dayBar) { + drawBar(top, (h*60+m)/1440); + } else { + drawLine(top); + } + }; + + const drawSecs = function (date) { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true); + }; + + const drawSteps = function (onlyCount) { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, 1); // left bottom + + const steps = Bangle.getHealthStatus('day').steps; + g.drawString(steps.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); + + if (onlyCount === true) { + return; + } + + const progress = steps / settings.stepGoal; + if (settings.stepBar) { + drawBar(g.getHeight() - 38, progress); + } else { + drawLine(g.getHeight() - 38); + } + + // icon + if (progress < 1) { + g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]); + } else { + g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]); + } + }; + + const drawCharge = function () { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, 1); // left bottom + + const charge = E.getBattery(); + g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); + + drawBar(g.getHeight() - 38, charge / 100); + g.drawImage(plugIcon, 0, g.getHeight() - 26); + }; + + const drawBar = function (top, progress) { + g.drawRect(0, top, g.getWidth()-1, top + 5); + g.drawRect(1, top+1, g.getWidth()-2, top + 4); + const barLen = progress > 1 ? g.getWidth() : g.getWidth() * progress; + g.drawLine(2, top+2, barLen, top + 2); + g.drawLine(2, top+3, barLen, top + 3); + }; + + const drawLine = function (top) { + const width = g.getWidth(); + g.drawLine(0, top+2, width, top + 2); + g.drawLine(0, top+3, width, top + 3); + }; + + + /* Event Handlers + ------------------------------------------------------------------------------*/ + + const onSecondInterval = function () { + const date = new Date(); + drawSecs(date); + if (date.getSeconds() === 0) { + onMinuteInterval(); + } + }; + + const onMinuteInterval = function () { + const date = new Date(); + drawTime(date); + drawLower(true); + }; + + const onMinuteIntervalStarter = function () { + drawInterval = setInterval(onMinuteInterval, 60000); + startTimeout = null; + onMinuteInterval(); + }; + + const onLcdPower = function (on) { + lcdPower = on; + if (on) { + drawAll(); + startTimers(); + } else { + stopTimers(); + } + }; + + const onMidnight = function () { + if (!lcdPower) return; + drawDate(new Date()); + // Lower part (steps/charge) will be updated every minute. + // However, to save power while on battery only step count will get updated. + // This will update icon and progress bar as well: + if (!charging) drawSteps(); + }; + + const onHealth = function () { + if (!lcdPower || charging) return; + // This will update progress bar and icon: + drawSteps(); + }; + + const onLock = function (locked) { + if (locked) return; + drawLower(); + }; + + const onCharging = function (isCharging) { + charging = isCharging; + if (isCharging && settings.buzzOnCharge) Bangle.buzz(); + if (!lcdPower) return; + drawLower(); + }; + + + /* Lifecycle Functions + ------------------------------------------------------------------------------*/ + + const registerEvents = function () { + // This is for original Bangle.js; version two has always-on display: + Bangle.on('lcdPower', onLcdPower); + + // Midnight event is triggered qhen health data is reset and a new day begins: + Bangle.on('midnight', onMidnight); + + // Health data is published via 10 mins interval: + Bangle.on('health', onHealth); + + // Lock event signals screen (un)lock: + Bangle.on('lock', onLock); + + // Charging event signals when charging status changes: + Bangle.on('charging', onCharging); + }; + + const deregisterEvents = function () { + Bangle.removeListener('lcdPower', onLcdPower); + Bangle.removeListener('midnight', onMidnight); + Bangle.removeListener('health', onHealth); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('charging', onCharging); + }; + + const startTimers = function () { + if (drawInterval) return; + if (settings.showSeconds) { + drawInterval = setInterval( onSecondInterval, 1000); + } else { + startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000); + } + }; + + const stopTimers = function () { + if (startTimeout) clearTimeout(startTimeout); + if (!drawInterval) return; + clearInterval(drawInterval); + drawInterval = null; + }; + + + /* Startup Process + ------------------------------------------------------------------------------*/ + + g.clear(); + drawAll(); + startTimers(); + registerEvents(); + + Bangle.setUI({mode: 'clock', remove: function() { + stopTimers(); + deregisterEvents(); + }}); + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} diff --git a/apps/edgeclk/app.png b/apps/edgeclk/app.png new file mode 100644 index 000000000..90fbec3f3 Binary files /dev/null and b/apps/edgeclk/app.png differ diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json new file mode 100644 index 000000000..ac12a5f62 --- /dev/null +++ b/apps/edgeclk/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "edgeclk", + "name": "Edge Clock", + "shortName": "Edge Clock", + "version": "0.01", + "description": "Crisp clock with perfect readability.", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"edgeclk.app.js", "url": "app.js"}, + {"name":"edgeclk.settings.js", "url": "settings.js"}, + {"name":"edgeclk.img", "url": "app-icon.js", "evaluate": true} + ], + "data": [{"name":"edgeclk.settings.json"}] +} diff --git a/apps/edgeclk/screenshot.png b/apps/edgeclk/screenshot.png new file mode 100644 index 000000000..758dca96b Binary files /dev/null and b/apps/edgeclk/screenshot.png differ diff --git a/apps/edgeclk/screenshot2.png b/apps/edgeclk/screenshot2.png new file mode 100644 index 000000000..febac2d2c Binary files /dev/null and b/apps/edgeclk/screenshot2.png differ diff --git a/apps/edgeclk/screenshot3.png b/apps/edgeclk/screenshot3.png new file mode 100644 index 000000000..bdad9e1d5 Binary files /dev/null and b/apps/edgeclk/screenshot3.png differ diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js new file mode 100644 index 000000000..205dc5170 --- /dev/null +++ b/apps/edgeclk/settings.js @@ -0,0 +1,112 @@ +(function(back) { + const SETTINGS_FILE = 'edgeclk.settings.json'; + const storage = require('Storage'); + + const settings = { + buzzOnCharge: true, + monthFirst: true, + twentyFourH: true, + showAmPm: false, + showSeconds: true, + stepGoal: 10000, + stepBar: true, + weekBar: true, + mondayFirst: true, + dayBar: true, + }; + + const saved_settings = storage.readJSON(SETTINGS_FILE, true); + if (saved_settings) { + for (const key in saved_settings) { + if (!settings.hasOwnProperty(key)) continue; + settings[key] = saved_settings[key]; + } + } + + let save = function() { + storage.write(SETTINGS_FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Edge Clock' }, + '< Back': back, + 'Charge Buzz': { + value: settings.buzzOnCharge, + onchange: () => { + settings.buzzOnCharge = !settings.buzzOnCharge; + save(); + }, + }, + 'Month First': { + value: settings.monthFirst, + onchange: () => { + settings.monthFirst = !settings.monthFirst; + save(); + }, + }, + '24h Clock': { + value: settings.twentyFourH, + onchange: () => { + settings.twentyFourH = !settings.twentyFourH; + save(); + }, + }, + 'Show AM/PM': { + value: settings.showAmPm, + onchange: () => { + settings.showAmPm = !settings.showAmPm; + // TODO can this be visually changed? + if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false; + save(); + }, + }, + 'Show Seconds': { + value: settings.showSeconds, + onchange: () => { + settings.showSeconds = !settings.showSeconds; + // TODO can this be visually changed? + if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false; + save(); + }, + }, + 'Step Goal': { + value: settings.stepGoal, + min: 250, + max: 50000, + step: 250, + onchange: v => { + settings.stepGoal = v; + save(); + } + }, + 'Step Progress': { + value: settings.stepBar, + onchange: () => { + settings.stepBar = !settings.stepBar; + save(); + } + }, + 'Week Progress': { + value: settings.weekBar, + onchange: () => { + settings.weekBar = !settings.weekBar; + save(); + }, + }, + 'Week Start': { + value: settings.mondayFirst, + format: () => settings.mondayFirst ? 'Monday' : 'Sunday', + onchange: () => { + settings.mondayFirst = !settings.mondayFirst; + save(); + }, + }, + 'Day Progress': { + value: settings.dayBar, + onchange: () => { + settings.dayBar = !settings.dayBar; + save(); + }, + }, + }); +})