Compare commits

..

102 Commits

Author SHA1 Message Date
tanner 0ea5212778 test: Add tests for leading zero visual input handling
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 5dd0272350 fix: Prevent multiple leading zeros in number input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 8de4d70c8b test: Add chaos tests for number input, decimals, and leading zeros
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 7a3e5cd868 fix: Prevent multiple decimal points in number input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 8ed03143bd fix: Set haptic feedback duration for button presses to 30ms 2026-06-13 16:13:48 -06:00
tanner 95cc91781d feat: Add haptic feedback for number, decimal, and backspace
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 1a2fa25ea7 fix: Use buttonPress() for trig functions in tests
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner df0a2c03fc test: Expect 0 for sin/tan(PI) results due to float correction
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner d2c4be15d0 test: Expand Pi button test coverage
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3937109edb feat: Replace percent button with Pi button, update logic and tests
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 98fdddac5d fix: Correct negative zero logic for +/- and update test
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 73cd819e94 fix: Convert scientific notation to decimal for test input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner abe931d61d style: Improve readability of edge case numbers in tests 2026-06-13 16:13:48 -06:00
tanner 0bf1220c62 test: Add extensive edge case tests and expand number generation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner e32b274e5c fix: Update checkDisplay logic and backspace/trig test assertions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2088081bb8 test: Improve output validation by spying on raw display numbers
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2754d4f52a test: Improve display truncation handling for large numbers
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 04efe1b4d9 fix: Adjust test stringWidth mock to prevent truncation issues
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 728687ab37 fix: Adjust test stringWidth mock for display precision
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 77288c92af fix: Prevent displayOutput from clearing state on Infinity/NaN
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner e8a361210d test: Correct infinity string comparison in checkDisplay
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 255984e52f fix: Apply negation to current number when new number is pressed
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 20ce84ed89 test: Replace static tests with comprehensive programmatic test suite
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 31531ff118 fix: Increase test tolerance for float comparisons
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 37867a095b fix: Improve display comparison for floating point and truncation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 83a50fa06a Revert to calc before tests 2026-06-13 16:13:48 -06:00
tanner ab8f7e78b8 fix: Round floats for improved precision in fixFloat
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6fc30311f3 fix: Correct process mock and use buttonPress for multi-char commands
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a59b92572b fix: Reset angle mode on AC and parse float for square root
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2c91af6759 fix: Parse numeric input for scientific ops and expose operators to tests
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6ae1b173bd feat: Update logging endpoint and add buzz on success 2026-06-13 16:13:48 -06:00
tanner f94d5c2105 fix: Capture correct display output in g.drawString mock
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 913156ca3c fix: Expose buttonPress function for testing
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a5adeb379c fix: Mock FontDylex7x13 module in Node.js test runner
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 281187fd91 test: Add full simulation tests for calculator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner f5abf6eb48 test: Add calculator tests 2026-06-13 16:13:48 -06:00
tanner 810e43bcfd feat: Add HTTP POST for calculation logs with error display
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 1c511d3441 fix: Correct expression display on repeated '=' press
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3b66810ba1 feat: Add send function to log calculation history and result
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner ebb1550fc7 fix: Relax scientific notation display trigger
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6b7285a017 refactor: Remove unnecessary number display formatting 2026-06-13 16:13:48 -06:00
tanner 85185dbbaa fix: Replace regex with manual thousands separator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 5cb5ee604b fix: Ensure thousands separators display without premature scientific notation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner e57e09eb17 style: Use comma as thousands separator 2026-06-13 16:13:48 -06:00
tanner f3fdf4e5ab feat: Dynamically adjust result display width based on font
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2106a4298e refactor: Use Dylex7x13 font for calculator UI 2026-06-13 16:13:48 -06:00
tanner ca2265e8aa feat: Add thousands separator to calculator output
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner b86f6720bc fix: Correctly parse leading zeros for decimal input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 8c7c488967 refactor: Simplify math operations, remove custom precision functions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3507d8938e fix: Parse scientific notation in getIntWithPrecision
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 9738c76140 fix: Parse numbers as decimal in getIntWithPrecision
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner f19a77d06f fix: Dynamically calculate precision for scientific notation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 739b337135 fix: Adjust precision for scientific notation display 2026-06-13 16:13:48 -06:00
tanner 68aa187f71 fix: Prevent extra space in scientific notation exponent
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner b369cb0c3b fix: Fix Math.log10 not found error using Math.log equivalent
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 212cbdb9b7 fix: Implement toExponential polyfill for broader compatibility
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a714c57b2b fix: Display small and large numbers in scientific notation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6ac882699a fix: Correct floating point precision for trig functions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 9b48f7342a fix: Set default angle mode to degrees and simplify error messages 2026-06-13 16:13:48 -06:00
tanner 8b5ac36284 feat: Add angle mode toggle and apply to trig functions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3c18e0326d feat: Add scientific operators page (trig, log, exp)
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2400ebd145 style: Set button text color to black
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 22e36c447d Use load() instead of Bangle.load() for calculator
see: https://www.espruino.com/ReferenceBANGLEJS2#l_Bangle_load
2026-06-13 16:13:48 -06:00
tanner 37de23ecfe feat: Launch calculator app via menu command
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2a9f8ce857 feat: Add 1/x inversion function to operators menu
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a113e10ae5 fix: Prevent leading zero when backspacing a decimal
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 197d393553 chore: Update square root operator display to 'sqrt' 2026-06-13 16:13:48 -06:00
tanner 8fd4b6e45a feat: Add square and square root functions to calculator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 36120b877f feat: Add backspace button to keypad
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 69330e03cb feat: Add calculator app 2026-06-13 16:13:48 -06:00
Tanner 4c506c7913 Make weather or temp data grey if it's old 2026-06-13 11:24:21 -06:00
tanner 8275abdb57 Swap stop watch positions 2026-03-12 12:58:52 -06:00
tanner 1c433fc56b Display both feels like and outdoor temperatures 2026-03-12 12:52:51 -06:00
tanner 14bda5c29f Use clearInterval to clear the stopwatch interval 2026-03-10 16:58:57 -06:00
tanner ea58f2101f Increase buzz duration 2026-03-10 16:54:44 -06:00
tanner 07bb582ddd feat: Initialize buzz state for stopwatches
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-10 16:22:44 -06:00
tanner 6d965dd016 feat: Add timed buzzing alerts to stopwatches
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-10 16:22:18 -06:00
tanner 04511d029c Disable submenu logging 2026-03-09 14:15:18 -06:00
tanner d2d89b361e fix: Load default stopwatch data if file is missing
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-09 14:12:12 -06:00
tanner 0b4d8d84c4 refactor: Improve stopwatch management and display 2026-03-09 14:12:06 -06:00
tanner dc44ac9910 fix: Ensure stopwatches display when paused and on app load
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-09 13:40:19 -06:00
tanner f96bddf0ba feat: Implement pause and stop functions for stopwatches
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-09 13:29:58 -06:00
tanner dda3130c56 Begin writing stop watch 2026-03-09 13:26:41 -06:00
tanner 9091d7a783 Remove old grid code 2026-03-09 12:02:39 -06:00
tanner 4a4be26977 Remove Anton font, unindent 1 level 2026-03-09 11:55:37 -06:00
tanner b4c6f1b0c8 Add UTC time to watch face 2026-02-25 19:44:58 -07:00
tanner 0f343e753f Dont cancel message timeout 2024-09-10 15:42:51 -06:00
tanner 6bd1f2f05b Add pull menu command, token auth to bangle requests 2024-09-09 13:59:01 -06:00
tanner fc8894015a Replace messagegui.app.js with updated app.js 2024-09-09 12:42:52 -06:00
tanner 0ba99a590a Apply my changes to messagegui app:
- skip the entire message display UI, go directly to
  showMessageScroller()
- modify message scroller font and alignment
- remove extra lines and < Back button
- make tapping the header open message settings
2024-09-09 12:38:17 -06:00
tanner 6167de7456 Add new messagegui app.js, indent 2024-09-09 12:16:55 -06:00
tanner eccca550ef Add quad menu, ignore more messages 2024-09-07 11:49:51 -06:00
tanner d5158eee54 Add 2x2 menu 2024-03-03 21:51:30 -07:00
tanner a8e2c2e990 Optimize message display 2023-06-07 11:18:59 -06:00
tanner 87f93e10d1 Make top edge repeat grid selection 2023-06-06 12:09:02 -06:00
tanner 9edab1f230 Move rank to middle, track edge direction 2023-06-06 12:01:00 -06:00
tanner f15fd9cf20 Fetch grid on load 2023-06-05 19:16:29 -06:00
tanner c5b9343c9a Begin prototype of grid 2023-06-05 18:37:47 -06:00
tanner 98dd426e8a Remove message buttons, make font bigger 2023-04-08 18:44:21 -06:00
tanner 0073735647 Filter out !chat commands 2023-03-22 10:58:36 -06:00
tanner f839c18eee Rename files 2023-02-24 15:29:34 -07:00
tanner 13d9596978 Delete all new Timer messages 2023-02-23 18:30:27 -07:00
7 changed files with 2466 additions and 564 deletions
+1
View File
@@ -0,0 +1 @@
.aider*
+456
View File
@@ -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);
}
+736
View File
@@ -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);
+669
View File
@@ -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();
-465
View File
@@ -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
+604
View File
@@ -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