customclock - Initial work on easily customizable clock

pull/1916/head
Martin Boonk 2022-02-19 02:34:37 +01:00
parent ac5c80ff38
commit f9001e1969
5 changed files with 553 additions and 0 deletions

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))

361
apps/imageclock/app.js Normal file
View File

@ -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);

BIN
apps/imageclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

174
apps/imageclock/custom.html Normal file
View File

@ -0,0 +1,174 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../core/lib/heatshrink.js"></script>
<script src="../../core/lib/imageconverter.js"></script>
<script src="../../core/lib/customize.js"></script>
<p>Upload watchface</p>
<input type="file" id="fileLoader" name="files[]" multiple directory="" webkitdirectory="" moxdirectory="" /><br/>
<button id="btnUpload">Upload to watch</button></br>
<button id="btnSave">Download resources file</button></br>
<canvas id="canvas" style="display:none;"></canvas></br>
<script>
var result = "";
var resultJson = {};
function imageLoaded() {
var options = {};
options.diffusion = infoJson.diffusion ? infoJson.diffusion : "none";
options.compression = false;
options.alphaToColor = false;
options.transparent = this.path.match(/\.t[^.]*\./);
options.inverted = false;
options.autoCrop = false;
options.brightness = 0;
options.contrast = 0;
options.mode = infoJson.color ? infoJson.color : "1bpp";
options.output = "jsonobject";
var canvas = document.getElementById("canvas")
canvas.width = this.width*2;
canvas.height = this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(this,0,0);
var imgstr = "";
var imageData = ctx.getImageData(0, 0, this.width, this.height);
ctx.fillStyle = 'white';
ctx.fillRect(options.width, 0, this.width, this.height);
var rgba = imageData.data;
options.rgbaOut = rgba;
options.width = this.width;
options.height = this.height;
imgstr = imageconverter.RGBAtoString(rgba, options);
var outputImageData = new ImageData(options.rgbaOut, options.width, options.height);
ctx.putImageData(outputImageData,this.width,0);
// checkerboard for transparency on original image
var imageData = ctx.getImageData(0, 0, this.width, this.height);
imageconverter.RGBAtoCheckerboard(imageData.data, {width:this.width,height:this.height});
ctx.putImageData(imageData,0,0);
console.log("result is ", imgstr);
var faceJson;
var jsonPath = this.path.replace(/^[^\/\\]*[\/\\](.*)(\.t[^.]){0,1}\.[^.]+/, "$1").split("/");
var currentElement = resultJson;
for (var i = 0; i < jsonPath.length; i++){
if (i == jsonPath.length - 1){
currentElement[jsonPath[i]] = JSON.parse(imgstr);
} else {
if (!currentElement[jsonPath[i]]) currentElement[jsonPath[i]] = {};
currentElement = currentElement[jsonPath[i]];
}
}
result += '"' + jsonPath.join(".") + '":"' + imgstr + '"' + ",\n";
console.log("json is ", result);
}
function handleWatchFace(infoFile, faceFile, resourceFiles){
var reader = new FileReader();
reader.path = infoFile.webkitRelativePath;
reader.onload = function(event) {
infoJson = JSON.parse(reader.result);
handleFaceJson(faceFile, resourceFiles);
};
reader.readAsText(infoFile);
}
function handleFaceJson(faceFile, resourceFiles){
var reader = new FileReader();
reader.path = faceFile.webkitRelativePath;
reader.onload = function(event) {
faceJson = JSON.parse(reader.result);
handleResourceFiles(resourceFiles);
};
reader.readAsText(faceFile);
}
function handleResourceFiles(files){
for (var current of files){
console.log('Handle resource file ', current);
var reader = new FileReader();
reader.path = current.webkitRelativePath.replace("resources/","");
reader.onload = function(event) {
var img = new Image();
img.path = this.path;
img.onload = imageLoaded;
img.src = event.target.result;
};
reader.readAsDataURL(current);
}
}
function handleFileSelect(event) {
console.log("File select event", event);
if (event.target.files.length == 0) return;
result = "";
resultJson= {};
var resourceFiles = [];
var faceFile;
var infoFile;
for (var current of event.target.files){
console.log('Handle file ', current);
if (current.webkitRelativePath.split("/")[1].startsWith("resources")){
console.log('Found resource file', current.name);
resourceFiles.push(current);
} else if (current.name == "face.json"){
console.log('Found face file', current.name);
faceFile = current;
} else if (current.name == "info.json"){
console.log('Found info file', current.name);
infoFile = current;
} else {
console.log('Found unsupported file', current.name);
}
}
handleWatchFace(infoFile, faceFile, resourceFiles);
};
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("btnSave").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson));
h.target = '_blank';
h.download = "imageclock.resources.json";
h.click();
});
document.getElementById("btnUpload").addEventListener("click", function() {
var appDef = {
id : "imageclock",
storage:[
{name:"imageclock.app.js", url:"app.js"},
{name:"imageclock.face.json", content: JSON.stringify(faceJson)},
{name:"imageclock.resources.json", content: JSON.stringify(resultJson)},
{name:"imageclock.img", url:"app-icon.js", evaluate:true},
]
};
sendCustomizedApp(appDef);
});
</script>
</body>
</html>

View File

@ -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}
]
}