diff --git a/apps.json b/apps.json index 6f4562093..001c47b18 100644 --- a/apps.json +++ b/apps.json @@ -4982,7 +4982,7 @@ "description": "Displays the image in \"showimage.user.img\". The file has to be uploaded via the espruino IDE. Returns to watch face after 60s or button push. I use it to display my vaccination certificate.", "icon": "app.png", "tags": "tool", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "storage": [ {"name":"showimg.app.js","url":"app.js"}, {"name":"showimg.img","url":"app-icon.js","evaluate":true} @@ -5004,5 +5004,26 @@ {"name":"lapcounter.app.js","url":"app.js"}, {"name":"lapcounter.img","url":"app-icon.js","evaluate":true} ] - } + }, + { "id": "circlesclock", + "name": "Circles clock", + "shortName":"Circles clock", + "version":"0.01", + "description": "A clock with circles for different data at the bottom in a probably familiar style", + "icon": "app.png", + "dependencies": {"widpedom":"app"}, + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"circlesclock.app.js","url":"app.js"}, + {"name":"circlesclock.img","url":"app-icon.js","evaluate":true}, + {"name":"circlesclock.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"circlesclock.json"} + ] + } ] diff --git a/apps/circlesclock/Changelog b/apps/circlesclock/Changelog new file mode 100644 index 000000000..af119ab59 --- /dev/null +++ b/apps/circlesclock/Changelog @@ -0,0 +1 @@ +0.01: New clock diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md new file mode 100644 index 000000000..87edd5981 --- /dev/null +++ b/apps/circlesclock/README.md @@ -0,0 +1,19 @@ +# Circles clock + +A clock with circles for different data at the bottom in a probably familiar style + +It shows besides time, date and day of week the following information: + * Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer)) + * Heart rate (when screen is on and unlocked) + * Battery + +## Screenshot + +![Screenshot](screenshot.png) + +## TODO +* Show weather information + + +## Creator +Marco ([myxor](https://github.com/myxor)) diff --git a/apps/circlesclock/app-icon.js b/apps/circlesclock/app-icon.js new file mode 100644 index 000000000..ad727251a --- /dev/null +++ b/apps/circlesclock/app-icon.js @@ -0,0 +1 @@ + require("heatshrink").decompress(atob("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")) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js new file mode 100644 index 000000000..8474b7f4e --- /dev/null +++ b/apps/circlesclock/app.js @@ -0,0 +1,218 @@ +const locale = require("locale"); +const heatshrink = require("heatshrink"); + +var shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA=")); +var heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM")); +var powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI")); + +const SETTINGS_FILE = "circlesclock.json"; +let settings; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE, 1) || { + 'maxHR': 200, + 'stepGoal': 10000 + }; +} + +const colorFg = '#fff'; +const colorBg = '#000'; +const colorGrey = '#808080'; + +let hrtValue; + +const h = g.getHeight(); +const w = g.getWidth(); +const hOffset = 30; +const h1 = Math.round(1 * h / 5 - hOffset); +const h2 = Math.round(3 * h / 5 - hOffset); +const h3 = Math.round(8 * h / 8 - hOffset); +const w1 = Math.round(w / 6); +const w2 = Math.round(3 * w / 6); +const w3 = Math.round(5 * w / 6); +const radiusOuter = 22; +const radiusInner = 16; + +function draw() { + g.reset(); + g.setColor(colorBg); + g.fillRect(0, 0, w, h); + + // time + g.setFont("Vector:50"); + g.setFontAlign(-1, -1); + g.setColor(colorFg); + g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8); + + // date & dow + g.setFont("Vector:20"); + g.setFontAlign(-1, 0); + g.drawString(locale.date(new Date()), w / 10, h2); + g.drawString(locale.dow(new Date()), w / 10, h2 + 22); + + // Steps circle + drawSteps(); + + // Heart circle + drawHeartRate(); + + // Battery circle + drawBattery(); +} + + + +function drawSteps() { + const steps = getSteps(); + const blue = '#0000ff'; + g.setColor(colorGrey); + g.fillCircle(w1, h3, radiusOuter); + + const stepGoal = settings.stepGoal; + if (stepGoal > 0) { + let percent = steps / stepGoal; + if (stepGoal < steps) percent = 1; + drawGauge(w1, h3, percent, blue); + } + + g.setColor(colorBg); + g.fillCircle(w1, h3, radiusInner); + + g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]); + + g.setFont("Vector:12"); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(shortValue(steps), w1 + 2, h3); + + g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6); +} + +function drawHeartRate() { + const red = '#ff0000'; + g.setColor(colorGrey); + g.fillCircle(w2, h3, radiusOuter); + + if (hrtValue != undefined) { + const percent = hrtValue / settings.maxHR; + drawGauge(w2, h3, percent, red); + } + + g.setColor(colorBg); + g.fillCircle(w2, h3, radiusInner); + + g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]); + + g.setFont("Vector:12"); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(hrtValue != undefined ? hrtValue : 0, w2, h3); + + g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6); +} + +function drawBattery() { + const battery = E.getBattery(); + const yellow = '#ffff00'; + g.setColor(colorGrey); + g.fillCircle(w3, h3, radiusOuter); + + if (battery > 0) { + const percent = battery / 100; + drawGauge(w3, h3, percent, yellow); + } + + g.setColor(colorBg); + g.fillCircle(w3, h3, radiusInner); + + g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]); + + g.setFont("Vector:12"); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(battery + '%', w3, h3); + + g.drawImage(powerIcon, w3 - 6, h3 + radiusOuter - 6); +} + +function radians(a) { + return a * Math.PI / 180; +} + + +function drawGauge(cx, cy, percent, color) { + let offset = 30; + let end = 300; + var i = 0; + var r = radiusInner + 3; + + if (percent > 1) percent = 1; + + var startrot = -offset; + var endrot = startrot - ((end - offset) * percent); + + g.setColor(color); + + // draw gauge + for (i = startrot; i > endrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + g.fillCircle(x, y, 4); + } +} + +function shortValue(v) { + if (isNaN(v)) return '-'; + if (v <= 999) return v; + if (v >= 1000 && v < 10000) { + v = Math.floor(v / 100) * 100; + return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } + if (v >= 10000) { + v = Math.floor(v / 1000) * 1000; + return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } +} + +function getSteps() { + if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } + return 0; +} + +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + Bangle.setHRMPower(1, "watch"); + } else { + Bangle.setHRMPower(0, "watch"); + } + drawHeartRate(); + drawSteps(); +}); + +Bangle.on('HRM', function(hrm) { + //if(hrm.confidence > 90){ + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + //} else { + // hrtValue = undefined; + //} +}); + +g.clear(); +Bangle.loadWidgets(); +/* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * area to the top bar doesn't get cleared. + */ +for (let wd of WIDGETS) { + wd.draw = () => {}; + wd.area = ""; +} +loadSettings(); +setInterval(draw, 60000); +draw(); +Bangle.setUI("clock"); diff --git a/apps/circlesclock/app.png b/apps/circlesclock/app.png new file mode 100644 index 000000000..94ff885fa Binary files /dev/null and b/apps/circlesclock/app.png differ diff --git a/apps/circlesclock/screenshot.png b/apps/circlesclock/screenshot.png new file mode 100644 index 000000000..94ff885fa Binary files /dev/null and b/apps/circlesclock/screenshot.png differ diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js new file mode 100644 index 000000000..2de278b47 --- /dev/null +++ b/apps/circlesclock/settings.js @@ -0,0 +1,33 @@ +(function(back) { + const SETTINGS_FILE = "circlesclock.json"; + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + function save(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + E.showMenu({ + '': { 'title': 'circlesclock' }, + 'max heartrate': { + value: "maxHR" in settings ? settings.maxHR : 200, + min: 20, + max : 250, + step: 10, + format: x => { + return x; + }, + onchange: x => save('maxHR', x), + }, + 'step goal': { + value: "stepGoal" in settings ? settings.stepGoal : 10000, + min: 2000, + max : 50000, + step: 2000, + format: x => { + return x; + }, + onchange: x => save('stepGoal', x), + }, + '< Back': back, + }); +});