From f9001e1969d73749af0e4210dc46be4d0b175d76 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sat, 19 Feb 2022 02:34:37 +0100 Subject: [PATCH] customclock - Initial work on easily customizable clock --- apps/imageclock/app-icon.js | 1 + apps/imageclock/app.js | 361 ++++++++++++++++++++++++++++++++++ apps/imageclock/app.png | Bin 0 -> 929 bytes apps/imageclock/custom.html | 174 ++++++++++++++++ apps/imageclock/metadata.json | 17 ++ 5 files changed, 553 insertions(+) create mode 100644 apps/imageclock/app-icon.js create mode 100644 apps/imageclock/app.js create mode 100644 apps/imageclock/app.png create mode 100644 apps/imageclock/custom.html create mode 100644 apps/imageclock/metadata.json diff --git a/apps/imageclock/app-icon.js b/apps/imageclock/app-icon.js new file mode 100644 index 000000000..06f93e2ef --- /dev/null +++ b/apps/imageclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV")) diff --git a/apps/imageclock/app.js b/apps/imageclock/app.js new file mode 100644 index 000000000..3e77e8c63 --- /dev/null +++ b/apps/imageclock/app.js @@ -0,0 +1,361 @@ +var face = require("Storage").readJSON("imageclock.face.json"); +var resources = require("Storage").readJSON("imageclock.resources.json"); + +function getImg(resource){ + //print("getImg: ", resource); + var buffer; + if (resource.img){ + buffer = E.toArrayBuffer(atob(resource.img)); + //print("buffer from img"); + } else if (resource.file){ + buffer = E.toArrayBuffer(atob(require("Storage").read(resource.file))); + //print("buffer from file"); + } + + var result = { + width: resource.width, + height: resource.height, + bpp: resource.bpp, + buffer: buffer + }; + if (resource.transparent) result.transparent = resource.transparent; + + return result; +} + +function getByPath(object, path, lastElem){ + var current = object; + for (var c of path){ + if (!current[c]) return undefined; + current = current[c]; + } + if (lastElem!==undefined){ + if (!current["" + lastElem]) return undefined; + current = current["" + lastElem]; + } + return current; +} + +function splitNumberToDigits(num){ + return String(num).split('').map(item => Number(item)); +} + +function drawNumber(element, offset, number){ + //print("drawNumber: ", element, number); + var isNegative; + var digits; + if (number == undefined){ + isNegative = false; + digits = ["minus","minus","minus"]; + } else { + isNegative = number < 0; + if (isNegative) number *= -1; + digits = splitNumberToDigits(number); + } + + //print("digits: ", digits); + var numberOfDigits = element.Digits; + if (!numberOfDigits) numberOfDigits = digits.length; + var firstDigitX = element.TopLeftX; + var firstDigitY = element.TopLeftY; + var firstImage = getByPath(resources, element.ImagePath, 0); + + if (element.Alignment == "BottomRight"){ + var digitWidth = firstImage.width + element.Spacing; + var numberWidth = (numberOfDigits * digitWidth); + if (isNegative){ + numberWidth += firstImage.width + element.Spacing; + } + //print("Number width: ", numberWidth, firstImage.width, element.Spacing); + firstDigitX = element.BottomRightX - numberWidth + 1; + firstDigitY = element.BottomRightY - firstImage.height + 1; + //print("Calculated start " + firstDigitX + "," + firstDigitY + " From:" + element.BottomRightX + " " + firstImage.width + " " + element.Spacing); + } + var currentX = firstDigitX; + + if (isNegative){ + drawElement({X:currentX,Y:firstDigitY}, offset, element.ImagePath, "minus"); + currentX += firstImage.width + element.Spacing; + } + + for (var d = 0; d < numberOfDigits; d++){ + var currentDigit; + var difference = numberOfDigits - digits.length; + if (d >= difference){ + currentDigit = digits[d-difference]; + } else { + currentDigit = 0; + } + //print("Digit " + currentDigit + " " + currentX); + drawElement({X:currentX,Y:firstDigitY}, offset, element.ImagePath, currentDigit); + currentX += firstImage.width + element.Spacing; + } +} + +function setColors(properties){ + if (properties.fg) g.setColor(properties.fg); + if (properties.bg) g.setBgColor(properties.bg); +} + +function drawElement(pos, offset, path, lastElem){ + //print("drawElement ",pos, offset, path, lastElem); + var image = getByPath(resources, path, lastElem); + if (image){ + setColors(offset); + g.drawImage(getImg(image),offset.X + pos.X,offset.Y + pos.Y); + } else { + print("Could not create image from", path, lastElem); + } +} + +function drawScale(scale, offset, value){ + var segments = scale.Segments; + for (var i = 0; i < value * segments.length; i++){ + drawElement(segments[i], offset, scale.ImagePath, i); + } +} + +function drawDigit(element, offset, digit){ + drawElement(element, offset, element.ImagePath, digit); +} + +function drawMonthAndDay(element, offset){ + var date = new Date(); + + var dateOffset = updateOffset(element, offset); + + if (element.Separate){ + var separateOffset = updateOffset(element.Separate, dateOffset); + drawNumber(element.Separate.Month, separateOffset, date.getMonth() + 1); + drawNumber(element.Separate.Day, separateOffset, date.getDate()); + } +} + +function drawImage(image, offset, name){ + if (image.ImagePath) { + //print("drawImage", image, offset, name); + drawElement(image, offset, image.ImagePath, name ? "" + name: undefined); + } else if (image.ImageFile) { + var file = require("Storage").readJSON(image.ImageFile); + setColors(offset); + g.drawImage(getImg(file),image.X + offset.X, image.Y + offsetY); + } +} + +function drawWeather(element, offset){ + var jsonWeather = require("Storage").readJSON('weather.json'); + var weather = jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; + + var weatherOffset = updateOffset(element, offset); + + var iconOffset = updateOffset(element.Icon, weatherOffset); + if (weather && weather.code && element.Icon){ + var weathercode = weather.code; + //print(getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)); + if (!getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){ + weathercode = Math.floor(weathercode/10)*10; + //print("Weathercode ", weathercode); + } + if (!getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){ + weathercode = Math.floor(weathercode/100)*100; + //print("Weathercode ", weathercode); + } + if (getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){ + //print("Weathercode ", weathercode); + drawImage(element.Icon.CustomIcon, offset, weathercode); + } + } else if (getByPath(resources, element.Icon.CustomIcon.ImagePath, "000")) { + drawImage(element.Icon.CustomIcon, iconOffset, "000"); + } + + if (element.Temperature){ + var tempOffset = updateOffset(element.Temperature, weatherOffset); + if (weather && weather.temp && element.Temperature){ + drawNumber(element.Temperature.Current.Number, tempOffset, (weather.temp - 273.15).toFixed(0)); + } else { + drawNumber(element.Temperature.Current.Number, tempOffset); + } + drawImage(element.Temperature.Current, tempOffset, "centigrade"); + } + +} + +function updateOffset(element, offset){ + var newOffset = { X: offset.X, Y: offset.Y }; + if (element.X) newOffset.X += element.X; + if (element.Y) newOffset.Y += element.Y; + newOffset.fg = element.ForegroundColor ? element.ForegroundColor: offset.fg; + newOffset.bg = element.BackgroundColor ? element.BackgroundColor: offset.bg; + //print("Updated offset from ", offset, "to", newOffset); + return newOffset; +} + +function drawTime(element, offset){ + var date = new Date(); + var hours = date.getHours(); + var minutes = date.getMinutes(); + + var offsetTime = updateOffset(element, offset); + + var offsetHours = updateOffset(element.Hours, offsetTime); + if (element.Hours.Tens) { + drawDigit(element.Hours.Tens, offsetHours, Math.floor(hours/10)); + } + + if (element.Hours.Ones) { + drawDigit(element.Hours.Ones, offsetHours, hours % 10); + } + + var offsetMinutes = updateOffset(element.Minutes, offsetTime); + if (element.Minutes.Tens) { + drawDigit(element.Minutes.Tens, offsetMinutes, Math.floor(minutes/10)); + } + + if (element.Minutes.Ones) { + drawDigit(element.Minutes.Ones, offsetMinutes, minutes % 10); + } +} + +function drawSteps(element, offset){ + //print("drawSteps", element, offset); + if (Bangle.getHealthStatus) { + drawNumber(element.Number, offset, Bangle.getHealthStatus("day").steps); + } else { + drawNumber(element.Number, offset); + } +} + +function drawBattery(element, offset){ + if (element.Scale){ + drawScale(element.Scale, offset, E.getBattery()/100); + } +} + +function drawStatus(element, offset){ + var statusOffset = updateOffset(element, offset); + if (element.Lock) drawImage(element.Lock, statusOffset, Bangle.isLocked() ? "on" : "off"); + if (element.Charge) drawImage(element.Charge, statusOffset, Bangle.isCharging() ? "on" : "off"); + if (element.Bluetooth) drawImage(element.Bluetooth, statusOffset, NRF.getSecurityStatus().connected ? "on" : "off"); + if (element.Alarm) drawImage(element.Alarm, statusOffset, (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off"); + if (element.Notifications) drawImage(element.Notifications, statusOffset, ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "soundoff" : "vibrate"); +} + +function draw(element, offset){ + if (!element){ + element = face; + g.clear(); + } + g.setColor(g.theme.fg); + g.setBgColor(g.theme.bg); + + var elementOffset = updateOffset(element, offset); + setColors(elementOffset); + //print("Using offset", elementOffset); + + //print("Starting drawing loop", element); + for (var current in element){ + //print("Handling ", current, " with offset ", elementOffset); + var currentElement = element[current]; + switch(current){ + case "X": + case "Y": + case "Properties": + case "ForegroundColor": + case "BackgroundColor": + //Nothing to draw for these + break; + case "Background": + drawImage(currentElement, elementOffset); + break; + case "Time": + drawTime(currentElement, elementOffset); + break; + case "Battery": + drawBattery(currentElement, elementOffset); + break; + case "Steps": + drawSteps(currentElement, elementOffset); + break; + case "Pulse": + if (pulse) drawNumber(currentElement.Number, elementOffset, pulse); + break; + case "Pressure": + if (press) drawNumber(currentElement.Number, elementOffset, press.toFixed(0)); + break; + case "Altitude": + if (alt) drawNumber(currentElement.Number, elementOffset, alt.toFixed(0)); + break; + case "Temperature": + if (temp) drawNumber(currentElement.Number, elementOffset, temp.toFixed(0)); + break; + case "MonthAndDay": + drawMonthAndDay(currentElement, elementOffset); + break; + case "Weather": + drawWeather(currentElement, elementOffset); + break; + case "Status": + drawStatus(currentElement, elementOffset); + break; + default: + //print("Enter next level", currentElement, elementOffset); + draw(currentElement, elementOffset); + } + } + //print("Finished drawing loop"); +} + +var pulse,alt,temp,press; + + +var zeroOffset={X:0,Y:0}; + + +function handleHrm(e){ + if (e.confidence > 70){ + pulse = e.bpm; + } +} + +function handlePressure(e){ + alt = e.altitude; + temp = e.temperature; + press = e.pressure; +} + + +var unlockedDrawInterval; + +var lockedRedraw = getByPath(face, ["Properties","Redraw","Locked"]); +var unlockedRedraw = getByPath(face, ["Properties","Redraw","Unlocked"]); + +function handleLock(isLocked){ + if (!isLocked){ + Bangle.setHRMPower(1, "imageclock"); + Bangle.setBarometerPower(1, 'imageclock'); + unlockedDrawInterval = setInterval(()=>{ + draw(face, zeroOffset); + },unlockedRedraw?unlockedRedraw:1000); + draw(face, zeroOffset); + } else { + Bangle.setHRMPower(0, "imageclock"); + Bangle.setBarometerPower(0, 'imageclock'); + clearInterval(unlockedDrawInterval); + } + +} + +Bangle.setUI("clock"); + +Bangle.on('pressure', handlePressure); +Bangle.on('HRM', handleHrm); +Bangle.on('lock', handleLock); + + + +draw(face, zeroOffset); + + +setInterval(()=>{ + draw(face, zeroOffset); +}, lockedRedraw ? lockedRedraw : 6000); diff --git a/apps/imageclock/app.png b/apps/imageclock/app.png new file mode 100644 index 0000000000000000000000000000000000000000..cf057046be6eca51564a213bc3e7307271464009 GIT binary patch literal 929 zcmV;S177@zP)hL^4Q9~j~`7LX9JB(!Q` zf!g{&{RcFW&Q~mny+EQd5wRkG(GU|*v#i00#krWf=gz{s+-Te{nPTqF`ObG{=bo84 z7wV|v|Hht3JPkk}FaUG|9Y7m!Ao=|p@Bx?yW`SwoCGazne{UQEt^-TJmSbB2t^w^e zR}HiRcY*JgVs~m~;0|!8R^p4mY6RiBtpaENr0^Uv>(vCT0r!Cspcg0r`;t~jo{azx zfb|`CCKF##(F8oL;_xjnl#FrV8%ADg6Xz30z8#UnEWQMc5jI;nU_9Z{{Eq{QD8L7t!&STXpy9ZFm@kj^1Fem7{l-O!eBYr8>b)d^b3W}9W~#A6U*g95Oi>^=c4 z)~Zv`8K;EXvhfK3pp)!7am&FTkR7(HNX8sFd;lS+^%_N}!U1DH{Ly|K9>B#6_hueBM`PvzgXPMrIx>vGBiU)6Z4 zEpn3sb9fkCLB-z-YcKpmwJ#|r9RWl zC5~dFfRx^~zcZo4lLpcS%$dU%U1Vo|3%C)3JcraOHF*iC9^?bK^S~v{rNu|U-T>|j zG$FN0?PUAC7)9=|wrEE3yiS#A@zILA+sGlcN|VDaIFIb+1>`8+h@2|>kwB-& z?yx%JM4NdN29eBAO@MFvNQ4*G7A;7*Pj7ZUZ3CJ0gS8T$MjKKwTeK9b&o&dl(GUSK zKeKitb( + + + + + + + + + +

Upload watchface

+ + +
+
+
+
+ + + + + diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json new file mode 100644 index 000000000..9807b5271 --- /dev/null +++ b/apps/imageclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "imageclock", + "name": "imageclock", + "shortName": "imageclock", + "version": "0.01", + "type": "clock", + "description": "", + "icon": "app.png", + "tags": "clock", + "supports": ["BANGLEJS2"], + "custom": "custom.html", + "customConnect": false, + "storage": [ + {"name":"imageclock.app.js","url":"app.js"}, + {"name":"imageclock.img","url":"app-icon.js","evaluate":true} + ] +}