Compare commits
102 Commits
443921c30e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea5212778 | |||
| 5dd0272350 | |||
| 8de4d70c8b | |||
| 7a3e5cd868 | |||
| 8ed03143bd | |||
| 95cc91781d | |||
| 1a2fa25ea7 | |||
| df0a2c03fc | |||
| d2c4be15d0 | |||
| 3937109edb | |||
| 98fdddac5d | |||
| 73cd819e94 | |||
| abe931d61d | |||
| 0bf1220c62 | |||
| e32b274e5c | |||
| 2088081bb8 | |||
| 2754d4f52a | |||
| 04efe1b4d9 | |||
| 728687ab37 | |||
| 77288c92af | |||
| e8a361210d | |||
| 255984e52f | |||
| 20ce84ed89 | |||
| 31531ff118 | |||
| 37867a095b | |||
| 83a50fa06a | |||
| ab8f7e78b8 | |||
| 6fc30311f3 | |||
| a59b92572b | |||
| 2c91af6759 | |||
| 6ae1b173bd | |||
| f94d5c2105 | |||
| 913156ca3c | |||
| a5adeb379c | |||
| 281187fd91 | |||
| f5abf6eb48 | |||
| 810e43bcfd | |||
| 1c511d3441 | |||
| 3b66810ba1 | |||
| ebb1550fc7 | |||
| 6b7285a017 | |||
| 85185dbbaa | |||
| 5cb5ee604b | |||
| e57e09eb17 | |||
| f3fdf4e5ab | |||
| 2106a4298e | |||
| ca2265e8aa | |||
| b86f6720bc | |||
| 8c7c488967 | |||
| 3507d8938e | |||
| 9738c76140 | |||
| f19a77d06f | |||
| 739b337135 | |||
| 68aa187f71 | |||
| b369cb0c3b | |||
| 212cbdb9b7 | |||
| a714c57b2b | |||
| 6ac882699a | |||
| 9b48f7342a | |||
| 8b5ac36284 | |||
| 3c18e0326d | |||
| 2400ebd145 | |||
| 22e36c447d | |||
| 37de23ecfe | |||
| 2a9f8ce857 | |||
| a113e10ae5 | |||
| 197d393553 | |||
| 8fd4b6e45a | |||
| 36120b877f | |||
| 69330e03cb | |||
| 4c506c7913 | |||
| 8275abdb57 | |||
| 1c433fc56b | |||
| 14bda5c29f | |||
| ea58f2101f | |||
| 07bb582ddd | |||
| 6d965dd016 | |||
| 04511d029c | |||
| d2d89b361e | |||
| 0b4d8d84c4 | |||
| dc44ac9910 | |||
| f96bddf0ba | |||
| dda3130c56 | |||
| 9091d7a783 | |||
| 4a4be26977 | |||
| b4c6f1b0c8 | |||
| 0f343e753f | |||
| 6bd1f2f05b | |||
| fc8894015a | |||
| 0ba99a590a | |||
| 6167de7456 | |||
| eccca550ef | |||
| d5158eee54 | |||
| a8e2c2e990 | |||
| 87f93e10d1 | |||
| 9edab1f230 | |||
| f15fd9cf20 | |||
| c5b9343c9a | |||
| 98dd426e8a | |||
| 0073735647 | |||
| f839c18eee | |||
| 13d9596978 |
@@ -0,0 +1 @@
|
|||||||
|
.aider*
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
{ // must be inside our own scope here so that when we are unloaded everything disappears
|
||||||
|
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
|
||||||
|
const STATE_IDLE = 0;
|
||||||
|
const STATE_MENU_OPEN = 3;
|
||||||
|
|
||||||
|
let apiKey = require("Storage").readJSON("apikey.json", null);
|
||||||
|
if (!apiKey) {
|
||||||
|
require("Storage").writeJSON("apikey.json", "NEEDTOSET");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let watchState = STATE_IDLE;
|
||||||
|
|
||||||
|
let stopWatch = require("Storage").readJSON("mystopwatch.json", true) || {start1: null, elapsed1: null, buzz1: null, start2: null, elapsed2: null, buzzed2: null};
|
||||||
|
let saveStopWatch = function() {
|
||||||
|
require("Storage").writeJSON("mystopwatch.json", stopWatch);
|
||||||
|
}
|
||||||
|
let stopWatchTimer = null;
|
||||||
|
|
||||||
|
let myMessage = "";
|
||||||
|
let temperature = "?";
|
||||||
|
let temp_old = true;
|
||||||
|
let feels_like = "?";
|
||||||
|
let weather_old = true;
|
||||||
|
|
||||||
|
let drawTimer = null;
|
||||||
|
|
||||||
|
let menu = require("Storage").readJSON("menu.json", true);
|
||||||
|
let subMenu = null;
|
||||||
|
let menuCommand = "";
|
||||||
|
|
||||||
|
let drawStopWatches = function () {
|
||||||
|
if (watchState != STATE_IDLE) return;
|
||||||
|
|
||||||
|
var w = g.getWidth();
|
||||||
|
var x = w / 2;
|
||||||
|
var y = g.getHeight() / 2;
|
||||||
|
|
||||||
|
if (stopWatch.start1 || stopWatch.elapsed1) {
|
||||||
|
let Tt1 = (stopWatch.elapsed1 || 0);
|
||||||
|
if (stopWatch.start1) {
|
||||||
|
Tt1 += Date.now() - stopWatch.start1;
|
||||||
|
|
||||||
|
const fifteenMinutes = 15 * 60 * 1000;
|
||||||
|
let intervals = Math.floor(Tt1 / fifteenMinutes);
|
||||||
|
if (intervals > (stopWatch.buzz1 || 0)) {
|
||||||
|
stopWatch.buzz1 = intervals;
|
||||||
|
Bangle.buzz(500);
|
||||||
|
saveStopWatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Ttxt1 = timeToText(Tt1);
|
||||||
|
|
||||||
|
g.clearRect(0, y+61, w, y+88);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(Ttxt1, x, y+74);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopWatch.start2 || stopWatch.elapsed2) {
|
||||||
|
let Tt2 = (stopWatch.elapsed2 || 0);
|
||||||
|
if (stopWatch.start2) {
|
||||||
|
Tt2 += Date.now() - stopWatch.start2;
|
||||||
|
|
||||||
|
const oneMinute = 60 * 1000;
|
||||||
|
if (!stopWatch.buzzed2 && Tt2 >= oneMinute) {
|
||||||
|
stopWatch.buzzed2 = true;
|
||||||
|
Bangle.buzz(500);
|
||||||
|
saveStopWatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Ttxt2 = timeToText(Tt2);
|
||||||
|
|
||||||
|
g.clearRect(0, y-60, w, y-34);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(Ttxt2, x, y-45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let paintFace = function() {
|
||||||
|
if (watchState != STATE_IDLE) return;
|
||||||
|
|
||||||
|
var x = g.getWidth() / 2;
|
||||||
|
var y = g.getHeight() / 2;
|
||||||
|
|
||||||
|
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
|
||||||
|
|
||||||
|
var date = new Date();
|
||||||
|
var timeStr = require("locale").time(date, 1); // Hour and minute
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 60).drawString(timeStr, x, y); // Used to be Anton, +20
|
||||||
|
|
||||||
|
var utc_sec = getTime();
|
||||||
|
var utc = utc_sec % 86400;
|
||||||
|
var utcHour = Math.floor(utc / 3600);
|
||||||
|
var utcMinute = Math.floor((utc % 3600) / 60);
|
||||||
|
var utcStr = utcHour.toString().padStart(2, '0') + ":" + utcMinute.toString().padStart(2, '0');
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 36).drawString(utcStr, x-32, y+43);
|
||||||
|
|
||||||
|
//var tz_offset = date.toString().indexOf("GMT");
|
||||||
|
//var tz = date.toString().substring(tz_offset+3, tz_offset+6);
|
||||||
|
//g.setFontAlign(0, 0).setFont("Vector", 24).drawString(tz, x+60, y+43);
|
||||||
|
|
||||||
|
if (temp_old) g.setColor("#888");
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(temperature, x+53, y+43);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
|
||||||
|
// Show date and day of week
|
||||||
|
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
var dateStr = date.getDate() + " " + days[date.getDay()];
|
||||||
|
|
||||||
|
// don't draw date string if stopwatch 1 is running
|
||||||
|
if (!stopWatch.start1 && !stopWatch.elapsed1) {
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(dateStr, x-32, y+74);
|
||||||
|
if (weather_old) g.setColor("#888");
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(feels_like, x+53, y+74);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
//var wrapped = g.wrapString(myMessage, g.getWidth()-10).join("\n");
|
||||||
|
|
||||||
|
// don't draw message if stopwatch 2 is running
|
||||||
|
if (!stopWatch.start2 && !stopWatch.elapsed2) {
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(myMessage, x, y-45);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawStopWatches();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actually draw the watch face
|
||||||
|
let draw = function() {
|
||||||
|
if (Bangle.http){
|
||||||
|
Bangle.http("https://api.home.dns.t0.vc/bangle", {timeout:3000}).then(event => {
|
||||||
|
let result = JSON.parse(event.resp);
|
||||||
|
myMessage = result.context;
|
||||||
|
temperature = result.temperature;
|
||||||
|
temp_old = result.temp_old;
|
||||||
|
feels_like = result.feels_like;
|
||||||
|
weather_old = result.weather_old;
|
||||||
|
if (watchState == STATE_IDLE) {
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
}
|
||||||
|
}).catch((e)=>{
|
||||||
|
myMessage = "GET error";
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// queue next draw
|
||||||
|
if (drawTimer) clearTimeout(drawTimer);
|
||||||
|
drawTimer = setTimeout(function() {
|
||||||
|
drawTimer = undefined;
|
||||||
|
draw();
|
||||||
|
}, 60000 - (Date.now() % 60000));
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeToText = function(t) {
|
||||||
|
let hrs = Math.floor(t/3600000);
|
||||||
|
let mins = Math.floor(t/60000)%60;
|
||||||
|
let secs = Math.floor(t/1000)%60;
|
||||||
|
let tnth = Math.floor(t/100)%10;
|
||||||
|
let text;
|
||||||
|
|
||||||
|
if (hrs === 0) {
|
||||||
|
text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
|
||||||
|
} else {
|
||||||
|
text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let handleSwipe = function(dir1, dir2) {
|
||||||
|
var direction;
|
||||||
|
|
||||||
|
if (dir1 == 1) {
|
||||||
|
direction = "right";
|
||||||
|
} else if (dir1 == -1) {
|
||||||
|
direction = "left";
|
||||||
|
} else if (dir2 == 1) {
|
||||||
|
direction = "down";
|
||||||
|
} else if (dir2 == -1) {
|
||||||
|
direction = "up";
|
||||||
|
} else {
|
||||||
|
direction = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Bangle.http){
|
||||||
|
const headers = {"Authorization": "Bearer " + apiKey};
|
||||||
|
const options = {timeout:3000, method: "post", body: direction, headers: headers};
|
||||||
|
Bangle.http("https://api.home.dns.t0.vc/bangle", options).then(event => {
|
||||||
|
myMessage = "Sent " + direction;
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
}).catch((e)=>{
|
||||||
|
myMessage = "POST error";
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let drawMenu = function(menu) {
|
||||||
|
const keys = Object.keys(menu);
|
||||||
|
|
||||||
|
g.reset().clearRect(Bangle.appRect);
|
||||||
|
|
||||||
|
g.drawLine(88, 0, 88, 175); // |
|
||||||
|
g.drawLine(0, 88, 175, 88); // --
|
||||||
|
|
||||||
|
const wrap = function(text) {
|
||||||
|
return g.wrapString(text, 80).join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(wrap(keys[0]), 50, 50); // 0
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(wrap(keys[1]), 130, 50); // 1
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(wrap(keys[2]), 50, 130); // 2
|
||||||
|
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(wrap(keys[3]), 130, 130); // 3
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let startSW1 = function() {
|
||||||
|
console.log("Starting stopwatch 1...");
|
||||||
|
stopWatch.start1 = Date.now();
|
||||||
|
if (!stopWatch.elapsed1) {
|
||||||
|
stopWatch.elapsed1 = 0;
|
||||||
|
stopWatch.buzz1 = 0;
|
||||||
|
}
|
||||||
|
saveStopWatch();
|
||||||
|
|
||||||
|
if (!stopWatchTimer) {
|
||||||
|
drawStopWatches();
|
||||||
|
stopWatchTimer = setInterval(drawStopWatches, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let startSW2 = function() {
|
||||||
|
console.log("Starting stopwatch 2...");
|
||||||
|
stopWatch.start2 = Date.now();
|
||||||
|
if (!stopWatch.elapsed2) {
|
||||||
|
stopWatch.elapsed2 = 0;
|
||||||
|
stopWatch.buzzed2 = false;
|
||||||
|
}
|
||||||
|
saveStopWatch();
|
||||||
|
|
||||||
|
if (!stopWatchTimer) {
|
||||||
|
drawStopWatches();
|
||||||
|
stopWatchTimer = setInterval(drawStopWatches, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pauseSW1 = function() {
|
||||||
|
if (!stopWatch.start1) return;
|
||||||
|
stopWatch.elapsed1 += Date.now() - stopWatch.start1;
|
||||||
|
stopWatch.start1 = null;
|
||||||
|
saveStopWatch();
|
||||||
|
|
||||||
|
if (!stopWatch.start2) {
|
||||||
|
if (stopWatchTimer) {
|
||||||
|
clearInterval(stopWatchTimer);
|
||||||
|
stopWatchTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stopSW1 = function() {
|
||||||
|
stopWatch.start1 = null;
|
||||||
|
stopWatch.elapsed1 = null;
|
||||||
|
stopWatch.buzz1 = null;
|
||||||
|
saveStopWatch();
|
||||||
|
|
||||||
|
if (!stopWatch.start2) {
|
||||||
|
if (stopWatchTimer) {
|
||||||
|
clearInterval(stopWatchTimer);
|
||||||
|
stopWatchTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pauseSW2 = function() {
|
||||||
|
if (!stopWatch.start2) return;
|
||||||
|
stopWatch.elapsed2 += Date.now() - stopWatch.start2;
|
||||||
|
stopWatch.start2 = null;
|
||||||
|
saveStopWatch();
|
||||||
|
|
||||||
|
if (!stopWatch.start1) {
|
||||||
|
if (stopWatchTimer) {
|
||||||
|
clearInterval(stopWatchTimer);
|
||||||
|
stopWatchTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stopSW2 = function() {
|
||||||
|
stopWatch.start2 = null;
|
||||||
|
stopWatch.elapsed2 = null;
|
||||||
|
stopWatch.buzzed2 = null;
|
||||||
|
saveStopWatch();
|
||||||
|
|
||||||
|
if (!stopWatch.start1) {
|
||||||
|
if (stopWatchTimer) {
|
||||||
|
clearInterval(stopWatchTimer);
|
||||||
|
stopWatchTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let handleTouch = function(button, xy) {
|
||||||
|
//console.log(button, xy);
|
||||||
|
|
||||||
|
const touchX = xy.x;
|
||||||
|
const touchY = xy.y;
|
||||||
|
const pressed = xy.type == 0;
|
||||||
|
|
||||||
|
// 0 1
|
||||||
|
// 2 3
|
||||||
|
|
||||||
|
let quad = -1;
|
||||||
|
if (touchY < 110) {
|
||||||
|
if (touchX < 110) {
|
||||||
|
quad = 0;
|
||||||
|
} else {
|
||||||
|
quad = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (touchX < 110) {
|
||||||
|
quad = 2;
|
||||||
|
} else {
|
||||||
|
quad = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//console.log("quad:", quad);
|
||||||
|
|
||||||
|
if (watchState == STATE_IDLE && pressed) {
|
||||||
|
watchState = STATE_MENU_OPEN;
|
||||||
|
subMenu = menu;
|
||||||
|
menuCommand = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(subMenu);
|
||||||
|
const key = keys[quad];
|
||||||
|
|
||||||
|
if (key == "stop watch") {
|
||||||
|
subMenu = {};
|
||||||
|
const sw2key = stopWatch.start2 ? 'pause2' : 'start2';
|
||||||
|
subMenu[sw2key] = null;
|
||||||
|
subMenu["stop2"] = null;
|
||||||
|
const sw1key = stopWatch.start1 ? 'pause1' : 'start1';
|
||||||
|
subMenu[sw1key] = null;
|
||||||
|
subMenu["stop1"] = null;
|
||||||
|
} else {
|
||||||
|
subMenu = subMenu[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
menuCommand += key + ",";
|
||||||
|
|
||||||
|
//console.log("submenu:", subMenu);
|
||||||
|
|
||||||
|
if (subMenu) {
|
||||||
|
drawMenu(subMenu);
|
||||||
|
} else {
|
||||||
|
watchState = STATE_IDLE;
|
||||||
|
console.log("menuCommand:", menuCommand);
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
Bangle.buzz(100, 0.2);
|
||||||
|
|
||||||
|
if (menuCommand == "commands,util,pull menu,") {
|
||||||
|
console.log("Pulling new menu...");
|
||||||
|
menu = null;
|
||||||
|
require("Storage").writeJSON("menu.json", menu);
|
||||||
|
} else if (menuCommand == "commands,util,calc,") {
|
||||||
|
load("calculator.app.js");
|
||||||
|
} else if (menuCommand == "states,stop watch,start1,") {
|
||||||
|
startSW1();
|
||||||
|
} else if (menuCommand == "states,stop watch,start2,") {
|
||||||
|
startSW2();
|
||||||
|
} else if (menuCommand == "states,stop watch,pause1,") {
|
||||||
|
pauseSW1();
|
||||||
|
} else if (menuCommand == "states,stop watch,pause2,") {
|
||||||
|
pauseSW2();
|
||||||
|
} else if (menuCommand == "states,stop watch,stop1,") {
|
||||||
|
stopSW1();
|
||||||
|
} else if (menuCommand == "states,stop watch,stop2,") {
|
||||||
|
stopSW2();
|
||||||
|
} else {
|
||||||
|
if (Bangle.http){
|
||||||
|
const headers = {"Authorization": "Bearer " + apiKey};
|
||||||
|
const options = {timeout:3000, method: "post", body: menuCommand, headers: headers};
|
||||||
|
Bangle.http("https://api.home.dns.t0.vc/menu", options).then(event => {
|
||||||
|
myMessage = "ok";
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
}).catch((e)=>{
|
||||||
|
myMessage = "POST error";
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
myMessage = key;
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let handleTwist = function() {
|
||||||
|
console.log("twisted");
|
||||||
|
|
||||||
|
if (watchState == STATE_MENU_OPEN) {
|
||||||
|
watchState = STATE_IDLE;
|
||||||
|
|
||||||
|
myMessage = "cancelled";
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Bangle.http && !menu){
|
||||||
|
Bangle.http("https://api.home.dns.t0.vc/menu", {timeout:3000}).then(event => {
|
||||||
|
menu = JSON.parse(event.resp);
|
||||||
|
require("Storage").writeJSON("menu.json", menu);
|
||||||
|
}).catch((e)=>{
|
||||||
|
myMessage = "Menu error";
|
||||||
|
if (paintFace) paintFace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on('swipe', handleSwipe);
|
||||||
|
//Bangle.on('drag', handleDrag);
|
||||||
|
Bangle.on('touch', handleTouch);
|
||||||
|
Bangle.on('twist', handleTwist);
|
||||||
|
|
||||||
|
// Show launcher when middle button pressed
|
||||||
|
Bangle.setUI({
|
||||||
|
mode : "clock",
|
||||||
|
remove : function() {
|
||||||
|
// Called to unload all of the clock app
|
||||||
|
Bangle.removeListener('swipe', handleSwipe);
|
||||||
|
//Bangle.removeListener('drag', handleDrag);
|
||||||
|
Bangle.removeListener('touch', handleTouch);
|
||||||
|
Bangle.removeListener('twist', handleTwist);
|
||||||
|
if (drawTimer) clearTimeout(drawTimer);
|
||||||
|
drawTimer = undefined;
|
||||||
|
if (stopWatchTimer) clearInterval(stopWatchTimer);
|
||||||
|
stopWatchTimer = undefined;
|
||||||
|
|
||||||
|
paintFace = undefined; // http request may resolve after font's been unloaded, so unset
|
||||||
|
}});
|
||||||
|
// Load widgets
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
paintFace();
|
||||||
|
drawStopWatches();
|
||||||
|
if (stopWatch.start1 || stopWatch.start2) {
|
||||||
|
if (!stopWatchTimer) {
|
||||||
|
stopWatchTimer = setInterval(drawStopWatches, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
setTimeout(Bangle.drawWidgets,0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,736 @@
|
|||||||
|
/**
|
||||||
|
* BangleJS Calculator
|
||||||
|
*
|
||||||
|
* Original Author: Frederic Rousseau https://github.com/fredericrous
|
||||||
|
* Created: April 2020
|
||||||
|
*
|
||||||
|
* Contributors: thyttan https://github.com/thyttan
|
||||||
|
*/
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
require("FontDylex7x13").add(Graphics);
|
||||||
|
|
||||||
|
var DEFAULT_SELECTION_NUMBERS = '5';
|
||||||
|
var RESULT_HEIGHT = 40;
|
||||||
|
var COLORS = {
|
||||||
|
// [normal, selected]
|
||||||
|
DEFAULT: ['#7F8183', '#A6A6A7'],
|
||||||
|
OPERATOR: ['#F99D1C', '#CA7F2A'],
|
||||||
|
SPECIAL: ['#65686C', '#7F8183']
|
||||||
|
};
|
||||||
|
|
||||||
|
var KEY_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()];
|
||||||
|
|
||||||
|
var screen, screenColor;
|
||||||
|
var globalGrid = [4, 5];
|
||||||
|
var swipeEnabled;
|
||||||
|
|
||||||
|
var numbersGrid = [3, 4];
|
||||||
|
var numbers = {
|
||||||
|
'0': {grid: [1, 3], globalGrid: [1, 4], trbl: '2.0B'},
|
||||||
|
'.': {grid: [2, 3], globalGrid: [2, 4], trbl: '3=.0'},
|
||||||
|
'1': {grid: [0, 2], globalGrid: [0, 3], trbl: '42B1'},
|
||||||
|
'2': {grid: [1, 2], globalGrid: [1, 3], trbl: '5301'},
|
||||||
|
'3': {grid: [2, 2], globalGrid: [2, 3], trbl: '6+.2'},
|
||||||
|
'4': {grid: [0, 1], globalGrid: [0, 2], trbl: '7514'},
|
||||||
|
'5': {grid: [1, 1], globalGrid: [1, 2], trbl: '8624'},
|
||||||
|
'6': {grid: [2, 1], globalGrid: [2, 2], trbl: '9-35'},
|
||||||
|
'7': {grid: [0, 0], globalGrid: [0, 1], trbl: 'R847'},
|
||||||
|
'8': {grid: [1, 0], globalGrid: [1, 1], trbl: 'N957'},
|
||||||
|
'9': {grid: [2, 0], globalGrid: [2, 1], trbl: 'p*68'},
|
||||||
|
'B': {grid: [0, 3], globalGrid: [0, 4], trbl: '10BB', val: '<-', color: COLORS.SPECIAL},
|
||||||
|
};
|
||||||
|
|
||||||
|
var operatorsGrid = [2, 3];
|
||||||
|
var operators = {
|
||||||
|
'+': {grid: [0, 0], globalGrid: [3, 3], trbl: '-+=3'},
|
||||||
|
'-': {grid: [1, 0], globalGrid: [3, 2], trbl: '*-+6'},
|
||||||
|
'*': {grid: [0, 1], globalGrid: [3, 1], trbl: '/*-9'},
|
||||||
|
'/': {grid: [1, 1], globalGrid: [3, 0], trbl: '//*p'},
|
||||||
|
'=': {grid: [1, 2], globalGrid: [3, 4], trbl: '+==.'},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.HWVERSION!=1) {
|
||||||
|
operatorsGrid = [2, 4];
|
||||||
|
operators['='].grid = [1, 3];
|
||||||
|
operators.r = {grid: [0, 2], val: 'sqrt'};
|
||||||
|
operators.s = {grid: [1, 2], val: 'x^2'};
|
||||||
|
operators.i = {grid: [0, 3], val: '1/x'};
|
||||||
|
}
|
||||||
|
|
||||||
|
var scientificOperatorsGrid = [2, 4];
|
||||||
|
var scientificOperators = {
|
||||||
|
'sin': {grid: [0, 0], val: 'sin'},
|
||||||
|
'cos': {grid: [1, 0], val: 'cos'},
|
||||||
|
'tan': {grid: [0, 1], val: 'tan'},
|
||||||
|
'angleMode': {grid: [1, 1], val: 'deg'},
|
||||||
|
'log': {grid: [0, 2], val: 'log'},
|
||||||
|
'tenpow': {grid: [1, 2], val: '10^x'},
|
||||||
|
'ln': {grid: [0, 3], val: 'ln'},
|
||||||
|
'epow': {grid: [1, 3], val: 'e^x'},
|
||||||
|
};
|
||||||
|
|
||||||
|
var specialsGrid = [2, 2];
|
||||||
|
var specials = {
|
||||||
|
'R': {grid: [0, 0], globalGrid: [0, 0], trbl: 'RN7R', val: 'AC'},
|
||||||
|
'N': {grid: [1, 0], globalGrid: [1, 0], trbl: 'Np8R', val: '+/-'},
|
||||||
|
'p': {grid: [0, 1], globalGrid: [2, 0], trbl: 'p/9N', val: 'Pi'},
|
||||||
|
'send': {grid: [1, 1], val: 'send'},
|
||||||
|
};
|
||||||
|
|
||||||
|
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||||
|
var prevNumber = null;
|
||||||
|
var currNumber = null;
|
||||||
|
var operator = null;
|
||||||
|
var results = null;
|
||||||
|
var isDecimal = false;
|
||||||
|
var hasPressedEquals = false;
|
||||||
|
var angleMode = 'deg';
|
||||||
|
var prevExpression = null;
|
||||||
|
var currExpression = "";
|
||||||
|
|
||||||
|
function prepareScreen(screen, grid, defaultColor) {
|
||||||
|
for (var k in screen) {
|
||||||
|
if (screen.hasOwnProperty(k)) {
|
||||||
|
screen[k].color = screen[k].color || defaultColor;
|
||||||
|
var position = [];
|
||||||
|
var xGrid = (KEY_AREA[2]-KEY_AREA[0])/grid[0];
|
||||||
|
var yGrid = (KEY_AREA[3]-KEY_AREA[1])/grid[1];
|
||||||
|
if (swipeEnabled) {
|
||||||
|
position[0] = KEY_AREA[0]+xGrid*screen[k].grid[0];
|
||||||
|
position[1] = KEY_AREA[1]+yGrid*screen[k].grid[1];
|
||||||
|
} else {
|
||||||
|
position[0] = KEY_AREA[0]+xGrid*screen[k].globalGrid[0];
|
||||||
|
position[1] = KEY_AREA[1]+yGrid*screen[k].globalGrid[1];
|
||||||
|
}
|
||||||
|
position[2] = position[0]+xGrid-1;
|
||||||
|
position[3] = position[1]+yGrid-1;
|
||||||
|
screen[k].xy = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawKey(name, k, selected) {
|
||||||
|
var color = k.color || COLORS.DEFAULT;
|
||||||
|
g.setColor(color[selected ? 1 : 0]);
|
||||||
|
g.setFont('Dylex7x13', 2).setFontAlign(0,0);
|
||||||
|
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
|
||||||
|
g.setColor(0);
|
||||||
|
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawKeys() {
|
||||||
|
g.setColor(screenColor[0]);
|
||||||
|
g.fillRect(KEY_AREA[0], KEY_AREA[1], KEY_AREA[2], KEY_AREA[3]);
|
||||||
|
for (var k in screen) {
|
||||||
|
if (screen.hasOwnProperty(k)) {
|
||||||
|
drawKey(k, screen[k], k == selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function drawGlobal() {
|
||||||
|
screen = {};
|
||||||
|
screenColor = COLORS.DEFAULT;
|
||||||
|
prepareScreen(numbers, globalGrid, COLORS.DEFAULT);
|
||||||
|
for (var k in numbers) {
|
||||||
|
screen[k] = numbers[k];
|
||||||
|
}
|
||||||
|
prepareScreen(operators, globalGrid, COLORS.OPERATOR);
|
||||||
|
for (var k in operators) {
|
||||||
|
screen[k] = operators[k];
|
||||||
|
}
|
||||||
|
prepareScreen(specials, globalGrid, COLORS.SPECIAL);
|
||||||
|
for (var k in specials) {
|
||||||
|
screen[k] = specials[k];
|
||||||
|
}
|
||||||
|
drawKeys();
|
||||||
|
}
|
||||||
|
function drawNumbers() {
|
||||||
|
screen = numbers;
|
||||||
|
screenColor = COLORS.DEFAULT;
|
||||||
|
drawKeys();
|
||||||
|
}
|
||||||
|
function drawOperators() {
|
||||||
|
screen = operators;
|
||||||
|
screenColor =COLORS.OPERATOR;
|
||||||
|
drawKeys();
|
||||||
|
}
|
||||||
|
function drawScientificOperators() {
|
||||||
|
screen = scientificOperators;
|
||||||
|
screenColor =COLORS.OPERATOR;
|
||||||
|
drawKeys();
|
||||||
|
}
|
||||||
|
function drawSpecials() {
|
||||||
|
screen = specials;
|
||||||
|
screenColor = COLORS.SPECIAL;
|
||||||
|
drawKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function toExponential(num, precision) {
|
||||||
|
if (num === 0) {
|
||||||
|
let z = "0";
|
||||||
|
if (precision > 0) z += "." + "0".repeat(precision);
|
||||||
|
return z + "e+0";
|
||||||
|
}
|
||||||
|
var sign = "";
|
||||||
|
if (num < 0) {
|
||||||
|
sign = "-";
|
||||||
|
num = -num;
|
||||||
|
}
|
||||||
|
var exp = Math.floor(Math.log(num) / Math.LN10);
|
||||||
|
var mantissa = num / Math.pow(10, exp);
|
||||||
|
|
||||||
|
var mantissaFixed = mantissa.toFixed(precision);
|
||||||
|
if (parseFloat(mantissaFixed) >= 10) {
|
||||||
|
mantissaFixed = (mantissa/10).toFixed(precision);
|
||||||
|
exp++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign + mantissaFixed + "e" + (exp >= 0 ? "+" : "") + exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixFloat(n) {
|
||||||
|
if (Math.abs(n) < 1e-10) return 0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSeparators(s) {
|
||||||
|
var parts = s.split(".");
|
||||||
|
var intPart = parts[0];
|
||||||
|
var sign = "";
|
||||||
|
if (intPart[0] === "-") {
|
||||||
|
sign = "-";
|
||||||
|
intPart = intPart.slice(1);
|
||||||
|
}
|
||||||
|
var result = "";
|
||||||
|
while (intPart.length > 3) {
|
||||||
|
result = "," + intPart.slice(-3) + result;
|
||||||
|
intPart = intPart.slice(0, -3);
|
||||||
|
}
|
||||||
|
result = intPart + result;
|
||||||
|
parts[0] = sign + result;
|
||||||
|
return parts.join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function doMath(x, y, operator) {
|
||||||
|
x = parseFloat(x);
|
||||||
|
y = parseFloat(y);
|
||||||
|
switch (operator) {
|
||||||
|
case '/':
|
||||||
|
return x / y;
|
||||||
|
case '*':
|
||||||
|
return x * y;
|
||||||
|
case '+':
|
||||||
|
return x + y;
|
||||||
|
case '-':
|
||||||
|
return x - y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayOutput(num) {
|
||||||
|
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
|
||||||
|
g.setColor(-1);
|
||||||
|
g.setFont('Dylex7x13', 2);
|
||||||
|
if (num === Infinity || num === -Infinity || isNaN(num)) {
|
||||||
|
// handle division by 0
|
||||||
|
if (num === Infinity) {
|
||||||
|
num = 'INF';
|
||||||
|
} else if (num === -Infinity) {
|
||||||
|
num = '-INF';
|
||||||
|
} else {
|
||||||
|
num = 'NaN';
|
||||||
|
}
|
||||||
|
specials.R.val = 'AC';
|
||||||
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
|
} else {
|
||||||
|
// might not be a number due to display of dot "."
|
||||||
|
var numNumeric = Number(num);
|
||||||
|
|
||||||
|
if (typeof num === 'string') {
|
||||||
|
if (num.indexOf('.') !== -1) {
|
||||||
|
// display a 0 before a lonely dot
|
||||||
|
if (numNumeric == 0) {
|
||||||
|
num = '0.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// remove preceding 0
|
||||||
|
while (num.length > 1 && num[0] === '0')
|
||||||
|
num = num.substr(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var numStr = num.toString();
|
||||||
|
var displayStr;
|
||||||
|
|
||||||
|
if (typeof num === 'number' && (numStr.indexOf('e') > -1 || (num !== 0 && Math.abs(num) < 1e-6))) {
|
||||||
|
// try to format as scientific notation
|
||||||
|
let precision = 10; // start with high precision
|
||||||
|
while (precision >= 0) {
|
||||||
|
let scientificStr = toExponential(num, precision).replace("e", "E");
|
||||||
|
if (g.stringWidth(scientificStr) <= g.getWidth() - 20) {
|
||||||
|
displayStr = scientificStr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
precision--;
|
||||||
|
}
|
||||||
|
if (precision < 0) { // if it still doesn't fit
|
||||||
|
displayStr = toExponential(num, 0).replace("e", "E");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayStr = addSeparators(numStr);
|
||||||
|
}
|
||||||
|
num = displayStr;
|
||||||
|
|
||||||
|
// final check for truncation
|
||||||
|
if (g.stringWidth(num) > g.getWidth() - 20) {
|
||||||
|
while(g.stringWidth(num+'...') > g.getWidth() - 20 && num.length > 1) {
|
||||||
|
num = num.slice(0,-1);
|
||||||
|
}
|
||||||
|
num += '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.setFontAlign(1,0);
|
||||||
|
g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2);
|
||||||
|
if (operator) {
|
||||||
|
g.setFont('Dylex7x13', 2).setFontAlign(1,0);
|
||||||
|
g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var wasPressedEquals = false;
|
||||||
|
var hasPressedNumber = false;
|
||||||
|
function calculatorLogic(x) {
|
||||||
|
if (wasPressedEquals && hasPressedNumber !== false) {
|
||||||
|
prevNumber = null;
|
||||||
|
currNumber = hasPressedNumber;
|
||||||
|
wasPressedEquals = false;
|
||||||
|
hasPressedNumber = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasPressedEquals) {
|
||||||
|
if (hasPressedNumber) {
|
||||||
|
prevNumber = null;
|
||||||
|
hasPressedNumber = false;
|
||||||
|
operator = null;
|
||||||
|
} else {
|
||||||
|
currNumber = null;
|
||||||
|
prevNumber = results;
|
||||||
|
}
|
||||||
|
hasPressedEquals = false;
|
||||||
|
wasPressedEquals = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) {
|
||||||
|
operator = x;
|
||||||
|
displayOutput(prevNumber);
|
||||||
|
} else if (prevNumber != null && currNumber != null && operator != null) {
|
||||||
|
// we execute the calculus only when there was a previous number entered before and an operator
|
||||||
|
let oldOperator = operator;
|
||||||
|
results = doMath(prevNumber, currNumber, operator);
|
||||||
|
operator = x;
|
||||||
|
prevNumber = results;
|
||||||
|
prevExpression = "(" + prevExpression + " " + oldOperator + " " + currExpression + ")";
|
||||||
|
currNumber = null;
|
||||||
|
currExpression = "";
|
||||||
|
displayOutput(results);
|
||||||
|
} else if (prevNumber == null && currNumber != null && operator == null) {
|
||||||
|
// no operator yet, save the current number for later use when an operator is pressed
|
||||||
|
operator = x;
|
||||||
|
prevNumber = currNumber;
|
||||||
|
prevExpression = currExpression;
|
||||||
|
currNumber = null;
|
||||||
|
currExpression = "";
|
||||||
|
displayOutput(prevNumber);
|
||||||
|
} else if (prevNumber == null && currNumber == null && operator == null) {
|
||||||
|
displayOutput(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buttonPress(val) {
|
||||||
|
switch (val) {
|
||||||
|
case 'R':
|
||||||
|
currNumber = null;
|
||||||
|
results = null;
|
||||||
|
isDecimal = false;
|
||||||
|
hasPressedEquals = false;
|
||||||
|
if (specials.R.val == 'AC') {
|
||||||
|
prevNumber = null;
|
||||||
|
operator = null;
|
||||||
|
prevExpression = null;
|
||||||
|
currExpression = "";
|
||||||
|
} else {
|
||||||
|
specials.R.val = 'AC';
|
||||||
|
drawKey('R', specials.R, true);
|
||||||
|
}
|
||||||
|
wasPressedEquals = false;
|
||||||
|
hasPressedNumber = false;
|
||||||
|
displayOutput(0);
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
specials.R.val = 'C';
|
||||||
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
|
currNumber = Math.PI;
|
||||||
|
if (hasPressedEquals === 1) {
|
||||||
|
hasPressedEquals = 2;
|
||||||
|
}
|
||||||
|
hasPressedNumber = currNumber;
|
||||||
|
currExpression = currNumber.toString();
|
||||||
|
displayOutput(currNumber);
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
if (results != null) {
|
||||||
|
results = Math.sqrt(results);
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "sqrt(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
currNumber = Math.sqrt(currNumber);
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "sqrt(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
if (results != null) {
|
||||||
|
results = results * results;
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "(" + prevExpression + ")^2";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
var num = parseFloat(currNumber);
|
||||||
|
currNumber = num * num;
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "(" + currExpression + ")^2";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'sin':
|
||||||
|
if (results != null) {
|
||||||
|
let angle = results;
|
||||||
|
if (angleMode === 'deg') {
|
||||||
|
angle = angle * Math.PI / 180;
|
||||||
|
}
|
||||||
|
results = fixFloat(Math.sin(angle));
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "sin(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
let angle = currNumber;
|
||||||
|
if (angleMode === 'deg') {
|
||||||
|
angle = angle * Math.PI / 180;
|
||||||
|
}
|
||||||
|
currNumber = fixFloat(Math.sin(angle));
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "sin(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'cos':
|
||||||
|
if (results != null) {
|
||||||
|
let angle = results;
|
||||||
|
if (angleMode === 'deg') {
|
||||||
|
angle = angle * Math.PI / 180;
|
||||||
|
}
|
||||||
|
results = fixFloat(Math.cos(angle));
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "cos(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
let angle = currNumber;
|
||||||
|
if (angleMode === 'deg') {
|
||||||
|
angle = angle * Math.PI / 180;
|
||||||
|
}
|
||||||
|
currNumber = fixFloat(Math.cos(angle));
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "cos(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'tan':
|
||||||
|
if (results != null) {
|
||||||
|
let angle = results;
|
||||||
|
if (angleMode === 'deg') {
|
||||||
|
angle = angle * Math.PI / 180;
|
||||||
|
}
|
||||||
|
results = fixFloat(Math.tan(angle));
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "tan(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
let angle = currNumber;
|
||||||
|
if (angleMode === 'deg') {
|
||||||
|
angle = angle * Math.PI / 180;
|
||||||
|
}
|
||||||
|
currNumber = fixFloat(Math.tan(angle));
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "tan(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'log':
|
||||||
|
if (results != null) {
|
||||||
|
results = Math.log(results) / Math.LN10;
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "log(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
currNumber = Math.log(currNumber) / Math.LN10;
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "log(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'tenpow':
|
||||||
|
if (results != null) {
|
||||||
|
results = Math.pow(10, results);
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "10^(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
currNumber = Math.pow(10, currNumber);
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "10^(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'ln':
|
||||||
|
if (results != null) {
|
||||||
|
results = Math.log(results);
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "ln(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
currNumber = Math.log(currNumber);
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "ln(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'epow':
|
||||||
|
if (results != null) {
|
||||||
|
results = Math.exp(results);
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "e^(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
currNumber = Math.exp(currNumber);
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "e^(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'angleMode':
|
||||||
|
if (angleMode === 'rad') {
|
||||||
|
angleMode = 'deg';
|
||||||
|
} else {
|
||||||
|
angleMode = 'rad';
|
||||||
|
}
|
||||||
|
scientificOperators.angleMode.val = angleMode;
|
||||||
|
drawKey('angleMode', scientificOperators.angleMode);
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
if (results != null) {
|
||||||
|
results = 1 / results;
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "1/(" + prevExpression + ")";
|
||||||
|
} else if (currNumber != null) {
|
||||||
|
currNumber = 1 / parseFloat(currNumber);
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "1/(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'N':
|
||||||
|
if (results != null && !hasPressedNumber) {
|
||||||
|
if (results === 0) {
|
||||||
|
results = (1/results === -Infinity || Object.is(results, -0)) ? 0 : -0;
|
||||||
|
} else {
|
||||||
|
results *= -1;
|
||||||
|
}
|
||||||
|
displayOutput(results);
|
||||||
|
prevExpression = "-(" + prevExpression + ")";
|
||||||
|
} else {
|
||||||
|
if (currNumber === null) currNumber = '0';
|
||||||
|
var num = parseFloat(currNumber);
|
||||||
|
if (num === 0) {
|
||||||
|
// Toggle between 0 and -0
|
||||||
|
currNumber = (1/currNumber === -Infinity || Object.is(currNumber, -0)) ? '0' : -0;
|
||||||
|
} else {
|
||||||
|
currNumber = num * -1;
|
||||||
|
}
|
||||||
|
displayOutput(currNumber);
|
||||||
|
currExpression = "-(" + currExpression + ")";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'B':
|
||||||
|
Bangle.buzz(30);
|
||||||
|
if (isDecimal) {
|
||||||
|
isDecimal = false;
|
||||||
|
displayOutput(currNumber);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (currNumber != null) {
|
||||||
|
currNumber = currNumber.toString();
|
||||||
|
if (currNumber.length > 1) {
|
||||||
|
currNumber = currNumber.slice(0, -1);
|
||||||
|
} else {
|
||||||
|
currNumber = '0';
|
||||||
|
}
|
||||||
|
// if we removed a decimal point
|
||||||
|
if (currNumber.indexOf('.') === -1) {
|
||||||
|
isDecimal = false;
|
||||||
|
}
|
||||||
|
hasPressedNumber = currNumber;
|
||||||
|
currExpression = currNumber;
|
||||||
|
displayOutput(currNumber);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
case '*':
|
||||||
|
case '-':
|
||||||
|
case '+':
|
||||||
|
calculatorLogic(val);
|
||||||
|
hasPressedNumber = false;
|
||||||
|
if (swipeEnabled) drawNumbers();
|
||||||
|
break;
|
||||||
|
case '.':
|
||||||
|
Bangle.buzz(30);
|
||||||
|
if (currNumber != null && currNumber.toString().indexOf('.') !== -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
specials.R.val = 'C';
|
||||||
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
|
isDecimal = true;
|
||||||
|
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
if (prevNumber != null && currNumber != null && operator != null) {
|
||||||
|
let cExpr = currExpression;
|
||||||
|
if (hasPressedEquals && cExpr === "") {
|
||||||
|
cExpr = currNumber.toString();
|
||||||
|
}
|
||||||
|
prevExpression = prevExpression + " " + operator + " " + cExpr;
|
||||||
|
results = doMath(prevNumber, currNumber, operator);
|
||||||
|
prevNumber = results;
|
||||||
|
currExpression = "";
|
||||||
|
displayOutput(results);
|
||||||
|
hasPressedEquals = 1;
|
||||||
|
}
|
||||||
|
hasPressedNumber = false;
|
||||||
|
break;
|
||||||
|
case 'send':
|
||||||
|
let logStr;
|
||||||
|
if (hasPressedEquals) {
|
||||||
|
logStr = prevExpression + " = " + results;
|
||||||
|
} else if (operator && prevExpression) { // Incomplete binary operation, e.g. "5 +" or "5 + 3"
|
||||||
|
logStr = prevExpression + " " + operator;
|
||||||
|
if (currExpression) {
|
||||||
|
logStr += " " + currExpression;
|
||||||
|
}
|
||||||
|
} else if (results) { // Result of a unary op on a previous result
|
||||||
|
logStr = prevExpression + " = " + results;
|
||||||
|
} else if (currNumber) { // A number has been entered, or a unary op on it
|
||||||
|
if (currExpression && currExpression !== currNumber.toString()) {
|
||||||
|
logStr = currExpression + " = " + currNumber;
|
||||||
|
} else {
|
||||||
|
logStr = currNumber.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logStr = results ? results.toString() : "0";
|
||||||
|
}
|
||||||
|
console.log(logStr);
|
||||||
|
if (Bangle.http){
|
||||||
|
const options = {timeout:3000, method: "post", body: logStr};
|
||||||
|
Bangle.http("https://tbot.tannercollin.com/banglecalc", options).then(event => {
|
||||||
|
Bangle.buzz();
|
||||||
|
console.log("Successfully sent");
|
||||||
|
}).catch((e)=>{
|
||||||
|
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
|
||||||
|
g.setColor(-1);
|
||||||
|
g.setFont('Dylex7x13', 2);
|
||||||
|
g.setFontAlign(1,0);
|
||||||
|
g.drawString('ERROR', g.getWidth()-20, RESULT_HEIGHT/2);
|
||||||
|
if (operator) {
|
||||||
|
g.setFont('Dylex7x13', 2).setFontAlign(1,0);
|
||||||
|
g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
Bangle.buzz(30);
|
||||||
|
specials.R.val = 'C';
|
||||||
|
if (!swipeEnabled) drawKey('R', specials.R);
|
||||||
|
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
|
||||||
|
if (isDecimal) {
|
||||||
|
currNumber = currNumber == null || hasPressedEquals === 1 ? '0.' + val : parseInt(currNumber, 10) + '.' + val;
|
||||||
|
isDecimal = false;
|
||||||
|
} else {
|
||||||
|
currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val);
|
||||||
|
// remove preceding 0 on integers
|
||||||
|
if (typeof currNumber === 'string' && currNumber.length > 1 && currNumber[0] === '0' && currNumber.indexOf('.') === -1) {
|
||||||
|
currNumber = currNumber.substr(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasPressedEquals === 1) {
|
||||||
|
hasPressedEquals = 2;
|
||||||
|
}
|
||||||
|
hasPressedNumber = currNumber;
|
||||||
|
currExpression = currNumber;
|
||||||
|
displayOutput(currNumber);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDirection(d) {
|
||||||
|
drawKey(selected, screen[selected]);
|
||||||
|
prevSelected = selected;
|
||||||
|
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : screen[selected].trbl[d];
|
||||||
|
drawKey(selected, screen[selected], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.HWVERSION==1) {
|
||||||
|
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
|
||||||
|
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
|
||||||
|
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
|
||||||
|
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
|
||||||
|
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
|
||||||
|
swipeEnabled = false;
|
||||||
|
drawGlobal();
|
||||||
|
} else { // touchscreen?
|
||||||
|
selected = "NONE";
|
||||||
|
swipeEnabled = true;
|
||||||
|
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
|
||||||
|
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
|
||||||
|
prepareScreen(scientificOperators, scientificOperatorsGrid, COLORS.OPERATOR);
|
||||||
|
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
|
||||||
|
drawNumbers();
|
||||||
|
|
||||||
|
Bangle.setUI({
|
||||||
|
mode : 'custom',
|
||||||
|
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
|
||||||
|
touch : (n,e)=>{
|
||||||
|
for (var key in screen) {
|
||||||
|
if (typeof screen[key] == "undefined") break;
|
||||||
|
var r = screen[key].xy;
|
||||||
|
if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
|
||||||
|
//print("Press "+key);
|
||||||
|
buttonPress(""+key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
swipe : (LR, UD) => {
|
||||||
|
if (UD !== 0) {
|
||||||
|
drawNumbers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (LR === 1) { // right
|
||||||
|
if (screen === scientificOperators) drawOperators();
|
||||||
|
else if (screen === operators) drawNumbers();
|
||||||
|
else if (screen === numbers) drawSpecials();
|
||||||
|
else if (screen === specials) drawNumbers();
|
||||||
|
}
|
||||||
|
if (LR === -1) { // left
|
||||||
|
if (screen === numbers) drawOperators();
|
||||||
|
else if (screen === operators) drawScientificOperators();
|
||||||
|
else if (screen === specials) drawNumbers();
|
||||||
|
else if (screen === scientificOperators) drawNumbers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
displayOutput(0);
|
||||||
@@ -0,0 +1,669 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
// --- Mock Bangle.js environment ---
|
||||||
|
let mock_display_str = "";
|
||||||
|
let mock_ui_callbacks = {};
|
||||||
|
let drawn_since_clear = false;
|
||||||
|
global.last_display_num = null;
|
||||||
|
|
||||||
|
// Mock 'g' (graphics)
|
||||||
|
global.g = {
|
||||||
|
_col: 0,
|
||||||
|
_bg: 0,
|
||||||
|
_font: "6x8",
|
||||||
|
_font_size: 1,
|
||||||
|
clear: () => { drawn_since_clear = false; },
|
||||||
|
clearRect: () => { drawn_since_clear = false; },
|
||||||
|
setColor: (c) => { global.g._col = c; return global.g; },
|
||||||
|
setBgColor: (c) => { global.g._bg = c; return global.g; },
|
||||||
|
fillRect: () => {},
|
||||||
|
setFont: function(font, size) {
|
||||||
|
if (font && font.includes(":")) {
|
||||||
|
const parts = font.split(":");
|
||||||
|
this._font = parts[0];
|
||||||
|
this._font_size = parseInt(parts[1], 10);
|
||||||
|
} else {
|
||||||
|
this._font = font;
|
||||||
|
if (size) this._font_size = size;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
setFontAlign: () => {},
|
||||||
|
stringWidth: (s) => s.length * 4.7 * (global.g._font_size || 1), // approximation to match watch
|
||||||
|
getWidth: () => 176,
|
||||||
|
getHeight: () => 176,
|
||||||
|
drawString: (s) => {
|
||||||
|
if (!drawn_since_clear) {
|
||||||
|
mock_display_str = String(s);
|
||||||
|
drawn_since_clear = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFontHeight: () => 8 * (global.g._font_size || 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
global.Graphics = {};
|
||||||
|
|
||||||
|
// Mock Bangle.js-specific process.env properties
|
||||||
|
process.env.HWVERSION = 2; // Simulate Bangle.js 2
|
||||||
|
|
||||||
|
// Mock 'Bangle'
|
||||||
|
global.Bangle = {
|
||||||
|
setUI: (callbacks) => { mock_ui_callbacks = callbacks; },
|
||||||
|
http: () => new Promise(resolve => resolve({resp: "{}"})),
|
||||||
|
buzz: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock 'load' (to exit app)
|
||||||
|
global.load = () => {};
|
||||||
|
|
||||||
|
// --- End Mock ---
|
||||||
|
|
||||||
|
|
||||||
|
// --- Load calculator app ---
|
||||||
|
// We inject a "spy" to capture the raw value passed to displayOutput,
|
||||||
|
// making our tests independent of UI formatting.
|
||||||
|
let calculatorCode = fs.readFileSync(path.join(__dirname, 'calculator.app.js'), 'utf8');
|
||||||
|
// 1. Rename the original function so we can call it.
|
||||||
|
calculatorCode = calculatorCode.replace(
|
||||||
|
'function displayOutput(num)',
|
||||||
|
'function _original_displayOutput(num)'
|
||||||
|
);
|
||||||
|
// 2. Prepend our spy that captures the value and then calls the original.
|
||||||
|
const spyCode = `
|
||||||
|
var displayOutput = function(num) {
|
||||||
|
global.last_display_num = num;
|
||||||
|
_original_displayOutput(num);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
const processedCode = spyCode + calculatorCode;
|
||||||
|
|
||||||
|
const wrappedCode = `(function(require) { ${processedCode}; return { buttonPress, scientificOperators }; })`;
|
||||||
|
const getAppFns = eval(wrappedCode);
|
||||||
|
const { buttonPress, scientificOperators } = getAppFns((name) => {
|
||||||
|
if (name === "FontDylex7x13") {
|
||||||
|
return { add: () => {} };
|
||||||
|
}
|
||||||
|
return require(name);
|
||||||
|
});
|
||||||
|
// --- End Load ---
|
||||||
|
|
||||||
|
|
||||||
|
// --- Test Framework ---
|
||||||
|
const test_suite = [];
|
||||||
|
let tests_passed = 0;
|
||||||
|
let tests_failed = 0;
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
test_suite.push({ name, fn });
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
test_suite.forEach(t => {
|
||||||
|
// Reset state before each test
|
||||||
|
buttonPress('R'); // C
|
||||||
|
buttonPress('R'); // AC
|
||||||
|
if (global.last_display_num !== 0) {
|
||||||
|
console.error(`[FAIL] ${t.name} - Failed to reset state. Last number is: "${global.last_display_num}"`);
|
||||||
|
tests_failed++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
t.fn();
|
||||||
|
console.log(`[PASS] ${t.name}`);
|
||||||
|
tests_passed++;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[FAIL] ${t.name}`);
|
||||||
|
console.error(e);
|
||||||
|
tests_failed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\nTests finished. Passed: ${tests_passed}, Failed: ${tests_failed}.`);
|
||||||
|
if (tests_failed > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function press(buttons) {
|
||||||
|
for (const button of buttons) {
|
||||||
|
buttonPress(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDisplay(expected, message) {
|
||||||
|
const actual = global.last_display_num;
|
||||||
|
|
||||||
|
if (typeof expected === 'number' && isNaN(expected)) {
|
||||||
|
assert.ok(typeof actual === 'number' && isNaN(actual), message || `Expected NaN, got ${actual}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coerce to numbers for comparison to handle string vs number differences ('5' vs 5)
|
||||||
|
// and use a tolerance for floating point values.
|
||||||
|
const actualNum = parseFloat(actual);
|
||||||
|
const expectedNum = parseFloat(expected);
|
||||||
|
|
||||||
|
if (!isNaN(actualNum) && !isNaN(expectedNum)) {
|
||||||
|
const tolerance = 1e-9;
|
||||||
|
if (Math.abs(expectedNum - actualNum) < tolerance || actualNum === expectedNum) { // second part handles Infinity
|
||||||
|
return; // The numbers are close enough.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to strict equality for non-numeric strings or when numeric check fails
|
||||||
|
assert.strictEqual(actual, expected, message);
|
||||||
|
}
|
||||||
|
// --- End Test Framework ---
|
||||||
|
|
||||||
|
|
||||||
|
// --- Test Cases ---
|
||||||
|
|
||||||
|
const basicNumbers = [
|
||||||
|
0, 1, -1, 2, -2, 5, -5, 10, -10, 0.1, -0.1, 0.2, -0.2, 0.5, -0.5,
|
||||||
|
123, -123, 1.23, -1.23,
|
||||||
|
1234567, -1234567, 99999999, -99999999,
|
||||||
|
];
|
||||||
|
|
||||||
|
const edgeCaseNumbers = [
|
||||||
|
-0,
|
||||||
|
Number.MAX_SAFE_INTEGER,
|
||||||
|
Number.MIN_SAFE_INTEGER,
|
||||||
|
Math.PI, Math.E,
|
||||||
|
0.000001, -0.000001, 0.00000001, -0.00000001,
|
||||||
|
1000000, -1000000, 100000000, -100000000,
|
||||||
|
0.1+0.2, // floating point fun
|
||||||
|
1/3, 2/3, 0.9999999, 1.0000001
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate some more numbers to reach the target count
|
||||||
|
const generatedNumbers = [];
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
// integers
|
||||||
|
generatedNumbers.push(i * 123);
|
||||||
|
generatedNumbers.push(i * -123);
|
||||||
|
// floats
|
||||||
|
generatedNumbers.push(i / 10);
|
||||||
|
generatedNumbers.push(i / -10);
|
||||||
|
// small floats
|
||||||
|
generatedNumbers.push(i / 1000);
|
||||||
|
generatedNumbers.push(i / -1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testNumbers = [...new Set([...basicNumbers, ...edgeCaseNumbers, ...generatedNumbers])];
|
||||||
|
// We need to filter out Infinity/-Infinity for pressNumber helper as they can't be typed.
|
||||||
|
// They can only appear as results, which we test for separately.
|
||||||
|
const finiteTestNumbers = testNumbers.filter(n => isFinite(n));
|
||||||
|
|
||||||
|
// Helper to press a number, handling negatives
|
||||||
|
function pressNumber(n) {
|
||||||
|
let s = String(n);
|
||||||
|
// If string representation is in scientific notation, convert to decimal string
|
||||||
|
if (s.includes('e')) {
|
||||||
|
s = n.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
|
||||||
|
}
|
||||||
|
if (s.startsWith('-')) {
|
||||||
|
press(s.substring(1));
|
||||||
|
buttonPress('N');
|
||||||
|
} else {
|
||||||
|
press(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Generated Tests: Binary Operations ---
|
||||||
|
const binaryOps = {
|
||||||
|
'+': (a, b) => a + b,
|
||||||
|
'-': (a, b) => a - b,
|
||||||
|
'*': (a, b) => a * b,
|
||||||
|
'/': (a, b) => a / b,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const op in binaryOps) {
|
||||||
|
for (const a of finiteTestNumbers) {
|
||||||
|
for (const b of finiteTestNumbers) {
|
||||||
|
if (op === '/' && b === 0) {
|
||||||
|
test(`Binary Edge Case: ${a} / 0`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
buttonPress('/');
|
||||||
|
pressNumber(0);
|
||||||
|
buttonPress('=');
|
||||||
|
checkDisplay(a === 0 ? NaN : (a > 0 ? Infinity : -Infinity));
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
test(`Binary: ${a} ${op} ${b}`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
buttonPress(op);
|
||||||
|
pressNumber(b);
|
||||||
|
buttonPress('=');
|
||||||
|
const expected = binaryOps[op](a, b);
|
||||||
|
checkDisplay(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Generated Tests: Operations with non-finite results ---
|
||||||
|
const smallFiniteTestNumbers = [1, -1, 5, -5, 0, 100, -100]; // for speed
|
||||||
|
|
||||||
|
// Test operations that result in Infinity, -Infinity, or NaN, and then continue with another operation
|
||||||
|
for (const a of smallFiniteTestNumbers) {
|
||||||
|
// a / 0 -> Infinity/-Infinity/NaN
|
||||||
|
test(`Non-finite chaining: (${a} / 0) + 5`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
press('/0=');
|
||||||
|
const intermediate = a/0;
|
||||||
|
checkDisplay(intermediate);
|
||||||
|
press('+5=');
|
||||||
|
checkDisplay(intermediate + 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Non-finite chaining: (${a} / 0) * 5`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
press('/0=');
|
||||||
|
const intermediate = a/0;
|
||||||
|
checkDisplay(intermediate);
|
||||||
|
press('*5=');
|
||||||
|
checkDisplay(intermediate * 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Non-finite chaining: (${a} / 0) * 0`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
press('/0=');
|
||||||
|
const intermediate = a/0;
|
||||||
|
checkDisplay(intermediate);
|
||||||
|
press('*0=');
|
||||||
|
checkDisplay(intermediate * 0); // Should be NaN
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Non-finite chaining: (${a} / 0) / 5`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
press('/0=');
|
||||||
|
const intermediate = a/0;
|
||||||
|
checkDisplay(intermediate);
|
||||||
|
press('/5=');
|
||||||
|
checkDisplay(intermediate / 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Non-finite chaining: (${a} / 0) / 0`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
press('/0=');
|
||||||
|
const intermediate = a/0;
|
||||||
|
checkDisplay(intermediate);
|
||||||
|
press('/0=');
|
||||||
|
checkDisplay(intermediate / 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 0/0 = NaN. Test operations on NaN
|
||||||
|
test(`Non-finite chaining: (0 / 0) + 5`, () => {
|
||||||
|
press('0/0=');
|
||||||
|
checkDisplay(NaN);
|
||||||
|
press('+5=');
|
||||||
|
checkDisplay(NaN + 5); // anything + NaN is NaN
|
||||||
|
});
|
||||||
|
test(`Non-finite chaining: (0 / 0) * 5`, () => {
|
||||||
|
press('0/0=');
|
||||||
|
checkDisplay(NaN);
|
||||||
|
press('*5=');
|
||||||
|
checkDisplay(NaN * 5);
|
||||||
|
});
|
||||||
|
test(`Non-finite chaining: (0 / 0) / 5`, () => {
|
||||||
|
press('0/0=');
|
||||||
|
checkDisplay(NaN);
|
||||||
|
press('/5=');
|
||||||
|
checkDisplay(NaN / 5);
|
||||||
|
});
|
||||||
|
test(`Non-finite chaining: sqrt(negative) then op`, () => {
|
||||||
|
press('9N'); // -9
|
||||||
|
press('r'); // sqrt
|
||||||
|
checkDisplay(NaN);
|
||||||
|
press('+1=');
|
||||||
|
checkDisplay(NaN);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Generated Tests: Unary Operations ---
|
||||||
|
const unaryOps = {
|
||||||
|
'r': { name: 'sqrt', fn: Math.sqrt },
|
||||||
|
's': { name: 'x^2', fn: (a) => a * a },
|
||||||
|
'i': { name: '1/x', fn: (a) => 1 / a },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const op in unaryOps) {
|
||||||
|
for (const a of finiteTestNumbers) {
|
||||||
|
if (op === 'r' && a < 0) {
|
||||||
|
test(`Unary Edge Case: sqrt(${a})`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
buttonPress('r');
|
||||||
|
checkDisplay(NaN);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
test(`Unary: ${unaryOps[op].name}(${a})`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
buttonPress(op);
|
||||||
|
const expected = unaryOps[op].fn(a);
|
||||||
|
checkDisplay(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Generated Tests: Chaining Operations ---
|
||||||
|
// To keep runtime sane, we'll pick a smaller subset for chaining
|
||||||
|
const chainNumbers = [0, 1, -1, 5, -5, 0.5, -0.5, 10, -10, 123, -123, 1.23];
|
||||||
|
const chainOps = ['+', '-', '*', '/'];
|
||||||
|
|
||||||
|
for (const a of chainNumbers) {
|
||||||
|
for (const b of chainNumbers) {
|
||||||
|
for (const c of chainNumbers) {
|
||||||
|
for (const op1 of chainOps) {
|
||||||
|
for (const op2 of chainOps) {
|
||||||
|
test(`Chaining: ${a} ${op1} ${b} ${op2} ${c}`, () => {
|
||||||
|
pressNumber(a);
|
||||||
|
buttonPress(op1);
|
||||||
|
pressNumber(b);
|
||||||
|
buttonPress(op2); // This will execute the first operation
|
||||||
|
pressNumber(c);
|
||||||
|
buttonPress('='); // This will execute the second operation
|
||||||
|
// Calculator does sequential evaluation: (a op1 b) op2 c
|
||||||
|
const expected = binaryOps[op2](binaryOps[op1](a, b), c);
|
||||||
|
checkDisplay(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Generated Tests: Trigonometry ---
|
||||||
|
const trigOps = {
|
||||||
|
'sin': { fn: Math.sin, name: 'sin' },
|
||||||
|
'cos': { fn: Math.cos, name: 'cos' },
|
||||||
|
'tan': { fn: Math.tan, name: 'tan' },
|
||||||
|
};
|
||||||
|
const trigAngles = [0, 15, 30, 45, 60, 75, 90, 180, 270, 360, -30, -45, -90, -180, 450, 720, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, Math.PI, 3*Math.PI/2, 2*Math.PI, -Math.PI/2, -Math.PI];
|
||||||
|
|
||||||
|
// Test in DEG mode (default)
|
||||||
|
for (const op in trigOps) {
|
||||||
|
for (const angle of trigAngles) {
|
||||||
|
test(`Trig (deg): ${trigOps[op].name}(${angle})`, () => {
|
||||||
|
pressNumber(angle);
|
||||||
|
buttonPress(op);
|
||||||
|
const expected = trigOps[op].fn(angle * Math.PI / 180);
|
||||||
|
checkDisplay(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test in RAD mode
|
||||||
|
test('Switch to RAD mode', () => {
|
||||||
|
buttonPress('angleMode');
|
||||||
|
assert.strictEqual(scientificOperators.angleMode.val, 'rad');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const op in trigOps) {
|
||||||
|
for (const angle of trigAngles) {
|
||||||
|
test(`Trig (rad): ${trigOps[op].name}(${angle})`, () => {
|
||||||
|
pressNumber(angle);
|
||||||
|
buttonPress(op);
|
||||||
|
const expected = trigOps[op].fn(angle);
|
||||||
|
checkDisplay(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Switch back to DEG mode for subsequent tests', () => {
|
||||||
|
buttonPress('angleMode');
|
||||||
|
assert.strictEqual(scientificOperators.angleMode.val, 'deg');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Specific Functionality Tests ---
|
||||||
|
|
||||||
|
test('Clear and All Clear', () => {
|
||||||
|
press('123+');
|
||||||
|
checkDisplay('123');
|
||||||
|
press('R'); // Clear
|
||||||
|
checkDisplay('0');
|
||||||
|
press('456');
|
||||||
|
checkDisplay('456');
|
||||||
|
press('=');
|
||||||
|
checkDisplay('579'); // 123 + 456
|
||||||
|
press('R'); // Clear
|
||||||
|
checkDisplay('0');
|
||||||
|
buttonPress('R'); // All Clear
|
||||||
|
press('+1=');
|
||||||
|
checkDisplay('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Backspace', () => {
|
||||||
|
press('123.45B');
|
||||||
|
checkDisplay('123.4');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('123.');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('123');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('12');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('1');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('0');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Number input chaos: decimals and backspace', () => {
|
||||||
|
// Multiple decimals
|
||||||
|
press('1.2.3');
|
||||||
|
checkDisplay('1.23', 'Second decimal point should be ignored');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
// Multiple leading decimals
|
||||||
|
press('..123');
|
||||||
|
checkDisplay('0.123', 'Multiple leading decimal points should result in 0.123');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
// Backspace over decimal and re-add
|
||||||
|
press('1.23B');
|
||||||
|
checkDisplay('1.2', 'Backspace once');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('1.', 'Backspace twice');
|
||||||
|
press('.');
|
||||||
|
checkDisplay('1.', 'Adding decimal again should have no effect');
|
||||||
|
press('B');
|
||||||
|
checkDisplay('1', 'Backspace a third time');
|
||||||
|
press('.');
|
||||||
|
checkDisplay('1.', 'Should be able to add decimal back');
|
||||||
|
press('45');
|
||||||
|
checkDisplay('1.45', 'Can add numbers after new decimal');
|
||||||
|
press('.');
|
||||||
|
checkDisplay('1.45', 'Another decimal should be ignored');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Number input chaos: leading zeros', () => {
|
||||||
|
press('01');
|
||||||
|
checkDisplay('1', 'Leading zero on integer should be replaced');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('000123');
|
||||||
|
checkDisplay('123', 'Multiple leading zeros on integer should be replaced');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('0.123');
|
||||||
|
checkDisplay('0.123', 'Single zero before decimal is kept');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('00.123');
|
||||||
|
checkDisplay('0.123', 'Multiple zeros before decimal are collapsed to one');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('123');
|
||||||
|
press('R'); // C
|
||||||
|
checkDisplay(0, 'Clear should result in 0');
|
||||||
|
press('007');
|
||||||
|
checkDisplay('7', 'Can input number with leading zeros after a clear');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Leading zeros are visually removed during input', () => {
|
||||||
|
press('0');
|
||||||
|
assert.strictEqual(global.last_display_num, '0', 'Input: 0');
|
||||||
|
press('0');
|
||||||
|
assert.strictEqual(global.last_display_num, '0', 'Second 0 should not create "00"');
|
||||||
|
press('7');
|
||||||
|
assert.strictEqual(global.last_display_num, '7', '0 should be replaced by 7');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('0');
|
||||||
|
press('0');
|
||||||
|
press('.');
|
||||||
|
assert.strictEqual(global.last_display_num, '0.', 'Input: 00. -> "0."');
|
||||||
|
press('4');
|
||||||
|
assert.strictEqual(global.last_display_num, '0.4', 'Input: 00.4 -> "0.4"');
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
// After a calculation, starting a new number
|
||||||
|
press('1+2=');
|
||||||
|
checkDisplay(3);
|
||||||
|
press('0');
|
||||||
|
assert.strictEqual(global.last_display_num, '0', 'Post-calc: 0');
|
||||||
|
press('0');
|
||||||
|
assert.strictEqual(global.last_display_num, '0', 'Post-calc: 00 -> "0"');
|
||||||
|
press('5');
|
||||||
|
assert.strictEqual(global.last_display_num, '5', 'Post-calc: 005 -> "5"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Repeated equals', () => {
|
||||||
|
press('2+3=');
|
||||||
|
checkDisplay('5');
|
||||||
|
press('=');
|
||||||
|
checkDisplay('8');
|
||||||
|
press('=');
|
||||||
|
checkDisplay('11');
|
||||||
|
press('R'); press('R');
|
||||||
|
press('10-2=');
|
||||||
|
checkDisplay('8');
|
||||||
|
press('=');
|
||||||
|
checkDisplay('6');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Operator change', () => {
|
||||||
|
press('10+-*/2=');
|
||||||
|
checkDisplay('5'); // 10/2=5
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Long number input', () => {
|
||||||
|
press('1234567');
|
||||||
|
checkDisplay('1234567');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Input starting with decimal point', () => {
|
||||||
|
press('.5');
|
||||||
|
checkDisplay('0.5');
|
||||||
|
press('*2=');
|
||||||
|
checkDisplay('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Negative zero handling', () => {
|
||||||
|
press('0N'); // get -0
|
||||||
|
checkDisplay(-0);
|
||||||
|
press('*5');
|
||||||
|
checkDisplay('5'); // after typing 5, display should be 5
|
||||||
|
press('=');
|
||||||
|
checkDisplay(-0); // -0 * 5 = -0
|
||||||
|
buttonPress('R'); buttonPress('R');
|
||||||
|
press('5*0N='); // 5 * -0 = -0
|
||||||
|
checkDisplay(-0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pi button', () => {
|
||||||
|
press('p');
|
||||||
|
checkDisplay(Math.PI);
|
||||||
|
press('*2=');
|
||||||
|
checkDisplay(Math.PI * 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pi replaces current number entry', () => {
|
||||||
|
press('123p');
|
||||||
|
checkDisplay(Math.PI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Operation with Pi', () => {
|
||||||
|
press('5*p=');
|
||||||
|
checkDisplay(5 * Math.PI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pi as second operand', () => {
|
||||||
|
press('2+p=');
|
||||||
|
checkDisplay(2 + Math.PI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chaining operations with Pi', () => {
|
||||||
|
press('p*2+3=');
|
||||||
|
checkDisplay(Math.PI * 2 + 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unary operations on Pi', () => {
|
||||||
|
press('p');
|
||||||
|
checkDisplay(Math.PI);
|
||||||
|
press('r'); // sqrt
|
||||||
|
checkDisplay(Math.sqrt(Math.PI));
|
||||||
|
press('R'); press('R'); // AC
|
||||||
|
press('p');
|
||||||
|
press('s'); // x^2
|
||||||
|
checkDisplay(Math.PI * Math.PI);
|
||||||
|
press('R'); press('R'); // AC
|
||||||
|
press('p');
|
||||||
|
press('i'); // 1/x
|
||||||
|
checkDisplay(1 / Math.PI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Trig functions with Pi (rad)', () => {
|
||||||
|
buttonPress('angleMode'); // switch to RAD
|
||||||
|
assert.strictEqual(scientificOperators.angleMode.val, 'rad');
|
||||||
|
|
||||||
|
press('p');
|
||||||
|
buttonPress('sin');
|
||||||
|
checkDisplay(0); // Math.sin(Math.PI) is ~0, calculator snaps to 0
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('p');
|
||||||
|
buttonPress('cos');
|
||||||
|
checkDisplay(-1);
|
||||||
|
press('R'); press('R');
|
||||||
|
|
||||||
|
press('p');
|
||||||
|
buttonPress('tan');
|
||||||
|
checkDisplay(0); // Math.tan(Math.PI) is ~0, calculator snaps to 0
|
||||||
|
|
||||||
|
buttonPress('angleMode'); // switch back to DEG
|
||||||
|
assert.strictEqual(scientificOperators.angleMode.val, 'deg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pi after equals', () => {
|
||||||
|
press('1+2=');
|
||||||
|
checkDisplay(3);
|
||||||
|
press('p');
|
||||||
|
checkDisplay(Math.PI);
|
||||||
|
press('+1=');
|
||||||
|
checkDisplay(Math.PI + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pi with negative operator', () => {
|
||||||
|
press('pN');
|
||||||
|
checkDisplay(-Math.PI);
|
||||||
|
press('*2=');
|
||||||
|
checkDisplay(-Math.PI * 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Negative number times Pi', () => {
|
||||||
|
press('2N*p=');
|
||||||
|
checkDisplay(-2 * Math.PI);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run all the defined tests
|
||||||
|
runTests();
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
/* MESSAGES is a list of:
|
|
||||||
{id:int,
|
|
||||||
src,
|
|
||||||
title,
|
|
||||||
subject,
|
|
||||||
body,
|
|
||||||
sender,
|
|
||||||
tel:string,
|
|
||||||
new:true // not read yet
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* For example for maps:
|
|
||||||
|
|
||||||
// a message
|
|
||||||
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Clock","title":"Timer","body":"test"})
|
|
||||||
// maps
|
|
||||||
require("messages").pushMessage({"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="});
|
|
||||||
// call
|
|
||||||
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
|
||||||
*/
|
|
||||||
var Layout = require("Layout");
|
|
||||||
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
|
||||||
var fontSmall = "6x8";
|
|
||||||
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
|
|
||||||
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
|
|
||||||
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
|
|
||||||
var active; // active screen
|
|
||||||
var openMusic = false; // go back to music screen after we handle something else?
|
|
||||||
// hack for 2v10 firmware's lack of ':size' font handling
|
|
||||||
try {
|
|
||||||
g.setFont("6x8:2");
|
|
||||||
} catch (e) {
|
|
||||||
g._setFont = g.setFont;
|
|
||||||
g.setFont = function(f,s) {
|
|
||||||
if (f.includes(":")) {
|
|
||||||
f = f.split(":");
|
|
||||||
return g._setFont(f[0],f[1]);
|
|
||||||
}
|
|
||||||
return g._setFont(f,s);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** this is a timeout if the app has started and is showing a single message
|
|
||||||
but the user hasn't seen it (eg no user input) - in which case
|
|
||||||
we should start a timeout for settings.unreadTimeout to return
|
|
||||||
to the clock. */
|
|
||||||
var unreadTimeout;
|
|
||||||
/// List of all our messages
|
|
||||||
var MESSAGES = require("messages").getMessages();
|
|
||||||
if (Bangle.MESSAGES) {
|
|
||||||
// fast loading messages
|
|
||||||
Bangle.MESSAGES.forEach(m => require("messages").apply(m, MESSAGES));
|
|
||||||
delete Bangle.MESSAGES;
|
|
||||||
}
|
|
||||||
|
|
||||||
var onMessagesModified = function(type,msg) {
|
|
||||||
if (msg.handled) return;
|
|
||||||
msg.handled = true;
|
|
||||||
require("messages").apply(msg, MESSAGES);
|
|
||||||
// TODO: if new, show this new one
|
|
||||||
if (msg && msg.id!=="music" && msg.new && active!="map" &&
|
|
||||||
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
|
|
||||||
require("messages").buzz(msg.src);
|
|
||||||
}
|
|
||||||
if (msg && msg.id=="music") {
|
|
||||||
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to
|
|
||||||
if (active!="music") return; // don't open music over other screens
|
|
||||||
}
|
|
||||||
showMessage(msg&&msg.id);
|
|
||||||
};
|
|
||||||
Bangle.on("message", onMessagesModified);
|
|
||||||
|
|
||||||
function saveMessages() {
|
|
||||||
require("messages").write(MESSAGES);
|
|
||||||
}
|
|
||||||
E.on("kill", saveMessages);
|
|
||||||
|
|
||||||
function showMapMessage(msg) {
|
|
||||||
active = "map";
|
|
||||||
var m, distance, street, target, eta;
|
|
||||||
m=msg.title.match(/(.*) - (.*)/);
|
|
||||||
if (m) {
|
|
||||||
distance = m[1];
|
|
||||||
street = m[2];
|
|
||||||
} else street=msg.title;
|
|
||||||
m=msg.body.match(/(.*) - (.*)/);
|
|
||||||
if (m) {
|
|
||||||
target = m[1];
|
|
||||||
eta = m[2];
|
|
||||||
} else target=msg.body;
|
|
||||||
layout = new Layout({ type:"v", c: [
|
|
||||||
{type:"txt", font:fontMedium, label:target, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 },
|
|
||||||
{type:"h", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, c: [
|
|
||||||
{type:"txt", font:"6x8", label:"Towards" },
|
|
||||||
{type:"txt", font:fontLarge, label:street }
|
|
||||||
]},
|
|
||||||
{type:"h",fillx:1, filly:1, c: [
|
|
||||||
msg.img?{type:"img",src:atob(msg.img), scale:2}:{},
|
|
||||||
{type:"v", fillx:1, c: [
|
|
||||||
{type:"txt", font:fontLarge, label:distance||"" }
|
|
||||||
]},
|
|
||||||
]},
|
|
||||||
{type:"txt", font:"6x8:2", label:eta }
|
|
||||||
]});
|
|
||||||
g.reset().clearRect(Bangle.appRect);
|
|
||||||
layout.render();
|
|
||||||
function back() { // mark as not new and return to menu
|
|
||||||
msg.new = false;
|
|
||||||
layout = undefined;
|
|
||||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0});
|
|
||||||
}
|
|
||||||
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateLabelsInterval;
|
|
||||||
|
|
||||||
function showMusicMessage(msg) {
|
|
||||||
active = "music";
|
|
||||||
// defaults, so e.g. msg.xyz.length doesn't error. `msg` should contain up to date info
|
|
||||||
msg = Object.assign({artist: "", album: "", track: "Music"}, msg);
|
|
||||||
openMusic = msg.state=="play";
|
|
||||||
var trackScrollOffset = 0;
|
|
||||||
var artistScrollOffset = 0;
|
|
||||||
var albumScrollOffset = 0;
|
|
||||||
var trackName = '';
|
|
||||||
var artistName = '';
|
|
||||||
var albumName = '';
|
|
||||||
|
|
||||||
function fmtTime(s) {
|
|
||||||
var m = Math.floor(s/60);
|
|
||||||
s = (parseInt(s%60)).toString().padStart(2,0);
|
|
||||||
return m+":"+s;
|
|
||||||
}
|
|
||||||
function reduceStringAndPad(text, offset, maxLen) {
|
|
||||||
var sliceLength = offset + maxLen > text.length ? text.length - offset : maxLen;
|
|
||||||
return text.substr(offset, sliceLength).padEnd(maxLen, " ");
|
|
||||||
}
|
|
||||||
function back() {
|
|
||||||
clearInterval(updateLabelsInterval);
|
|
||||||
updateLabelsInterval = undefined;
|
|
||||||
openMusic = false;
|
|
||||||
var wasNew = msg.new;
|
|
||||||
msg.new = false;
|
|
||||||
layout = undefined;
|
|
||||||
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0});
|
|
||||||
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
|
||||||
}
|
|
||||||
function updateLabels() {
|
|
||||||
trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13);
|
|
||||||
artistName = reduceStringAndPad(msg.artist, artistScrollOffset, 21);
|
|
||||||
albumName = reduceStringAndPad(msg.album, albumScrollOffset, 21);
|
|
||||||
|
|
||||||
trackScrollOffset++;
|
|
||||||
artistScrollOffset++;
|
|
||||||
albumScrollOffset++;
|
|
||||||
|
|
||||||
if ((trackScrollOffset + 13) > msg.track.length) trackScrollOffset = 0;
|
|
||||||
if ((artistScrollOffset + 21) > msg.artist.length) artistScrollOffset = 0;
|
|
||||||
if ((albumScrollOffset + 21) > msg.album.length) albumScrollOffset = 0;
|
|
||||||
}
|
|
||||||
updateLabels();
|
|
||||||
|
|
||||||
layout = new Layout({ type:"v", c: [
|
|
||||||
{type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [
|
|
||||||
{ type:"v", fillx:1, c: [
|
|
||||||
{ type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:artistName, pad:2, id:"artist" },
|
|
||||||
{ type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:albumName, pad:2, id:"album" }
|
|
||||||
]}
|
|
||||||
]},
|
|
||||||
{type:"txt", font:fontLarge, bgCol:g.theme.bg, label:trackName, fillx:1, filly:1, pad:2, id:"track" },
|
|
||||||
Bangle.musicControl?{type:"h",fillx:1, c: [
|
|
||||||
{type:"btn", pad:8, label:"\0"+atob("FhgBwAADwAAPwAA/wAD/gAP/gA//gD//gP//g///j///P//////////P//4//+D//gP/4A/+AD/gAP8AA/AADwAAMAAA"), cb:()=>Bangle.musicControl("play")}, // play
|
|
||||||
{type:"btn", pad:8, label:"\0"+atob("EhaBAHgHvwP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP3gHg"), cb:()=>Bangle.musicControl("pause")}, // pause
|
|
||||||
{type:"btn", pad:8, label:"\0"+atob("EhKBAMAB+AB/gB/wB/8B/+B//B//x//5//5//x//B/+B/8B/wB/gB+AB8ABw"), cb:()=>Bangle.musicControl("next")}, // next
|
|
||||||
]}:{},
|
|
||||||
{type:"txt", font:"6x8:2", label:msg.dur?fmtTime(msg.dur):"--:--" }
|
|
||||||
]}, { back : back });
|
|
||||||
g.reset().clearRect(Bangle.appRect);
|
|
||||||
layout.render();
|
|
||||||
|
|
||||||
updateLabelsInterval = setInterval(function() {
|
|
||||||
updateLabels();
|
|
||||||
layout.artist.label = artistName;
|
|
||||||
layout.album.label = albumName;
|
|
||||||
layout.track.label = trackName;
|
|
||||||
layout.render();
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessageScroller(msg) {
|
|
||||||
active = "scroller";
|
|
||||||
var bodyFont = fontBig;
|
|
||||||
g.setFont(bodyFont);
|
|
||||||
var lines = [];
|
|
||||||
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10)
|
|
||||||
var titleCnt = lines.length;
|
|
||||||
if (titleCnt) lines.push(""); // add blank line after title
|
|
||||||
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
|
|
||||||
E.showScroller({
|
|
||||||
h : g.getFontHeight(), // height of each menu item in pixels
|
|
||||||
c : lines.length, // number of menu items
|
|
||||||
// a function to draw a menu item
|
|
||||||
draw : function(idx, r) {
|
|
||||||
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
|
|
||||||
g.setBgColor(idx<titleCnt ? g.theme.bg2 : g.theme.bg).
|
|
||||||
setColor(idx<titleCnt ? g.theme.fg2 : g.theme.fg).
|
|
||||||
clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
|
||||||
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
|
|
||||||
}, select : function(idx) {
|
|
||||||
if (idx>=lines.length-2)
|
|
||||||
showMessage(msg.id);
|
|
||||||
},
|
|
||||||
back : () => showMessage(msg.id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessageSettings(msg) {
|
|
||||||
active = "settings";
|
|
||||||
E.showMenu({"":{"title":/*LANG*/"Message"},
|
|
||||||
"< Back" : () => showMessage(msg.id),
|
|
||||||
/*LANG*/"View Message" : () => {
|
|
||||||
showMessageScroller(msg);
|
|
||||||
},
|
|
||||||
/*LANG*/"Delete" : () => {
|
|
||||||
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
|
||||||
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
|
||||||
},
|
|
||||||
/*LANG*/"Mark Unread" : () => {
|
|
||||||
msg.new = true;
|
|
||||||
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
|
||||||
},
|
|
||||||
/*LANG*/"Mark all read" : () => {
|
|
||||||
MESSAGES.forEach(msg => msg.new = false);
|
|
||||||
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
|
||||||
},
|
|
||||||
/*LANG*/"Delete all messages" : () => {
|
|
||||||
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
|
|
||||||
if (isYes) {
|
|
||||||
MESSAGES = [];
|
|
||||||
}
|
|
||||||
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessage(msgid) {
|
|
||||||
var msg = MESSAGES.find(m=>m.id==msgid);
|
|
||||||
if (updateLabelsInterval) {
|
|
||||||
clearInterval(updateLabelsInterval);
|
|
||||||
updateLabelsInterval=undefined;
|
|
||||||
}
|
|
||||||
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found
|
|
||||||
if (msg.id=="music") {
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
return showMusicMessage(msg);
|
|
||||||
}
|
|
||||||
if (msg.src=="Maps") {
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
return showMapMessage(msg);
|
|
||||||
}
|
|
||||||
active = "message";
|
|
||||||
// Normal text message display
|
|
||||||
var title=msg.title, titleFont = fontLarge, lines;
|
|
||||||
var body=msg.body, bodyFont = fontLarge;
|
|
||||||
// If no body, use the title text instead...
|
|
||||||
if (body===undefined) {
|
|
||||||
body = title;
|
|
||||||
title = undefined;
|
|
||||||
}
|
|
||||||
if (title) {
|
|
||||||
var w = g.getWidth()-48;
|
|
||||||
if (g.setFont(titleFont).stringWidth(title) > w) {
|
|
||||||
titleFont = fontBig;
|
|
||||||
if (settings.fontSize!=1 && g.setFont(titleFont).stringWidth(title) > w)
|
|
||||||
titleFont = fontMedium;
|
|
||||||
}
|
|
||||||
if (g.setFont(titleFont).stringWidth(title) > w) {
|
|
||||||
lines = g.wrapString(title, w);
|
|
||||||
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If body of message is only two lines long w/ large font, use large font.
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
var w = g.getWidth()-10;
|
|
||||||
if (g.setFont(bodyFont).stringWidth(body) > w * 2) {
|
|
||||||
bodyFont = fontBig;
|
|
||||||
if (settings.fontSize!=1 && g.setFont(bodyFont).stringWidth(body) > w * 3)
|
|
||||||
bodyFont = fontMedium;
|
|
||||||
}
|
|
||||||
if (g.setFont(bodyFont).stringWidth(body) > w) {
|
|
||||||
lines = g.setFont(bodyFont).wrapString(msg.body, w);
|
|
||||||
var maxLines = Math.floor((g.getHeight()-110) / g.getFontHeight());
|
|
||||||
body = (lines.length>maxLines) ? lines.slice(0,maxLines).join("\n")+"..." : lines.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function goBack() {
|
|
||||||
layout = undefined;
|
|
||||||
msg.new = false; // read mail
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic});
|
|
||||||
}
|
|
||||||
var buttons = [
|
|
||||||
];
|
|
||||||
if (msg.positive) {
|
|
||||||
buttons.push({type:"btn", src:atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="), cb:()=>{
|
|
||||||
msg.new = false;
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
Bangle.messageResponse(msg,true);
|
|
||||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
if (msg.negative) {
|
|
||||||
if (buttons.length) buttons.push({width:32}); // nasty hack...
|
|
||||||
buttons.push({type:"btn", src:atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="), cb:()=>{
|
|
||||||
msg.new = false;
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
Bangle.messageResponse(msg,false);
|
|
||||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
layout = new Layout({ type:"v", c: [
|
|
||||||
{type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [
|
|
||||||
{ type:"v", fillx:1, c: [
|
|
||||||
{type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 },
|
|
||||||
title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{},
|
|
||||||
]},
|
|
||||||
{ type:"btn",
|
|
||||||
src:require("messageicons").getImage(msg),
|
|
||||||
col:require("messageicons").getColor(msg, {settings:settings, default:g.theme.fg2}),
|
|
||||||
pad: 3, cb:()=>{
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
showMessageSettings(msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]},
|
|
||||||
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{
|
|
||||||
// allow tapping to show a larger version
|
|
||||||
showMessageScroller(msg);
|
|
||||||
} },
|
|
||||||
{type:"h",fillx:1, c: buttons}
|
|
||||||
]},{back:goBack});
|
|
||||||
g.reset().clearRect(Bangle.appRect);
|
|
||||||
layout.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* options = {
|
|
||||||
clockIfNoMsg : bool
|
|
||||||
clockIfAllRead : bool
|
|
||||||
showMsgIfUnread : bool
|
|
||||||
openMusic : bool // open music if it's playing
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
function checkMessages(options) {
|
|
||||||
options=options||{};
|
|
||||||
|
|
||||||
if (MESSAGES.length && MESSAGES[0].title == "Timer") {
|
|
||||||
MESSAGES.pop(0);
|
|
||||||
return load();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no messages, just show 'no messages' and return
|
|
||||||
if (!MESSAGES.length) {
|
|
||||||
if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{
|
|
||||||
title:/*LANG*/"Messages",
|
|
||||||
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
|
|
||||||
buttons : {/*LANG*/"Ok":1}
|
|
||||||
}).then(() => { load() });
|
|
||||||
return load();
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have >0 messages
|
|
||||||
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
|
|
||||||
// If we have a new message, show it
|
|
||||||
if (options.showMsgIfUnread && newMessages.length) {
|
|
||||||
delete newMessages[0].show; // stop us getting stuck here if we're called a second time
|
|
||||||
showMessage(newMessages[0].id);
|
|
||||||
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
|
|
||||||
if (global.BUZZ_ON_NEW_MESSAGE) {
|
|
||||||
// this is set if we entered the messages app by loading `messagegui.new.js`
|
|
||||||
// ... but only buzz the first time we view a new message
|
|
||||||
global.BUZZ_ON_NEW_MESSAGE = false;
|
|
||||||
// messages.buzz respects quiet mode - no need to check here
|
|
||||||
require("messages").buzz(newMessages[0].src);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// no new messages: show playing music? Only if we have playing music, or state=="show" (set by messagesmusic)
|
|
||||||
if (options.openMusic && MESSAGES.some(m=>m.id=="music" && ((m.track && m.state=="play") || m.state=="show")))
|
|
||||||
return showMessage('music');
|
|
||||||
// no new messages - go to clock?
|
|
||||||
if (options.clockIfAllRead && newMessages.length==0)
|
|
||||||
return load();
|
|
||||||
active = "main";
|
|
||||||
// Otherwise show a menu
|
|
||||||
E.showScroller({
|
|
||||||
h : 48,
|
|
||||||
c : Math.max(MESSAGES.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11)
|
|
||||||
draw : function(idx, r) {"ram"
|
|
||||||
var msg = MESSAGES[idx];
|
|
||||||
if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH);
|
|
||||||
else g.setBgColor(g.theme.bg).setColor(g.theme.fg);
|
|
||||||
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
|
||||||
if (!msg) return;
|
|
||||||
var x = r.x+2, title = msg.title, body = msg.body;
|
|
||||||
var img = require("messageicons").getImage(msg);
|
|
||||||
if (msg.id=="music") {
|
|
||||||
title = msg.artist || /*LANG*/"Music";
|
|
||||||
body = msg.track;
|
|
||||||
}
|
|
||||||
if (img) {
|
|
||||||
var fg = g.getColor(),
|
|
||||||
col = require("messageicons").getColor(msg, {settings:settings, default:fg});
|
|
||||||
g.setColor(col).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
|
|
||||||
.setColor(fg); // only color the icon
|
|
||||||
x += 50;
|
|
||||||
}
|
|
||||||
var m = msg.title+"\n"+msg.body, longBody=false;
|
|
||||||
if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2);
|
|
||||||
if (body) {
|
|
||||||
g.setFontAlign(-1,-1).setFont("6x8");
|
|
||||||
var l = g.wrapString(body, r.w-(x+14));
|
|
||||||
if (l.length>3) {
|
|
||||||
l = l.slice(0,3);
|
|
||||||
l[l.length-1]+="...";
|
|
||||||
}
|
|
||||||
longBody = l.length>2;
|
|
||||||
g.drawString(l.join("\n"), x+10,r.y+20);
|
|
||||||
}
|
|
||||||
if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2);
|
|
||||||
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
|
|
||||||
},
|
|
||||||
select : idx => showMessage(MESSAGES[idx].id),
|
|
||||||
back : () => load()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function cancelReloadTimeout() {
|
|
||||||
if (!unreadTimeout) return;
|
|
||||||
clearTimeout(unreadTimeout);
|
|
||||||
unreadTimeout = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
g.clear();
|
|
||||||
|
|
||||||
Bangle.loadWidgets();
|
|
||||||
require("messages").toggleWidget(false);
|
|
||||||
Bangle.drawWidgets();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60;
|
|
||||||
if (settings.unreadTimeout)
|
|
||||||
unreadTimeout = setTimeout(load, settings.unreadTimeout*1000);
|
|
||||||
// only openMusic on launch if music is new, or state=="show" (set by messagesmusic)
|
|
||||||
var musicMsg = MESSAGES.find(m => m.id === "music");
|
|
||||||
checkMessages({
|
|
||||||
clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1,
|
|
||||||
openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show") });
|
|
||||||
}, 10); // if checkMessages wants to 'load', do that
|
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
/* MESSAGES is a list of:
|
||||||
|
{id:int,
|
||||||
|
src,
|
||||||
|
title,
|
||||||
|
subject,
|
||||||
|
body,
|
||||||
|
sender,
|
||||||
|
tel:string,
|
||||||
|
new:true // not read yet
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* For example for maps:
|
||||||
|
|
||||||
|
// a message
|
||||||
|
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title":"My Friend","body":"Hey! How's everything going?",positive:1,negative:1})
|
||||||
|
// maps
|
||||||
|
GB({t:"nav",src:"maps",title:"Navigation",instr:"High St towards Tollgate Rd",distance:"966yd",action:"continue",eta:"08:39"})
|
||||||
|
GB({t:"nav",src:"maps",title:"Navigation",instr:"High St",distance:"12km",action:"left_slight",eta:"08:39"})
|
||||||
|
GB({t:"nav",src:"maps",title:"Navigation",instr:"Main St / I-29 ALT / Centerpoint Dr",distance:12345,action:"left_slight",eta:"08:39"})
|
||||||
|
// call
|
||||||
|
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
||||||
|
|
||||||
|
// tanner
|
||||||
|
require("messages").pushMessage({"t":"add","id":"1234","src":"Telegram","title":"testing","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",positive:true,negative:true})
|
||||||
|
*/
|
||||||
|
var Layout = require("Layout");
|
||||||
|
var layout; // global var containing the layout for the currently displayed message
|
||||||
|
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||||
|
var reply;
|
||||||
|
try { reply = require("reply"); } catch (e) {}
|
||||||
|
var fontSmall = "6x8";
|
||||||
|
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
|
||||||
|
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
|
||||||
|
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
|
||||||
|
var fontVLarge = g.getFonts().includes("6x15")?"12x20:2":"6x8:5";
|
||||||
|
|
||||||
|
// If a font library is installed, just switch to using that for everything in messages
|
||||||
|
if (Graphics.prototype.setFontIntl) {
|
||||||
|
fontSmall = "Intl";
|
||||||
|
fontMedium = "Intl";
|
||||||
|
fontBig = "Intl";
|
||||||
|
/* 2v21 and before have a bug where the scale factor for PBF fonts wasn't
|
||||||
|
taken into account in metrics, so we can't have big fonts on those firmwares.
|
||||||
|
Having 'PBF' listed as a font was a bug fixed at the same time so we check for that. */
|
||||||
|
let noScale = g.getFonts().includes("PBF");
|
||||||
|
fontLarge = noScale?"Intl":"Intl:2";
|
||||||
|
fontVLarge = noScale?"Intl":"Intl:3";
|
||||||
|
}
|
||||||
|
|
||||||
|
var active; // active screen (undefined/"list"/"music"/"map"/"message"/"scroller"/"settings")
|
||||||
|
var openMusic = false; // go back to music screen after we handle something else?
|
||||||
|
var replying = false; // If we're replying to a message, don't interrupt
|
||||||
|
// hack for 2v10 firmware's lack of ':size' font handling
|
||||||
|
try {
|
||||||
|
g.setFont("6x8:2");
|
||||||
|
} catch (e) {
|
||||||
|
g._setFont = g.setFont;
|
||||||
|
g.setFont = function(f,s) {
|
||||||
|
if (f.includes(":")) {
|
||||||
|
f = f.split(":");
|
||||||
|
return g._setFont(f[0],f[1]);
|
||||||
|
}
|
||||||
|
return g._setFont(f,s);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** this is a timeout if the app has started and is showing a single message
|
||||||
|
but the user hasn't seen it (eg no user input) - in which case
|
||||||
|
we should start a timeout for settings.unreadTimeout to return
|
||||||
|
to the clock. */
|
||||||
|
var unreadTimeout;
|
||||||
|
/// List of all our messages
|
||||||
|
var MESSAGES = require("messages").getMessages();
|
||||||
|
if (Bangle.MESSAGES) {
|
||||||
|
// fast loading messages
|
||||||
|
Bangle.MESSAGES.forEach(m => require("messages").apply(m, MESSAGES));
|
||||||
|
delete Bangle.MESSAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
var onMessagesModified = function(type,msg) {
|
||||||
|
if (msg.handled) return;
|
||||||
|
msg.handled = true;
|
||||||
|
require("messages").apply(msg, MESSAGES);
|
||||||
|
// TODO: if new, show this new one
|
||||||
|
if (msg && msg.id!=="music" && msg.id!=="nav" && msg.new &&
|
||||||
|
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
|
||||||
|
require("messages").buzz(msg.src);
|
||||||
|
}
|
||||||
|
if (msg && msg.id=="music") {
|
||||||
|
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to
|
||||||
|
if ((active!=undefined) && (active!="list") && (active!="music")) return; // don't open music over other screens (but do if we're in the main menu)
|
||||||
|
}
|
||||||
|
if (msg && msg.id=="nav" && msg.t=="modify" && active!="map")
|
||||||
|
return; // don't show an updated nav message if we're just in the menu
|
||||||
|
showMessage(msg&&msg.id, false);
|
||||||
|
};
|
||||||
|
Bangle.on("message", onMessagesModified);
|
||||||
|
|
||||||
|
function saveMessages() {
|
||||||
|
require("messages").write(MESSAGES);
|
||||||
|
}
|
||||||
|
E.on("kill", saveMessages);
|
||||||
|
|
||||||
|
function showMapMessage(msg) {
|
||||||
|
active = "map";
|
||||||
|
require("messages").stopBuzz(); // stop repeated buzzing while the map is showing
|
||||||
|
var m, distance, street, target, img;
|
||||||
|
if ("string"==typeof msg.distance) // new gadgetbridge
|
||||||
|
distance = msg.distance;
|
||||||
|
else if ("number"==typeof msg.distance) // 0.74 gadgetbridge
|
||||||
|
distance = require("locale").distance(msg.distance);
|
||||||
|
if (msg.instr) {
|
||||||
|
var instr = msg.instr.replace(/\s*\/\s*/g," \/\n"); // convert slashes to newlines
|
||||||
|
if (instr.includes("towards") || instr.includes("toward")) {
|
||||||
|
m = instr.split(/towards|toward/);
|
||||||
|
target = m[0].trim();
|
||||||
|
street = m[1].trim();
|
||||||
|
}else
|
||||||
|
target = instr;
|
||||||
|
}
|
||||||
|
var carIsRHD = !!settings.carIsRHD;
|
||||||
|
switch (msg.action) {
|
||||||
|
case "continue": img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA";break;
|
||||||
|
case "left": img = "GhcBAYAAAPAAAHwAAD4AAB8AAA+AAAf//8P///x///+PAAPx4AA8fAAHD4ABwfAAcDwAHAIABwAAAcAAAHAAABwAAAcAAAHAAABwAAAc";break;
|
||||||
|
case "right": img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";break;
|
||||||
|
case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break;
|
||||||
|
case "right_slight": img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";break;
|
||||||
|
case "left_sharp": img = "GBaBAAAA+AAB/AAH/gAPjgAeBwA8BwB4B+DwB+HgB+PAB+eAB+8AB+4AB/wAB/gAB//gB//gB//gBwAABwAABwAABwAABw=="; break;
|
||||||
|
case "right_sharp": img = "GBaBAB8AAD+AAH/gAHHwAOB4AOA8AOAeAOAPB+AHh+ADx+AB5+AA9+AAd+AAP+AAH+AH/+AH/+AH/+AAAOAAAOAAAOAAAA==";break;
|
||||||
|
case "keep_left": img = "ERmBAACAAOAB+AD+AP+B/+H3+PO+8c8w4wBwADgAHgAPAAfAAfAAfAAfAAeAAeAAcAA8AA4ABwADgA==";break;
|
||||||
|
case "keep_right": img = "ERmBAACAAOAA/AD+AP+A//D/fPueeceY4YBwADgAPAAeAB8AHwAfAB8ADwAPAAcAB4ADgAHAAOAAAA==";break;
|
||||||
|
case "uturn_left": img = "GRiBAAAH4AAP/AAP/wAPj8APAfAPAHgHgB4DgA8BwAOA4AHAcADsOMB/HPA7zvgd9/gOf/gHH/gDh/gBwfgA4DgAcBgAOAAAHAAADgAABw==";break;
|
||||||
|
case "uturn_right": img = "GRiBAAPwAAf+AAf/gAfj4AfAeAPAHgPADwHgA4DgAcBwAOA4AHAcBjhuB5x/A+57gP99wD/84A/8cAP8OAD8HAA4DgAMBwAAA4AAAcAAAA==";break;
|
||||||
|
case "finish": img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A";break;
|
||||||
|
case "roundabout_left": img = carIsRHD ? "HBaCAAADwAAAAAAAD/AAAVUAAD/wABVVUAD/wABVVVQD/wAAVABUD/wAAVAAFT/////wABX/////8AAF//////AABT/////wABUP/AAD/AAVA/8AA/8AVAD/wAD//VQAP/AAP/1QAA/wAA/9AAADwAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AA=" : "HRYCAAPAAAAAAAAD/AAD//AAA/8AD///AAP/AA////AD/wAD/wP/A/8AA/wAP8P/////AAP//////8AA///////AAD/P////8AAP8P/AABUAD/AP/AAFUA/8AP/AAFX//AAP/AAFf/wAAP8AAB/8AAAPAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAA==";break;
|
||||||
|
case "roundabout_right": img = carIsRHD ? "HRaCAAAAAAAA8AAAP/8AAP8AAD///AA/8AA////AA/8AP/A/8AA/8A/wAP8AA/8P8AA/////8/wAD///////AAD//////8AAP////8P8ABUAAP/A/8AVQAD/wA//1UAA/8AA//VAAP/AAA/9AAA/wAAAPwAAA8AAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAA=" : "HBYCAAAAAAPAAABVQAAP8AAFVVQAD/wAFVVVAAP/ABUAFQAA/8BUAAVAAD/wVAAP/////FAAD/////9QAA//////VAAP/////FQAP8AAP/AVAP/AAP/AFX//AAP/AAV//AAP/AAAf/AAD/AAAD/AAAPAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAA==";break;
|
||||||
|
case "roundabout_straight": img = carIsRHD ? "EBuCAAADwAAAD/AAAD/8AAD//wAD///AD///8D/P8/z/D/D//A/wPzAP8AwA//UAA//1QA//9VA/8AFUP8AAVD8AAFQ/AABUPwAAVD8AAFQ/wABUP/ABVA//9VAD//VAAP/1AAAP8AAAD/AAAA/wAA==" : "EBsCAAPAAAAP8AAAP/wAAP//AAP//8AP///wP8/z/P8P8P/8D/A/MA/wDABf/wABX//ABV//8BVAD/wVAAP8FQAA/BUAAPwVAAD8FQAA/BUAA/wVQA/8BV//8AFf/8AAX/8AAA/wAAAP8AAAD/AA";break;
|
||||||
|
case "roundabout_uturn": img = carIsRHD ? "ICCBAAAAAAAAAAAAAAAAAAAP4AAAH/AAAD/4AAB4fAAA8DwAAPAcAADgHgAA4B4AAPAcAADwPAAAeHwAADz4AAAc8AAABPAAAADwAAAY8YAAPPPAAD73gAAf/4AAD/8AABf8AAAb+AAAHfAAABzwAAAcYAAAAAAAAAAAAAAAAAAAAAAA" : "ICABAAAAAAAAAAAAAAAAAAfwAAAP+AAAH/wAAD4eAAA8DwAAOA8AAHgHAAB4BwAAOA8AADwPAAA+HgAAHzwAAA84AAAPIAAADwAAAY8YAAPPPAAB73wAAf/4AAD/8AAAP+gAAB/YAAAPuAAADzgAAAY4AAAAAAAAAAAAAAAAAAAAAAA=";break;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout = new Layout({ type:"v", c: [
|
||||||
|
{type:"txt", font:street?fontMedium:fontLarge, label:target, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:3 },
|
||||||
|
street?{type:"h", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, c: [
|
||||||
|
{type:"txt", font:"6x8", label:"Towards" },
|
||||||
|
{type:"txt", font:fontLarge, label:street }
|
||||||
|
]}:{},
|
||||||
|
{type:"h",fillx:1, filly:1, c: [
|
||||||
|
img?{type:"img",src:atob(img), scale:2, pad:6}:{},
|
||||||
|
{type:"v", fillx:1, c: [
|
||||||
|
{type:"txt", font:fontVLarge, label:distance||"" }
|
||||||
|
]},
|
||||||
|
]},
|
||||||
|
{type:"txt", font:"6x8:2", label:msg.eta?`ETA ${msg.eta}`:"" }
|
||||||
|
]});
|
||||||
|
g.reset().clearRect(Bangle.appRect);
|
||||||
|
layout.render();
|
||||||
|
function back() { // mark as not new and return to menu
|
||||||
|
msg.new = false;
|
||||||
|
layout = undefined;
|
||||||
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0});
|
||||||
|
}
|
||||||
|
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateLabelsInterval;
|
||||||
|
|
||||||
|
function showMusicMessage(msg) {
|
||||||
|
active = "music";
|
||||||
|
// defaults, so e.g. msg.xyz.length doesn't error. `msg` should contain up to date info
|
||||||
|
msg = Object.assign({artist: "", album: "", track: "Music"}, msg);
|
||||||
|
openMusic = msg.state=="play";
|
||||||
|
var trackScrollOffset = 0;
|
||||||
|
var artistScrollOffset = 0;
|
||||||
|
var albumScrollOffset = 0;
|
||||||
|
var trackName = '';
|
||||||
|
var artistName = '';
|
||||||
|
var albumName = '';
|
||||||
|
|
||||||
|
function fmtTime(s) {
|
||||||
|
var m = Math.floor(s/60);
|
||||||
|
s = (parseInt(s%60)).toString().padStart(2,0);
|
||||||
|
return m+":"+s;
|
||||||
|
}
|
||||||
|
function reduceStringAndPad(text, offset, maxLen) {
|
||||||
|
var sliceLength = offset + maxLen > text.length ? text.length - offset : maxLen;
|
||||||
|
return text.substr(offset, sliceLength).padEnd(maxLen, " ");
|
||||||
|
}
|
||||||
|
function back() {
|
||||||
|
clearInterval(updateLabelsInterval);
|
||||||
|
updateLabelsInterval = undefined;
|
||||||
|
openMusic = false;
|
||||||
|
var wasNew = msg.new;
|
||||||
|
msg.new = false;
|
||||||
|
layout = undefined;
|
||||||
|
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0});
|
||||||
|
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||||
|
}
|
||||||
|
function updateLabels() {
|
||||||
|
trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13);
|
||||||
|
artistName = reduceStringAndPad(msg.artist, artistScrollOffset, 21);
|
||||||
|
albumName = reduceStringAndPad(msg.album, albumScrollOffset, 21);
|
||||||
|
|
||||||
|
trackScrollOffset++;
|
||||||
|
artistScrollOffset++;
|
||||||
|
albumScrollOffset++;
|
||||||
|
|
||||||
|
if ((trackScrollOffset + 13) > msg.track.length) trackScrollOffset = 0;
|
||||||
|
if ((artistScrollOffset + 21) > msg.artist.length) artistScrollOffset = 0;
|
||||||
|
if ((albumScrollOffset + 21) > msg.album.length) albumScrollOffset = 0;
|
||||||
|
}
|
||||||
|
updateLabels();
|
||||||
|
|
||||||
|
layout = new Layout({ type:"v", c: [
|
||||||
|
{type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [
|
||||||
|
{ type:"v", fillx:1, c: [
|
||||||
|
{ type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:artistName, pad:2, id:"artist" },
|
||||||
|
{ type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:albumName, pad:2, id:"album" }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
{type:"txt", font:fontLarge, bgCol:g.theme.bg, label:trackName, fillx:1, filly:1, pad:2, id:"track" },
|
||||||
|
Bangle.musicControl?{type:"h",fillx:1, c: [
|
||||||
|
{type:"btn", pad:8, label:"\0"+atob("FhgBwAADwAAPwAA/wAD/gAP/gA//gD//gP//g///j///P//////////P//4//+D//gP/4A/+AD/gAP8AA/AADwAAMAAA"), cb:()=>Bangle.musicControl("play")}, // play
|
||||||
|
{type:"btn", pad:8, label:"\0"+atob("EhaBAHgHvwP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP3gHg"), cb:()=>Bangle.musicControl("pause")}, // pause
|
||||||
|
{type:"btn", pad:8, label:"\0"+atob("EhKBAMAB+AB/gB/wB/8B/+B//B//x//5//5//x//B/+B/8B/wB/gB+AB8ABw"), cb:()=>Bangle.musicControl("next")}, // next
|
||||||
|
]}:{},
|
||||||
|
{type:"txt", font:"6x8:2", label:msg.dur?fmtTime(msg.dur):"--:--" }
|
||||||
|
]}, { back : back });
|
||||||
|
g.reset().clearRect(Bangle.appRect);
|
||||||
|
layout.render();
|
||||||
|
|
||||||
|
updateLabelsInterval = setInterval(function() {
|
||||||
|
updateLabels();
|
||||||
|
layout.artist.label = artistName;
|
||||||
|
layout.album.label = albumName;
|
||||||
|
layout.track.label = trackName;
|
||||||
|
layout.render();
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessageScroller(msg) {
|
||||||
|
//cancelReloadTimeout(); // commented out because this is the new message display and we want it to disappear
|
||||||
|
active = "scroller";
|
||||||
|
var bodyFont = fontLarge;
|
||||||
|
g.setFont(bodyFont);
|
||||||
|
var lines = [];
|
||||||
|
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10);
|
||||||
|
lines = [lines[0]];
|
||||||
|
var titleCnt = lines.length;
|
||||||
|
//if (titleCnt) lines.push(""); // add blank line after title
|
||||||
|
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10)); //,["",/*LANG*/"< Back"]);
|
||||||
|
E.showScroller({
|
||||||
|
h : g.getFontHeight(), // height of each menu item in pixels
|
||||||
|
c : lines.length, // number of menu items
|
||||||
|
// a function to draw a menu item
|
||||||
|
draw : function(idx, r) {
|
||||||
|
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
|
||||||
|
g.setBgColor(idx<titleCnt ? g.theme.bg2 : g.theme.bg).
|
||||||
|
setColor(idx<titleCnt ? g.theme.fg2 : g.theme.fg).
|
||||||
|
clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||||
|
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
|
||||||
|
}, select : function(idx) {
|
||||||
|
if (idx==0) {
|
||||||
|
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
showMessageSettings(msg);
|
||||||
|
} else {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
back : () => load()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessageSettings(msg) {
|
||||||
|
active = "settings";
|
||||||
|
var menu = {"":{"title":/*LANG*/"Message"},
|
||||||
|
"< Back" : () => showMessage(msg.id, true),
|
||||||
|
/*LANG*/"View Message" : () => {
|
||||||
|
showMessageScroller(msg);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (msg.reply && reply) {
|
||||||
|
menu[/*LANG*/"Reply"] = () => {
|
||||||
|
replying = true;
|
||||||
|
reply.reply({msg: msg})
|
||||||
|
.then(result => {
|
||||||
|
Bluetooth.println(JSON.stringify(result));
|
||||||
|
replying = false;
|
||||||
|
showMessage(msg.id);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
replying = false;
|
||||||
|
showMessage(msg.id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = Object.assign(menu, {
|
||||||
|
/*LANG*/"Delete" : () => {
|
||||||
|
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
||||||
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Bangle.messageIgnore && msg.src)
|
||||||
|
menu[/*LANG*/"Ignore"] = () => {
|
||||||
|
E.showPrompt(/*LANG*/"Ignore all messages from "+E.toJS(msg.src)+"?", {title:/*LANG*/"Ignore"}).then(isYes => {
|
||||||
|
if (isYes) {
|
||||||
|
Bangle.messageIgnore(msg);
|
||||||
|
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
||||||
|
}
|
||||||
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
menu = Object.assign(menu, {
|
||||||
|
/*LANG*/"Mark Unread" : () => {
|
||||||
|
msg.new = true;
|
||||||
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||||
|
},
|
||||||
|
/*LANG*/"Mark all read" : () => {
|
||||||
|
MESSAGES.forEach(msg => msg.new = false);
|
||||||
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||||
|
},
|
||||||
|
/*LANG*/"Delete all messages" : () => {
|
||||||
|
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
|
||||||
|
if (isYes) {
|
||||||
|
MESSAGES = [];
|
||||||
|
}
|
||||||
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
E.showMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(msgid, persist) {
|
||||||
|
if (replying) { return; }
|
||||||
|
if(!persist) resetReloadTimeout();
|
||||||
|
let idx = MESSAGES.findIndex(m=>m.id==msgid);
|
||||||
|
var msg = MESSAGES[idx];
|
||||||
|
if (updateLabelsInterval) {
|
||||||
|
clearInterval(updateLabelsInterval);
|
||||||
|
updateLabelsInterval=undefined;
|
||||||
|
}
|
||||||
|
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found
|
||||||
|
if (msg.id=="music") {
|
||||||
|
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
return showMusicMessage(msg);
|
||||||
|
}
|
||||||
|
if (msg.id=="nav") {
|
||||||
|
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
return showMapMessage(msg);
|
||||||
|
}
|
||||||
|
// active = "message";
|
||||||
|
// // Normal text message display
|
||||||
|
// var title=msg.title, titleFont = fontLarge, lines;
|
||||||
|
// var body=msg.body, bodyFont = fontLarge;
|
||||||
|
// // If no body, use the title text instead...
|
||||||
|
// if (body===undefined) {
|
||||||
|
// body = title;
|
||||||
|
// title = undefined;
|
||||||
|
// }
|
||||||
|
// if (title) {
|
||||||
|
// var w = g.getWidth()-48;
|
||||||
|
// if (g.setFont(titleFont).stringWidth(title) > w) {
|
||||||
|
// titleFont = fontBig;
|
||||||
|
// if (settings.fontSize!=1 && g.setFont(titleFont).stringWidth(title) > w)
|
||||||
|
// titleFont = fontMedium;
|
||||||
|
// }
|
||||||
|
// if (g.setFont(titleFont).stringWidth(title) > w) {
|
||||||
|
// lines = g.wrapString(title, w);
|
||||||
|
// title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (body) { // Try and find a font that fits...
|
||||||
|
// var w = g.getWidth()-2, h = Bangle.appRect.h-60;
|
||||||
|
// if (g.setFont(bodyFont).wrapString(body, w).length*g.getFontHeight() > h) {
|
||||||
|
// bodyFont = fontBig;
|
||||||
|
// if (settings.fontSize!=1 && g.setFont(bodyFont).wrapString(body, w).length*g.getFontHeight() > h) {
|
||||||
|
// bodyFont = fontMedium;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // Now crop, given whatever font we have available
|
||||||
|
// lines = g.setFont(bodyFont).wrapString(body, w);
|
||||||
|
// var maxLines = Math.floor(h / g.getFontHeight());
|
||||||
|
// if (lines.length>maxLines) // if too long, wrap with a bit less spae so we have room for '...'
|
||||||
|
// body = g.setFont(bodyFont).wrapString(body, w-10).slice(0,maxLines).join("\n")+"...";
|
||||||
|
// else
|
||||||
|
// body = lines.join("\n");
|
||||||
|
// }
|
||||||
|
// function goBack() {
|
||||||
|
// layout = undefined;
|
||||||
|
// msg.new = false; // read mail
|
||||||
|
// cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
// checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic});
|
||||||
|
// }
|
||||||
|
// var negHandler,posHandler,footer = [ ];
|
||||||
|
// if (msg.negative) {
|
||||||
|
// negHandler = ()=>{
|
||||||
|
// msg.new = false;
|
||||||
|
// cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
// Bangle.messageResponse(msg,false);
|
||||||
|
// checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
||||||
|
// }; footer.push({type:"img",src:atob("PhAB4A8AAAAAAAPAfAMAAAAAD4PwHAAAAAA/H4DwAAAAAH78B8AAAAAA/+A/AAAAAAH/Af//////w/gP//////8P4D///////H/Af//////z/4D8AAAAAB+/AfAAAAAA/H4DwAAAAAPg/AcAAAAADwHwDAAAAAA4A8AAAAAAAA=="),col:"#f00",cb:negHandler});
|
||||||
|
// }
|
||||||
|
// footer.push({fillx:1}); // push images to left/right
|
||||||
|
// if (msg.reply && reply) {
|
||||||
|
// posHandler = ()=>{
|
||||||
|
// replying = true;
|
||||||
|
// msg.new = false;
|
||||||
|
// cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
// reply.reply({msg: msg})
|
||||||
|
// .then(result => {
|
||||||
|
// Bluetooth.println(JSON.stringify(result));
|
||||||
|
// replying = false;
|
||||||
|
// layout.render();
|
||||||
|
// checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
||||||
|
// })
|
||||||
|
// .catch(() => {
|
||||||
|
// replying = false;
|
||||||
|
// layout.render();
|
||||||
|
// showMessage(msg.id);
|
||||||
|
// });
|
||||||
|
// }; footer.push({type:"img",src:atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="),col:"#0f0", cb:posHandler});
|
||||||
|
// }
|
||||||
|
// else if (msg.positive) {
|
||||||
|
// posHandler = ()=>{
|
||||||
|
// msg.new = false;
|
||||||
|
// cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
// Bangle.messageResponse(msg,true);
|
||||||
|
// checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
||||||
|
// }; footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler});
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// layout = new Layout({ type:"v", c: [
|
||||||
|
// {type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [
|
||||||
|
// { type:"v", fillx:1, c: [
|
||||||
|
// {type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 },
|
||||||
|
// title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{},
|
||||||
|
// ]},
|
||||||
|
// { type:"btn",
|
||||||
|
// src:require("messageicons").getImage(msg),
|
||||||
|
// col:require("messageicons").getColor(msg, {settings:settings, default:g.theme.fg2}),
|
||||||
|
// pad: 3, cb:()=>{
|
||||||
|
// cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
// showMessageSettings(msg);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ]},
|
||||||
|
// {type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{
|
||||||
|
// // allow tapping to show a larger version
|
||||||
|
// showMessageScroller(msg);
|
||||||
|
// } },
|
||||||
|
// {type:"h",fillx:1, c: footer}
|
||||||
|
// ]},{back:goBack});
|
||||||
|
//
|
||||||
|
// Bangle.swipeHandler = (lr,ud) => {
|
||||||
|
// if (lr>0 && posHandler) posHandler();
|
||||||
|
// if (lr<0 && negHandler) negHandler();
|
||||||
|
// if (ud>0 && idx<MESSAGES.length-1) showMessage(MESSAGES[idx+1].id, true);
|
||||||
|
// if (ud<0 && idx>0) showMessage(MESSAGES[idx-1].id, true);
|
||||||
|
// };
|
||||||
|
// Bangle.on("swipe", Bangle.swipeHandler);
|
||||||
|
// g.reset().clearRect(Bangle.appRect);
|
||||||
|
// layout.render();
|
||||||
|
|
||||||
|
showMessageScroller(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* options = {
|
||||||
|
clockIfNoMsg : bool
|
||||||
|
clockIfAllRead : bool
|
||||||
|
showMsgIfUnread : bool
|
||||||
|
openMusic : bool // open music if it's playing
|
||||||
|
dontStopBuzz : bool // don't stuf buzzing (any time other than the first this is undefined/false)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function checkMessages(options) {
|
||||||
|
options=options||{};
|
||||||
|
// If there's been some user interaction, it's time to stop repeated buzzing
|
||||||
|
if (!options.dontStopBuzz)
|
||||||
|
require("messages").stopBuzz();
|
||||||
|
// If no messages, just show 'no messages' and return
|
||||||
|
if (!MESSAGES.length) {
|
||||||
|
active=undefined; // no messages
|
||||||
|
if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{
|
||||||
|
title:/*LANG*/"Messages",
|
||||||
|
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
|
||||||
|
buttons : {/*LANG*/"Ok":1},
|
||||||
|
back: () => load()
|
||||||
|
}).then(() => load());
|
||||||
|
return load();
|
||||||
|
}
|
||||||
|
// we have >0 messages
|
||||||
|
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
|
||||||
|
// If we have a new message, show it
|
||||||
|
if (options.showMsgIfUnread && newMessages.length) {
|
||||||
|
delete newMessages[0].show; // stop us getting stuck here if we're called a second time
|
||||||
|
showMessage(newMessages[0].id, false);
|
||||||
|
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
|
||||||
|
if (global.BUZZ_ON_NEW_MESSAGE) {
|
||||||
|
// this is set if we entered the messages app by loading `messagegui.new.js`
|
||||||
|
// ... but only buzz the first time we view a new message
|
||||||
|
global.BUZZ_ON_NEW_MESSAGE = false;
|
||||||
|
// messages.buzz respects quiet mode - no need to check here
|
||||||
|
require("messages").buzz(newMessages[0].src);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// no new messages: show playing music? Only if we have playing music, or state=="show" (set by messagesmusic)
|
||||||
|
if (options.openMusic && MESSAGES.some(m=>m.id=="music" && ((m.track && m.state=="play") || m.state=="show")))
|
||||||
|
return showMessage('music', true);
|
||||||
|
// no new messages - go to clock?
|
||||||
|
if (options.clockIfAllRead && newMessages.length==0)
|
||||||
|
return load();
|
||||||
|
active = "list";
|
||||||
|
// Otherwise show a list of messages
|
||||||
|
E.showScroller({
|
||||||
|
h : 48,
|
||||||
|
c : Math.max(MESSAGES.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11)
|
||||||
|
draw : function(idx, r) {"ram"
|
||||||
|
var msg = MESSAGES[idx];
|
||||||
|
if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH);
|
||||||
|
else g.setBgColor(g.theme.bg).setColor(g.theme.fg);
|
||||||
|
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||||
|
if (!msg) return;
|
||||||
|
var x = r.x+2, title = msg.title, body = msg.body;
|
||||||
|
var img = require("messageicons").getImage(msg);
|
||||||
|
if (msg.id=="music") {
|
||||||
|
title = msg.artist || /*LANG*/"Music";
|
||||||
|
body = msg.track;
|
||||||
|
}
|
||||||
|
if (img) {
|
||||||
|
var fg = g.getColor(),
|
||||||
|
col = require("messageicons").getColor(msg, {settings:settings, default:fg});
|
||||||
|
g.setColor(col).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
|
||||||
|
.setColor(fg); // only color the icon
|
||||||
|
x += 50;
|
||||||
|
}
|
||||||
|
if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2);
|
||||||
|
var longBody = false;
|
||||||
|
if (body) {
|
||||||
|
g.setFontAlign(-1,-1).setFont(fontSmall);
|
||||||
|
// if the body includes an image, it probably won't be small enough to allow>1 line
|
||||||
|
let maxLines = Math.floor(34/g.getFontHeight()), pady = 0;
|
||||||
|
if (body.includes("\0")) { maxLines=1; pady=4; }
|
||||||
|
var l = g.wrapString(body, r.w-(x+14));
|
||||||
|
if (l.length>maxLines) {
|
||||||
|
l = l.slice(0,maxLines);
|
||||||
|
l[l.length-1]+="...";
|
||||||
|
}
|
||||||
|
longBody = l.length>2;
|
||||||
|
// draw the body
|
||||||
|
g.drawString(l.join("\n"), x+10,r.y+20+pady);
|
||||||
|
}
|
||||||
|
if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2);
|
||||||
|
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
|
||||||
|
},
|
||||||
|
select : idx => {
|
||||||
|
if (idx < MESSAGES.length)
|
||||||
|
showMessage(MESSAGES[idx].id, true);
|
||||||
|
},
|
||||||
|
back : () => load()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function cancelReloadTimeout() {
|
||||||
|
if (!unreadTimeout) return;
|
||||||
|
clearTimeout(unreadTimeout);
|
||||||
|
unreadTimeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetReloadTimeout(){
|
||||||
|
cancelReloadTimeout();
|
||||||
|
if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60;
|
||||||
|
if (settings.unreadTimeout)
|
||||||
|
unreadTimeout = setTimeout(load, settings.unreadTimeout*1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
require("messages").toggleWidget(false);
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// only openMusic on launch if music is new, or state=="show" (set by messagesmusic)
|
||||||
|
var musicMsg = MESSAGES.find(m => m.id === "music");
|
||||||
|
checkMessages({
|
||||||
|
clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1,
|
||||||
|
openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show"),
|
||||||
|
dontStopBuzz: 1 });
|
||||||
|
}, 10); // if checkMessages wants to 'load', do that
|
||||||
|
|
||||||
|
/* If the Bangle is unlocked by the user, treat that
|
||||||
|
as a queue to stop repeated buzzing */
|
||||||
|
Bangle.on('lock',locked => {
|
||||||
|
if (!locked)
|
||||||
|
require("messages").stopBuzz();
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user