diff --git a/CHANGELOG.md b/CHANGELOG.md index c243093c6..86fe0c10c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,3 +29,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * Added progress bar on Bangle.js for uploads * Provide a proper error message in case JSON decode fails * Check you're connecting with a Bangle.js of the correct version +* Allow 'data' style app files to be uploaded with the app (and switch over settings files for various apps) diff --git a/README.md b/README.md index 294b9cb75..e71d7eee0 100644 --- a/README.md +++ b/README.md @@ -249,14 +249,20 @@ and which gives information about the app for the Launcher. {"name":"appid.js", // filename to use in storage. // If name=='RAM', the code is sent directly to Bangle.js and is not saved to a file "url":"", // URL of file to load (currently relative to apps/) - "content":"..." // if supplied, this content is loaded directly - "evaluate":true // if supplied, data isn't quoted into a String before upload + "content":"...", // if supplied, this content is loaded directly + "evaluate":true, // if supplied, data isn't quoted into a String before upload // (eg it's evaluated as JS) + "noOverwrite":true // if supplied, this file will not be overwritten if it + // already exists }, ] "data": [ // list of files the app writes to {"name":"appid.data.json", // filename used in storage "storageFile":true // if supplied, file is treated as storageFile + "url":"", // if supplied URL of file to load (currently relative to apps/) + "content":"...", // if supplied, this content is loaded directly + "evaluate":true, // if supplied, data isn't quoted into a String before upload + // (eg it's evaluated as JS) }, {"wildcard":"appid.data.*" // wildcard of filenames used in storage }, // this is mutually exclusive with using "name" diff --git a/apps.json b/apps.json index b92e08f30..234d5b616 100644 --- a/apps.json +++ b/apps.json @@ -80,7 +80,7 @@ "name": "Notifications (default)", "shortName":"Notifications", "icon": "notify.png", - "version":"0.07", + "version":"0.08", "description": "A handler for displaying notifications that displays them in a bar at the top of the screen", "tags": "widget", "type": "notify", @@ -93,7 +93,7 @@ "name": "Fullscreen Notifications", "shortName":"Notifications", "icon": "notify.png", - "version":"0.07", + "version":"0.08", "description": "A handler for displaying notifications that displays them fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notifications library.", "tags": "widget", "type": "notify", @@ -139,7 +139,7 @@ { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.21", + "version":"0.22", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "readme": "README.md", @@ -171,7 +171,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.23", + "version":"0.24", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "readme": "README.md", @@ -180,13 +180,16 @@ {"name":"setting.boot.js","url":"boot.js"}, {"name":"setting.img","url":"settings-icon.js","evaluate":true} ], + "data": [ + {"name":"setting.json", "url":"settings.min.json","evaluate":true} + ], "sortorder" : -2 }, { "id": "alarm", "name": "Default Alarm", "shortName":"Alarms", "icon": "app.png", - "version":"0.10", + "version":"0.11", "description": "Set and respond to alarms", "tags": "tool,alarm,widget", "storage": [ @@ -216,24 +219,33 @@ { "id": "slidingtext", "name": "Sliding Clock", "icon": "slidingtext.png", - "version":"0.01", + "version":"0.02", "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently only English, French and Japanese are supported", "tags": "clock", "type":"clock", "allow_emulator":true, + "readme": "README.md", + "custom":"custom.html", "storage": [ {"name":"slidingtext.app.js","url":"slidingtext.js"}, - {"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true} + {"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true}, + {"name":"slidingtext.locale.en.js","url":"slidingtext.locale.en.js"}, + {"name":"slidingtext.locale.en2.js","url":"slidingtext.locale.en2.js"}, + {"name":"slidingtext.utils.en.js","url":"slidingtext.utils.en.js"}, + {"name":"slidingtext.locale.fr.js","url":"slidingtext.locale.fr.js"}, + {"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"}, + {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} ] }, { "id": "sweepclock", "name": "Sweep Clock", "icon": "sweepclock.png", - "version":"0.01", - "description": "Smooth sweep secondhand with single hour numeral. Use button1 to toggle the numeral font", + "version":"0.02", + "description": "Smooth sweep secondhand with single hour numeral. Use button1 to toggle the numeral font and button3 to change the colour theme", "tags": "clock", "type":"clock", "allow_emulator":true, + "readme": "README.md", "storage": [ {"name":"sweepclock.app.js","url":"sweepclock.js"}, {"name":"sweepclock.img","url":"sweepclock-icon.js","evaluate":true} @@ -413,7 +425,7 @@ { "id": "gpsrec", "name": "GPS Recorder", "icon": "app.png", - "version":"0.18", + "version":"0.19", "interface": "interface.html", "description": "Application that allows you to record a GPS track. Can run in background", "tags": "tool,outdoors,gps,widget", @@ -438,14 +450,16 @@ "interface":"waypoints.html", "storage": [ {"name":"gpsnav.app.js","url":"app.min.js"}, - {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"} ] }, { "id": "heart", "name": "Heart Rate Recorder", "icon": "app.png", - "version":"0.02", + "version":"0.04", "interface": "interface.html", "description": "Application that allows you to record your heart rate. Can run in background", "tags": "tool,health,widget", @@ -558,7 +572,7 @@ "shortName": "Battery Warning", "icon": "widget.png", "readme": "README.md", - "version":"0.01", + "version":"0.02", "description": "Show a warning when the battery runs low.", "tags": "tool,battery", "type":"widget", @@ -738,7 +752,7 @@ { "id": "route", "name": "Route Viewer", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Upload a KML file of a route, and have your watch display a map with how far around it you are", "tags": "", "custom": "custom.html", @@ -1072,7 +1086,7 @@ { "id": "widpedom", "name": "Pedometer widget", "icon": "widget.png", - "version":"0.11", + "version":"0.12", "description": "Daily pedometer widget", "tags": "widget", "type":"widget", @@ -1203,7 +1217,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.14", + "version":"0.15", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", @@ -1701,9 +1715,9 @@ { "id": "rclock", "name": "Round clock with seconds, minutes and date", - "shortName":"Round Clock", + "shortName": "Round Clock", "icon": "app.png", - "version":"0.04", + "version": "0.05", "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", "tags": "clock", "type": "clock", @@ -1712,6 +1726,20 @@ {"name":"rclock.img","url":"app-icon.js","evaluate":true} ] }, + { + "id": "fclock", + "name": "fclock", + "shortName": "F Clock", + "icon": "app.png", + "version": "0.01", + "description": "Simple design of a digital clock", + "tags": "clock", + "type": "clock", + "storage": [ + {"name":"fclock.app.js","url":"fclock.app.js"}, + {"name":"fclock.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "hamloc", "name": "QTH Locator / Maidenhead Locator System", "shortName": "QTH Locator", @@ -2100,7 +2128,7 @@ "name": "SleepPhaseAlarm", "shortName":"SleepPhaseAlarm", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", "tags": "alarm", "storage": [ @@ -2225,7 +2253,7 @@ "name": "Apple Notification Widget", "shortName":"ANCS Widget", "icon": "widget.png", - "version":"0.06", + "version":"0.07", "description": "Displays call, message etc notifications from a paired iPhone. Read README before installation as it only works with compatible apps", "readme": "README.md", "tags": "widget", @@ -2399,9 +2427,11 @@ "readme": "README.md", "storage": [ {"name":"worldclock.app.js","url":"app.js"}, - {"name":"worldclock.settings.json"}, {"name":"worldclock.img","url":"worldclock-icon.js","evaluate":true} - ] + ], + "data": [ + {"name":"worldclock.settings.json"} + ] }, { "id": "digiclock", "name": "Digital Clock Face", @@ -2593,7 +2623,7 @@ "name": "Hard Alarm", "shortName":"HardAlarm", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Make sure you wake up! Count to the right number to turn off the alarm", "tags": "tool,alarm,widget", "storage": [ @@ -2644,8 +2674,10 @@ "readme": "README.md", "storage": [ {"name":"breath.app.js","url":"app.js"}, - {"name":"breath.settings.json","url":"settings.json"}, {"name":"breath.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"breath.settings.json","url":"settings.json"} ] }, { "id": "lazyclock", @@ -2742,9 +2774,11 @@ "storage": [ {"name":"gpsservice.app.js","url":"app.js"}, {"name":"gpsservice.settings.js","url":"settings.js"}, - {"name":"gpsservice.settings.json","url":"settings.json"}, {"name":"gpsservice.wid.js","url":"widget.js"}, {"name":"gpsservice.img","url":"gpsservice-icon.js","evaluate":true} + ], + "data": [ + {"name":"gpsservice.settings.json","url":"settings.json"} ] }, { "id": "mclockplus", @@ -2841,16 +2875,18 @@ "storage": [ {"name":"gpssetup","url":"gpssetup.js"}, {"name":"gpssetup.settings.js","url":"settings.js"}, - {"name":"gpssetup.settings.json","url":"settings.json"}, {"name":"gpssetup.app.js","url":"app.js"}, {"name":"gpssetup.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"gpssetup.settings.json","url":"settings.json"} ] }, { "id": "walkersclock", "name": "Walkers Clock", "shortName":"Walkers Clock", "icon": "walkersclock48.png", - "version":"0.03", + "version":"0.04", "description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference", "type":"clock", "tags": "clock, gps, tools, outdoors", @@ -2944,8 +2980,10 @@ "interface":"waypoints.html", "storage": [ {"name":"waypointer.app.js","url":"app.js"}, - {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, {"name":"waypointer.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"} ] }, { "id": "color_catalog", @@ -3004,7 +3042,7 @@ "name": "Gadgetbridge Music Controls", "shortName":"Music Controls", "icon": "icon.png", - "version":"0.01", + "version":"0.02", "description": "Control the music on your Gadgetbridge-connected phone", "tags": "tools,bluetooth,gadgetbridge,music", "type":"app", @@ -3045,7 +3083,7 @@ { "id": "kitchen", "name": "Kitchen Combo", "icon": "kitchen.png", - "version":"0.02", + "version":"0.03", "description": "Combination of the stepo, walkersclock, arrow and waypointer apps into a multiclock format. 'Everything but the kitchen sink'. Requires firmware v2.08.167 or later", "tags": "tool,outdoors,gps", "readme": "README.md", @@ -3059,5 +3097,38 @@ {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} ] -} +}, +{ "id": "qmsched", + "name": "Quiet Mode Schedule", + "shortName":"Quiet Mode", + "icon": "app.png", + "version":"0.01", + "description": "Automatically turn Quiet Mode on or off at set times", + "readme": "README.md", + "tags": "tool", + "storage": [ + {"name":"qmsched","url":"lib.js"}, + {"name":"qmsched.app.js","url":"app.js"}, + {"name":"qmsched.boot.js","url":"boot.js"}, + {"name":"qmsched.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"qmsched.json"} + ] +}, +{ + "id": "hourstrike", + "name": "Hour Strike", + "shortName": "Hour Strike", + "icon": "app-icon.png", + "version": "0.07", + "description": "Strike the clock on the hour. A great tool to remind you an hour has passed!", + "tags": "tool,alarm", + "readme": "README.md", + "storage": [ + {"name":"hourstrike.app.js","url":"app.js"}, + {"name":"hourstrike.boot.js","url":"boot.js"}, + {"name":"hourstrike.img","url":"app-icon.js","evaluate":true} + ] +} ] diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 23b8ee562..96e1490ab 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -8,3 +8,4 @@ 0.08: Make alarm scheduling more reliable 0.09: Add per alarm auto-snooze option 0.10: Fix auto-snooze option (this stopped new alarms being added) (fix #506) +0.11: Respect Quiet Mode diff --git a/apps/alarm/alarm.js b/apps/alarm/alarm.js index 28261110a..26345e887 100644 --- a/apps/alarm/alarm.js +++ b/apps/alarm/alarm.js @@ -38,6 +38,7 @@ function showAlarm(alarm) { load(); }); function buzz() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence Bangle.buzz(100).then(()=>{ setTimeout(()=>{ Bangle.buzz(100).then(function() { diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog new file mode 100644 index 000000000..a8f708a0a --- /dev/null +++ b/apps/fclock/ChangeLog @@ -0,0 +1 @@ +0.01: First published version of app diff --git a/apps/fclock/app-icon.js b/apps/fclock/app-icon.js new file mode 100644 index 000000000..ba506d3ac --- /dev/null +++ b/apps/fclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("7OwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAAAB//AAAAAAAAAAAAAAAAAAAAAAAAAAD//AAAAAAAB//8AAAAAAAAAAAAAAAAAAAAAAAAAD//wAAAAAAB///wAAAAAAAAAAAAAAAAAAAAAAAAB//8AAAAAAB////AAAAAAAAAAAAAAAAAAAAAAAAB///AAAAAAB////8AAAAAAAAAAAAAAAAAAAAAAAA///wAAAAAB/////wAAAAAAAAAAAAAAAAAAAAAAA///8AAAAAB//////AAAAAAAAAAAAAAAAAAAAAAAf///AAAAAA//////4AAAAAAAAAAAAAAAAAAAAAAf///wAAAAAP/////+AAAAAAAAAAAAAAAAAAAAAAP///8AAAAAH//////wAAAAAAAAAAAAAAAAAAAAAP////AAAAAB//////8AAAAAAAAAAAAAAAAAAAAAH////wAAAAA///////gAAAAAAAAAAAAAAAAAAAAH////8AAAAAP//////4AAAAAAAAAAAAAAAAAAAAB//9//AAAAAH//AAH//AAAAAAAAAAAAAAAAAAAAAf/+f/wAAAAB//gAA//wAAAAAAAAAAAAAAAAAAAAH/+H/8AAAAA//wAAH/+AAAAAAAAAAAAAAAAAAAAB//B//AAAAAP/4AAA//gAAAAAAAAAAAAAAAAAAAAf/Af/wAAAAH/8AAAH/8AAAAAAAAAAAAAAAAAAAAH/gH/8AAAAB/+AAAA//AAAAAAAAAAAAAAAAAAAAB/gB//AAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAfwAf/wAAAAH/4AAAAAAAAAAAAAAAAAAAAAAAAAAHwAH/8AAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAB4AB//AAAAAf/gAAAAAAAAP4AAAAD8AAAAAAAAAAYAAf/wAAAAH/4AAAAAAAAH+AAAAB/AAAAAAAAAAEAAH/8AAAAB/+AAAAAAAAH/gAAAAfwAAAAAAAAAAAAB//AAAAAf/gf/wAAAAD/4AAAAP8AAAAAAAAAAAAAf/wAAAAH/4f//AAAAD/+AAAAH/AAAAAAAAAAAAAH/8AAAAB/+f//8AAAB//gAAAD/wAAAAAAAAAAAAB//AAAAAf/v///wAAA/34AAAA/8AAAAAAAAAAAAAf/wAAAAH/7////AAAP5+AAAAf/AAAAAAAAAAAAAH/8AAAAB/+////8AAD8fgAAAP/wAAAAAAAAAAAAB//AAAAAf/v////wAA8H4AAAD/8AAAAAAAAAAAAAf/wAAAAH/7////+AAOB+AAAB+/AAAAAAAAAAAAAH/8AAAAB/+/////gACAfgAAA/vwAAAAAAAAAAAAB//AAAAAf/v////8AAAH4AAAfz8AAPAB/AAAAAAAf/wAAAAH/7/////AAAB+AAAH4/AAPwA/8AAAAAAH/8AAAAB/+/////4AAAfgAAD8PwAH8AP/AAAAAAB//AAAAAf/v////+AAAH4AAB/D8AB3AHB4AAAAAAf/wAAAAH/7wAB//wAAB+AAAfg/AARwAAOAAAAAAH/8AAAAB/+4AAP/8AAAfgAAPwPwAAcAADgAAAAAB//AAAAAf/sAAB//gAAH4AAH4D8AAHAAB4AAAAAAf/wAAAAH/6AAAP/4AAB+AAD+A/AABwAH8AAAAAAH/8AAAAB/+AAAB//AAAfgAA/APwAAcAB/AAAAAAB//AAAAAf/gAAAP/wAAH4AAfgD8AAHAAfwAAAAAAf/wAAAAH/4AAAD/8AAB+AAH///4ABwAA8AAAAAAH/8AAAAB/+AAAA//AAAfgAB///+AAcAAHgAAAAAB//AAAAAf/gAAAP/wAAH4AAf///gAHAAA4AAAAAAf/wAAAAH/4AAAD/8AAB+AAH///4ABwAAOAAAAAAH/8AAAAB/+AAAA//AAAfgAB///+AAcAcHAAAAAAB//AAAAAf/gAAAP/wAAH4AAf///gB/+D/wAAAAAAf/wAAAAH/4AAAD/8AAB+AAAAA/AAf/g/4AAAAAAH/8AAAAB/+AAAA//AAAfgAAAAPwAH/4D8AAAAAAB//AAAAAf/gAAAP/wAAH4AAAAD8AAAAAAAAAAAAAf/wAAAAH/4AAAD/8AP///wAAA/AAAAAAAAAAAAAH/8AAAAB/+AAAA//AD///8AAAPwAAAAAAAAAAAAB//AAAAAf/gAAAP/wA////AAAD8AAAAAAAAAAAAAf/wAAAAH/4AAAD/8AP///wAAA/AAAAAAAAAAAAAH/8AAAAB//AAAB//AD///8AAAPwAAAAAAAAAAAAB//AAAAAP/4AAA//gA////AAAD8AAAAAAAAAAAAAf/wAAAAD//AAAf/4AP///wAAA/AAAAAAAAAAAAAH/8AAAAAf/4AAP/8AAAAAAAAAAAAAAAAAAAAAAAB//AAAAAH//AAH//AAAAAAAAAAAAAAAAAAAAAAAAf/wAAAAA//4AD//gAAAAAAAAAAAAAAAAAAAAH///////8AAP//////4AAAAAAAAAAAAAAAAAAAAB////////AAB//////8AAAAAAAAAAAAAAAAAAAAAf///////wAAf//////AAAAAAAAAAAAAAAAAAAAAH///////8AAD//////gAAAAAAAAAAAAAAAAAAAAB////////AAA//////4AAAAAAAAAAAAAAAAAAAAAf///////wAAH/////8AAAAAAAAAAAAAAAAAAAAAH///////8AAA/////+AAAAAAAAAAAAAAAAAAAAAB////////AAAD////+AAAAAAAAAAAAAAAAAAAAAAf///////wAAAP///+AAAAAAAAAAAAAAAAAAAAAAH///////8AAAA///+AAAAAAAAAAAAAAAAAAAAAAB////////AAAAD//+AAAAAAAAAAAAAAAAAAAAAAAf///////wAAAAP/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8P4DDwDAMPB4PAwAAAAAAAAAAAAAAAAAAAAAAAAfj+Ax+BwDH4/H48AAAAAAAAAAAAAAAAAAAAAAAAGMgAYYwcBhjMZjLAAAAAAAAAAAAAAAAAAAAAAAABDIAGEMPAYAyGAwwAAAAAAAAAAAAAAAAAAAAAAAAQx8DBDGwMAYhgYMAAAAAAAAAAAAAAAAAAAAAAAAEM/AwQzMDAOIYODAAAAAAAAAAAAAAAAAAAAAAAABDAYYEM/hgHCGHAwAAAAAAAAAAAAAAAAAAAAAAAAQwGGBDP4YDAhjAMAAAAAAAAAAAAAAAAAAAAAAAAEMhjAQwMMBgIZgDAAAAAAAAAAAAAAAAAAAAAAAAB+PwwH4DDAfz8fz+AAAAAAAAAAAAAAAAAAAAAAAAPB4YA8AxgH8eH8/gAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")) \ No newline at end of file diff --git a/apps/fclock/app.png b/apps/fclock/app.png new file mode 100644 index 000000000..6255b756c Binary files /dev/null and b/apps/fclock/app.png differ diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js new file mode 100644 index 000000000..044cde71f --- /dev/null +++ b/apps/fclock/fclock.app.js @@ -0,0 +1,206 @@ +{ + var minutes; + var seconds; + var hours; + var date; + var first = true; + var locale = require('locale'); + var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; + + //HR variables + var id = 0; + var grow = true; + var size=10; + + //Screen dimensions + const screen = { + width: g.getWidth(), + height: g.getWidth(), + middle: g.getWidth() / 2, + center: g.getHeight() / 2, + }; + + // Ssettings + const settings = { + time: { + color: '#dddddd', + font: 'Vector', + size: 100, + middle: screen.middle, + center: screen.center, + }, + date: { + color: '#dddddd', + font: 'Vector', + size: 15, + middle: screen.height-17, // at bottom of screen + center: screen.center, + }, + circle: { + colormin: '#ffffff', + colorsec: '#ffffff', + width: 10, + middle: screen.middle, + center: screen.center, + height: screen.height + }, + hr: { + color: '#333333', + size: 20, + x: screen.center, + y: screen.middle + 65 + } + }; + + const dateStr = function (date) { + return locale.date(new Date(), 1); + }; + + const getFormated = function(val) { + if (val<10) { + val='0'+val; + } + + return val; + }; + + const drawMin = function (sections, color) { + + g.setFontAlign(0, 0, 0); + g.setColor('#000000'); + g.setFont(settings.time.font, settings.time.size/2); + g.drawString(getFormated(sections-1), settings.time.center+50, settings.time.middle); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size/2); + g.drawString(getFormated(sections), settings.time.center+50, settings.time.middle); + }; + + const drawSec = function (sections, color) { + g.setFontAlign(0, 0, 0); + g.setColor('#000000'); + g.setFont(settings.time.font, settings.time.size/4); + g.drawString(getFormated(sections-1), settings.time.center+100, settings.time.middle); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size/4); + g.drawString(getFormated(sections), settings.time.center+100, settings.time.middle); + }; + + const drawClock = function () { + + currentTime = new Date(); + + //Get date as a string + date = dateStr(currentTime); + + if(seconds==59) { + g.clear(); + } + + // Update minutes when needed + if (minutes != currentTime.getMinutes()) { + minutes = currentTime.getMinutes(); + drawMin(minutes, settings.circle.colormin); + } + + //Update seconds when needed + if (seconds != currentTime.getSeconds()) { + seconds = currentTime.getSeconds(); + drawSec(seconds, settings.circle.colorsec); + } + + //Write the time as configured in the settings + hours = currentTime.getHours(); + if (_12hour && hours > 13) { + hours = hours - 12; + } + + var meridian; + + if (typeof locale.meridian === "function") { + meridian = locale.meridian(new Date()); + } else { + meridian = ""; + } + + var timestr; + + if (meridian.length > 0 && _12hour) { + timestr = hours + " " + meridian; + } else { + timestr = hours; + } + g.setFontAlign(0, 0, 0); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size); + g.drawString(timestr, settings.time.center-40, settings.time.middle); + + //Write the date as configured in the settings + g.setColor(settings.date.color); + g.setFont(settings.date.font, settings.date.size); + g.drawString(date, settings.date.center, settings.date.middle); + }; + + //setInterval for HR visualisation + const newBeats = function (hr) { + if (id != 0) { + changeInterval(id, 6e3 / hr.bpm); + } else { + id = setInterval(drawHR, 6e3 / hr.bpm); + } + }; + + //visualize HR with circles pulsating + const drawHR = function () { + if (grow && size < settings.hr.size) { + size++; + } + + if (!grow && size > 3) { + size--; + } + + if (size == settings.hr.size || size == 3) { + grow = !grow; + } + + if (grow) { + color = settings.hr.color; + g.setColor(color); + g.fillCircle(settings.hr.x, settings.hr.y, size); + } else { + color = "#000000"; + g.setColor(color); + g.drawCircle(settings.hr.x, settings.hr.y, size); + } + }; + + // clean app screen + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + //manage when things should be enabled and not + Bangle.on('lcdPower', function (on) { + if (on) { + Bangle.setHRMPower(1); + } else { + Bangle.setHRMPower(0); + } + }); + + // refesh every second + setInterval(drawClock, 1E3); + + //start HR monitor and update frequency of update + Bangle.setHRMPower(1); + Bangle.on('HRM', function (d) { + newBeats(d); + }); + + // draw now + drawClock(); + + // Show launcher when middle button pressed + setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +} \ No newline at end of file diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog index ec66c5568..4fa99c934 100644 --- a/apps/gbmusic/ChangeLog +++ b/apps/gbmusic/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version +0.02: Increase text brightness, improve controls, (try to) reduce memory usage \ No newline at end of file diff --git a/apps/gbmusic/README.md b/apps/gbmusic/README.md index acb5f5dfe..a5de044ed 100644 --- a/apps/gbmusic/README.md +++ b/apps/gbmusic/README.md @@ -23,9 +23,13 @@ You can change this under `Settings`->`App/Widget Settings`->`Music Controls`. ## Controls ### Buttons -* Button 1: Volume up (hold to repeat) -* Button 2: Toggle play/pause, long-press for menu -* Button 3: Volume down (hold to repeat, but remember that holding for too long resets your watch) +* Button 1: Volume up +* Button 2: + - Single press: toggle play/pause + - Double press: next song + - Triple press: previous song + - Long-press: open application launcher +* Button 3: Volume down ### Touch * Left: pause/previous song diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js index ab26c22ee..7cfbb574a 100644 --- a/apps/gbmusic/app.js +++ b/apps/gbmusic/app.js @@ -2,690 +2,620 @@ /** * Control the music on your Gadgetbridge-connected phone **/ -{ - let autoClose = false // only if opened automatically - let state = "" - let info = { - artist: "", - album: "", - track: "", - n: 0, - c: 0, - } +let auto = false; // auto close if opened automatically +let stat = ""; +let info = { + artist: "", + album: "", + track: "", + n: 0, + c: 0, +}; +const TOUT = 300000; // auto close timeout: 5 minutes (in ms) - const screen = { - width: g.getWidth(), - height: g.getHeight(), - center: g.getWidth()/2, - middle: g.getHeight()/2, - } +/////////////////////// +// Self-repeating timeouts +/////////////////////// - const TIMEOUT = 5*1000*60 // auto close timeout: 5 minutes - // drawText defaults - const defaults = { - time: { // top center - color: -1, - font: "Vector", - size: 24, - left: 10, - top: 30, - }, - date: { // bottom center - color: -1, - font: "Vector", - size: 16, - bottom: 26, - center: screen.width/2, - }, - num: { // top right - font: "Vector", - size: 30, - top: 30, - right: 15, - }, - track: { // center above middle - font: "Vector", - size: 40, // maximum size - min_size: 25, // scroll (at maximum size) if this doesn't fit - bottom: (screen.height/2)+10, - center: screen.width/2, - // Smaller interval+step might be smoother, but flickers :-( - interval: 200, // scroll interval in ms - step: 10, // scroll speed per interval - }, - artist: { // center below middle - font: "Vector", - size: 30, // maximum size - middle: (screen.height/2)+17, - center: screen.width/2, - }, - album: { // center below middle - font: "Vector", - size: 20, // maximum size - middle: (screen.height/2)+18, // moved down if artist is present - center: screen.width/2, - }, - // these work a bit different, as they apply to all controls - controls: { - color: "#008800", - highlight: 200, // highlight pressed controls for this long, ms - activeColor: "#ff0000", - size: 20, // icons - left: 10, // for right-side - right: 20, // for left-side (more space because of +- buttons) - top: 30, - bottom: 30, - font: "6x8", // volume buttons - volSize: 2, // volume buttons - }, +// Clock +let tock = -1; +function tick() { + if (!Bangle.isLCDOn()) { + return; } + const now = new Date; + if (now.getHours()*60+now.getMinutes()!==tock) { + drawDateTime(); + tock = now.getHours()*60+now.getMinutes(); + } + setTimeout(tick, 1000); // we only show minute precision anyway +} - class Ticker { - constructor(interval) { - this.i = null - this.interval = interval - this.active = false - } - clear() { - if (this.i) { - clearInterval(this.i) - } - this.i = null - } - start() { - this.active = true - this.resume() - } - stop() { - this.active = false - this.clear() - } - pause() { - this.clear() - } - resume() { - this.clear() - if (this.active && Bangle.isLCDOn()) { - this.tick() - this.i = setInterval(() => {this.tick()}, this.interval) - } - } +// Fade out while paused and auto closing +let fade = null; +function fadeOut() { + if (!Bangle.isLCDOn() || !fade) { + return; } + drawMusic(); + setTimeout(fadeOut, 500); +} +function brightness() { + if (!fade) { + return 1; + } + return Math.max(0, 1-((Date.now()-fade)/TOUT)); +} - /** - * Draw time and date - */ - class Clock extends Ticker { - constructor() { - super(1000) - } - tick() { - g.reset() - const now = new Date - drawText("time", this.text(now)) - drawText("date", require("locale").date(now, true)) - } - text(time) { - const l = require("locale") - const is12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] - if (!is12hour) { - return l.time(time, true) - } - const date12 = new Date(time.getTime()) - const hours = date12.getHours() - if (hours===0) { - date12.setHours(12) - } else if (hours>12) { - date12.setHours(hours-12) - } - return l.time(date12, true)+l.meridian(time) - } +// Scroll long track names +// use an interval to get smooth movement +let offset = null, // scroll Offset: null = no scrolling + iScroll; +function scroll() { + offset += 10; + drawScroller(); +} +function scrollStart() { + if (offset!==null) { + return; // already started } + offset = 0; + if (Bangle.isLCDOn()) { + if (!iScroll) { + iScroll = setInterval(scroll, 200); + } + drawScroller(); + } +} +function scrollStop() { + if (iScroll) { + clearInterval(iScroll); + iScroll = null; + } + offset = null; +} - /** - * Update all info every second while fading out - */ - class Fader extends Ticker { - constructor() { - super(defaults.track.interval) // redraw at same speed as scroller - } - tick() { - drawMusic() - } - start() { - this.since = Date.now() - super.start() - } - stop() { - super.stop() - this.since = Date.now() // force redraw at 100% brightness - drawMusic() - this.since = null - } - brightness() { - if (fadeOut.since) { - return Math.max(0, 1-((Date.now()-fadeOut.since)/TIMEOUT)) - } - return 1 - } +/** + * @param {string} text + * @return {number} Maximum font size to make text fit on screen + */ +function fitText(text) { + if (!text.length) { + return Infinity; } - - /** - * Scroll long track names - */ - class Scroller extends Ticker { - constructor() { - super(defaults.track.interval) - } - tick() { - this.offset += defaults.track.step - this.draw() - } - draw() { - const s = defaults.track - const sep = " " - g.setFont(s.font, s.size) - g.setColor(infoColor("track")) - const text = sep+info.track, - text2 = text.repeat(2), - w1 = g.stringWidth(text), - bottom = screen.height-s.bottom - this.offset = this.offset%w1 - g.setFontAlign(-1, 1) - g.clearRect(0, bottom-s.size, screen.width, bottom) - .drawString(text2, -this.offset, screen.height-s.bottom) - } - start() { - this.offset = 0 - super.start() - } - stop() { - super.stop() - const s = defaults.track, - bottom = screen.height-s.bottom - g.clearRect(0, bottom-s.size, screen.width, bottom) - } + // make a guess, then shrink/grow until it fits + const test = (s) => g.setFont("Vector", s).stringWidth(text); + let best = Math.floor(24000/test(100)); + if (test(best)===240) { // good guess! + return best; } - - function drawInfo(name, options) { - drawText(name, info[name], Object.assign({ - color: infoColor(name), - size: infoSize(name), - force: fadeOut.active, - }, options)) - } - let oldText = {} - function drawText(name, text, options) { - if (name in oldText && oldText[name].text===text && !(options || {}).force) { - return // nothing to do - } - const s = Object.assign( - // deep clone defaults to prevent them being overwritten with options - JSON.parse(JSON.stringify(defaults[name])), - options || {}, - ) - g.setColor(s.color) - g.setFont(s.font, s.size) - const ax = "left" in s ? -1 : ("right" in s ? 1 : 0), - ay = "top" in s ? -1 : ("bottom" in s ? 1 : 0) - g.setFontAlign(ax, ay) - // drawString coordinates - const x = "left" in s ? s.left : ("right" in s ? screen.width-s.right : s.center), - y = "top" in s ? s.top : ("bottom" in s ? screen.height-s.bottom : s.middle) - // bounding rectangle - const w = g.stringWidth(text), h = g.getFontHeight(), - left = "left" in s ? x : ("right" in s ? x-w : x-w/2), - top = "top" in s ? y : ("bottom" in s ? y-h : y-h/2) - if (name in oldText) { - const old = oldText[name] - // only clear if text/area has changed - if (old.text!==text - || old.left!==left || old.top!==top - || old.w!==w || old.h!==h) { - g.clearRect(old.left, old.top, old.left+old.w, old.top+old.h) - } - } - if (text.length) { - g.drawString(text, x, y) - // remember which rectangle to clear before next draw - oldText[name] = { - text: text, - left: left, top: top, - w: w, h: h, - } - } else { - delete oldText[name] - } - } - - /** - * - * @param text - * @return {number} Maximum font size to make text fit on screen - */ - function fitText(text) { - if (!text.length) { - return Infinity - } - // Vector: make a guess, then shrink/grow until it fits - const getWidth = (size) => g.setFont("Vector", size).stringWidth(text) - , sw = screen.width - let guess = Math.round(sw/(text.length*0.6)) - if (getWidth(guess)===sw) { // good guess! - return guess - } - if (getWidth(guess) target + if (test(best)<240) { do { - guess-- - } while(getWidth(guess)>sw) - return guess + best++; + } while(test(best)<=240); + return best-1; } + // width > 240 + do { + best--; + } while(test(best)>240); + return best; +} - /** - * @param name - * @return {number} Font size to use for given info - */ - function infoSize(name) { - if (name==="num") { // fixed size - return defaults[name].size - } - return Math.min( - defaults[name].size, - fitText(info[name]), - ) +/** + * @param {string} text + * @return {number} Randomish but deterministic number from 0-360 for text + */ +function textCode(text) { + "ram"; + let code = 0; + for(let i = 0; i code += c.charCodeAt(0)) - // dark magic - h = code%360 - s = 1 + return code%360; +} +// dark magic +function hsv2rgb(h, s, v) { + const f = (n) => { + const k = (n+h/60)%6; + return v-v*s*Math.max(Math.min(k, 4-k, 1), 0); + }; + return {r: f(5), g: f(3), b: f(1)}; +} +function f2hex(f) { + return ("00"+(Math.round(f*255)).toString(16)).substr(-2); +} +/** + * @param name + * @return {string} Semi-random color to use for given info + */ +function infoColor(name) { + let h, s, v; + if (name==="num") { + // always white + h = 0; + s = 0; + } else { + // make color depend deterministically on info + let code = 0; + switch(name) { + case "track": + code += textCode(info.track); + // fallthrough: also use album+artist + case "album": + code += textCode(info.album); + // fallthrough: also use artist + default: + code += textCode(info[name]); } - v = fadeOut.brightness() - const hsv2rgb = (h, s, v) => { - const f = (n) => { - const k = (n+h/60)%6 - return v-v*s*Math.max(Math.min(k, 4-k, 1), 0) - } - return {r: f(5), g: f(3), b: f(1)} - } - const rgb = hsv2rgb(h, s, v) - const f2hex = (f) => ("00"+(Math.round(f*255)).toString(16)).substr(-2) - const color = "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b) - infoColors[name] = color - return color + h = code%360; + s = 0.7; } + v = brightness(); + const rgb = hsv2rgb(h, s, v); + return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b); +} +/** + * Remember track color until info changes + * Because we need this every time we move the scroller + * @return {string} + */ +function trackColor() { + if (!("track_color" in info) || fade) { + info.track_color = infoColor("track"); + } + return info.track_color; +} - let lastTrack - function drawTrack() { - // we try if we can squeeze this in with a slightly smaller font, but if - // the title is too long we start up the scroller instead - const trackInfo = ([info.artist, info.album, info.n, info.track]).join("-") - if (trackInfo===lastTrack) { - return // already visible +//////////////////// +// Drawing functions +//////////////////// +/** + * Draw date and time + * @return {*} + */ +function drawDateTime() { + const now = new Date; + const l = require("locale"); + const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + let time; + if (is12) { + const d12 = new Date(now.getTime()); + const hour = d12.getHours(); + if (hour===0) { + d12.setHours(12); + } else if (hour>12) { + d12.setHours(hour-12); } - if (infoSize("track")0) { + num = "#"+info.n; + if ("c" in info && info.c>0) { // I've seen { c:-1 } + num += "/"+info.c; } - drawInfo("artist", { - size: artist_size, - }) - drawInfo("album", { - middle: album_middle, - }) } - const icons = { + g.reset(); + g.setFont("Vector", 30) + .setFontAlign(1, -1) // top right + .clearRect(225, 30, 120, 60) + .drawString(num, 225, 30); +} +/** + * Clear rectangle used by track title + */ +function clearTrack() { + g.clearRect(0, 60, 239, 119); +} +/** + * Draw track title + */ +function drawTrack() { + let size = fitText(info.track); + if (size<25) { + // the title is too long: start the scroller + scrollStart(); + return; + } else { + scrollStop(); + } + // stationary track + if (size>40) { + size = 40; + } + g.reset(); + g.setFont("Vector", size) + .setFontAlign(0, 1) // center bottom + .setColor(trackColor()); + clearTrack(); + g.drawString(info.track, 119, 109); +} +/** + * Draw scrolling track title + */ +function drawScroller() { + g.reset(); + g.setFont("Vector", 40); + const w = g.stringWidth(info.track)+40; + offset = offset%w; + g.setFontAlign(-1, 1) // left bottom + .setColor(trackColor()); + clearTrack(); + g.drawString(info.track, -offset+40, 109) + .drawString(info.track, -offset+40+w, 109); +} + +/** + * Draw track artist and album + */ +function drawArtistAlbum() { + // we just use small enough fonts to make these always fit + // calculate stuff before clear+redraw + const aCol = infoColor("artist"); + const bCol = infoColor("album"); + let aSiz = fitText(info.artist); + if (aSiz>30) { + aSiz = 30; + } + let bSiz = fitText(info.album); + if (bSiz>20) { + bSiz = 20; + } + g.reset(); + g.clearRect(0, 120, 240, 189); + let top = 124; + if (info.artist) { + g.setFont("Vector", aSiz) + .setFontAlign(0, -1) // center top + .setColor(aCol) + .drawString(info.artist, 119, top); + top += aSiz+4; // fit album neatly under artist + } + if (info.album) { + g.setFont("Vector", bSiz) + .setFontAlign(0, -1) // center top + .setColor(bCol) + .drawString(info.album, 119, top); + } +} + +/** + * + * @param {string} icon Icon name + * @param {number} x + * @param {number} y + * @param {number} s Icon size + */ +function drawIcon(icon, x, y, s) { + ({ pause: function(x, y, s) { - const w1 = s/3 - g.drawRect(x, y, x+w1, y+s) - g.drawRect(x+s-w1, y, x+s, y+s) + const w1 = s/3; + g.drawRect(x, y, x+w1, y+s); + g.drawRect(x+s-w1, y, x+s, y+s); }, play: function(x, y, s) { g.drawPoly([ x, y, x+s, y+s/2, x, y+s, - ], true) + ], true); }, previous: function(x, y, s) { - const w2 = s*1/5 + const w2 = s*1/5; g.drawPoly([ x+s, y, x+w2, y+s/2, x+s, y+s, - ], true) - g.drawRect(x, y, x+w2, y+s) + ], true); + g.drawRect(x, y, x+w2, y+s); }, next: function(x, y, s) { - const w2 = s*4/5 + const w2 = s*4/5; g.drawPoly([ x, y, x+w2, y+s/2, x, y+s, - ], true) - g.drawRect(x+w2, y, x+s, y+s) + ], true); + g.drawRect(x+w2, y, x+s, y+s); }, + })[icon](x, y, s); +} +function controlColor(ctrl) { + return (ctrl in tCommand) ? "#ff0000" : "#008800"; +} +function drawControl(ctrl, x, y) { + g.setColor(controlColor(ctrl)); + const s = 20; + if (stat!==controlState) { + g.clearRect(x, y, x+s, y+s); } - function controlColor(control) { - const s = defaults.controls - if (volCmd && control===volCmd) { - // volume button kept pressed down - return s.activeColor - } - return (control in tCommand) ? s.activeColor : s.color - } - function drawControl(control, x, y) { - g.setColor(controlColor(control)) - const s = defaults.controls.size - if (state!==controlState) { - g.clearRect(x, y, x+s, y+s) - } - icons[control](x, y, s) - } - let controlState - function drawControls() { - const s = defaults.controls - if (state==="play") { - // left touch - drawControl("pause", s.left, screen.height-(s.bottom+s.size)) - // right touch - drawControl("next", screen.width-(s.right+s.size), screen.height-(s.bottom+s.size)) - } else { - drawControl("previous", s.left, screen.height-(s.bottom+s.size)) - drawControl("play", screen.width-(s.right+s.size), screen.height-(s.bottom+s.size)) - } - g.setFont("6x8", s.volSize) - // BTN1 - g.setFontAlign(1, -1) - g.setColor(controlColor("volumeup")) - g.drawString("+", screen.width, s.top) - // BTN2 - g.setFontAlign(1, 1) - g.setColor(controlColor("volumedown")) - g.drawString("-", screen.width, screen.height-s.bottom) - controlState = state + drawIcon(ctrl, x, y, s); +} +let controlState; +function drawControls() { + g.reset(); + if (stat==="play") { + // left touch + drawControl("pause", 10, 190); + // right touch + drawControl("next", 200, 190); + } else { + drawControl("previous", 10, 190); + drawControl("play", 200, 190); } + g.setFont("6x8", 2); + // BTN1 + g.setFontAlign(1, -1); + g.setColor(controlColor("volumeup")); + g.drawString("+", 240, 30); + // BTN2 + g.setFontAlign(1, 1); + g.setColor(controlColor("volumedown")); + g.drawString("-", 240, 210); + controlState = stat; +} - function setNumInfo() { - info.num = "" - if ("n" in info && info.n>0) { - info.num = "#"+info.n - if ("c" in info && info.c>0) { // I've seen { c:-1 } - info.num += "/"+info.c - } +function drawMusic() { + drawNum(); + drawTrack(); + drawArtistAlbum(); +} + +//////////////////////// +// GB event handlers +/////////////////////// +/** + * Update music info + * @param e + */ +function musicInfo(e) { + info = e; + delete (info.t); + offset = null; + if (Bangle.isLCDOn()) { + drawMusic(); + } +} + +let tXit; +function musicState(e) { + stat = e.state; + // if paused for five minutes, load the clock + // (but timeout resets if we get new info, even while paused) + if (tXit) { + clearTimeout(tXit); + } + tXit = null; + fade = null; + delete info.track_color; + if (stat!=="play" && auto) { + if (stat==="stop") { // never actually happens with my phone :-( + load(); + } else { // also quit when paused for a long time + tXit = setTimeout(load, TOUT); + fade = Date.now(); + fadeOut(); } } - function drawMusic() { - g.reset() - setNumInfo() - drawInfo("num") - drawTrack() - drawArtistAlbum() - drawControls() + if (Bangle.isLCDOn()) { + drawControls(); } - let tQuit - function updateMusic() { - // if paused for five minutes, load the clock - // (but timeout resets if we get new info, even while paused) - if (tQuit) { - clearTimeout(tQuit) +} + +//////////////////// +// Events +//////////////////// + +// we put starting of watches inside a function, so we can defer it until +// we asked the user about autoStart +/** + * Start watching for BTN2 presses + */ +let tPress, nPress = 0; +function startButtonWatches() { + // BTN1/3: volume control + // Wait for falling edge to avoid messing with volume while long-pressing BTN3 + // to reload the watch (and same for BTN2 for consistency) + setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"}); + setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"}); + + // BTN2: long-press for launcher, otherwise depends on number of presses + setWatch(() => { + if (nPress===0) { + tPress = setTimeout(() => {Bangle.showLauncher();}, 3000); } - tQuit = null - if (state!=="play" && autoClose) { - if (state==="stop") { // never actually happens with my phone :-( - load() - } else { // also quit when paused for a long time - tQuit = setTimeout(load, TIMEOUT) - fadeOut.start() - } - } else { - fadeOut.stop() - } - drawMusic() + }, BTN2, {repeat: true, edge: "rising"}); + setWatch(() => { + nPress++; + clearTimeout(tPress); + tPress = setTimeout(handleButton2Press, 500); + }, BTN2, {repeat: true, edge: "falling"}); +} +function handleButton2Press() { + tPress = null; + switch(nPress) { + case 1: + togglePlay(); + break; + case 2: + sendCommand("next"); + break; + case 3: + sendCommand("previous"); + break; + default: // invalid + Bangle.buzz(50); } + nPress = 0; +} - // create tickers - const clock = new Clock() - const fadeOut = new Fader() - const scroller = new Scroller() +let tCommand = {}; +/** + * Send command and highlight corresponding control + * @param command "play/pause/next/previous/volumeup/volumedown" + */ +function sendCommand(command) { + Bluetooth.println(JSON.stringify({t: "music", n: command})); + // for controlColor + if (command in tCommand) { + clearTimeout(tCommand[command]); + } + tCommand[command] = setTimeout(function() { + delete tCommand[command]; + drawControls(); + }, 200); + drawControls(); +} - //////////////////// - // Events - //////////////////// - - // pause timers while screen is off - Bangle.on("lcdPower", on => { +// touch/swipe: navigation +function togglePlay() { + sendCommand(stat==="play" ? "pause" : "play"); +} +function startTouchWatches() { + Bangle.on("touch", side => { + switch(side) { + case 1: + sendCommand(stat==="play" ? "pause" : "previous"); + break; + case 2: + sendCommand(stat==="play" ? "next" : "play"); + break; + case 3: + togglePlay(); + } + }); + Bangle.on("swipe", dir => { + sendCommand(dir===1 ? "previous" : "next"); + }); +} +function startLCDWatch() { + Bangle.on("lcdPower", (on) => { if (on) { - clock.resume() - scroller.resume() - fadeOut.resume() + // redraw and resume scrolling + tick(); + drawMusic(); + drawControls(); + fadeOut(); + if (offset!==null) { + drawScroller(); + if (!iScroll) { + iScroll = setInterval(scroll, 200); + } + } } else { - clock.pause() - scroller.pause() - fadeOut.pause() - } - }) - - let tLauncher - // we put starting of watches inside a function, so we can defer it until we - // asked the user about autoStart - function startLauncherWatch() { - // long-press: launcher - // short-press: toggle play/pause - setWatch(function() { - if (tLauncher) { - clearTimeout(tLauncher) + // pause scrolling + if (iScroll) { + clearInterval(iScroll); + iScroll = null; } - tLauncher = setTimeout(Bangle.showLauncher, 1000) - }, BTN2, {repeat: true, edge: "rising"}) - setWatch(function() { - if (tLauncher) { - clearTimeout(tLauncher) - tLauncher = null - } - togglePlay() - }, BTN2, {repeat: true, edge: "falling"}) - } - - let tCommand = {} - /** - * Send command and highlight corresponding control - * @param command "play/pause/next/previous/volumeup/volumedown" - */ - function sendCommand(command) { - Bluetooth.println(JSON.stringify({t: "music", n: command})) - // for controlColor - if (command in tCommand) { - clearTimeout(tCommand[command]) } - tCommand[command] = setTimeout(function() { - delete tCommand[command] - drawControls() - }, defaults.controls.highlight) - drawControls() - } + }); +} - // BTN1/3: volume control (with repeat after long-press) - let tVol, volCmd - function volUp() { - volStart("up") +///////////////////// +// Startup +///////////////////// +// check for saved music stat (by widget) to load +g.clear(); +global.gbmusic_active = true; // we don't need our widget +Bangle.loadWidgets(); +Bangle.drawWidgets(); +delete (global.gbmusic_active); + +function startEmulator() { + if (typeof Bluetooth==="undefined") { // emulator! + Bluetooth = { + println: (line) => {console.log("Bluetooth:", line);}, + }; + // some example info + GB({"t": "musicinfo", "artist": "Some Artist Name", "album": "The Album Name", "track": "The Track Title Goes Here", "dur": 241, "c": 2, "n": 2}); + GB({"t": "musicstate", "state": "play", "position": 0, "shuffle": 1, "repeat": 1}); } - function volDown() { - volStart("down") - } - function volStart(dir) { - const command = "volume"+dir - stopVol() - sendCommand(command) - volCmd = command - tVol = setTimeout(repeatVol, 500) - } - function repeatVol() { - sendCommand(volCmd) - tVol = setTimeout(repeatVol, 100) - } - function stopVol() { - if (tVol) { - clearTimeout(tVol) - tVol = null +} +function startWatches() { + startButtonWatches(); + startTouchWatches(); + startLCDWatch(); +} + +function start() { + // start listening for music updates + const _GB = global.GB; + global.GB = (event) => { + // we eat music events! + switch(event.t) { + case "musicinfo": + musicInfo(event); + break; + case "musicstate": + musicState(event); + break; + default: + // pass on other events + if (_GB) { + setTimeout(_GB, 0, event); + } + return; } - volCmd = null - drawControls() - } - function startVolWatches() { - setWatch(volUp, BTN1, {repeat: true, edge: "rising"}) - setWatch(stopVol, BTN1, {repeat: true, edge: "falling"}) - setWatch(volDown, BTN3, {repeat: true, edge: "rising"}) - setWatch(stopVol, BTN3, {repeat: true, edge: "falling"}) - } + }; + drawMusic(); + drawControls(); + startWatches(); + tick(); + startEmulator(); +} - // touch/swipe: navigation - function togglePlay() { - sendCommand(state==="play" ? "pause" : "play") - } - function startTouchWatches() { - Bangle.on("touch", function(side) { - switch(side) { - case 1: - sendCommand(state==="play" ? "pause" : "previous") - break - case 2: - sendCommand(state==="play" ? "next" : "play") - break - case 3: - togglePlay() - } - }) - Bangle.on("swipe", function(dir) { - sendCommand(dir===1 ? "previous" : "next") - }) - } - ///////////////////// - // Startup - ///////////////////// - // check for saved music state (by widget) to load - g.clear() - global.gbmusic_active = true // we don't need our widget - Bangle.loadWidgets() - Bangle.drawWidgets() - delete (global.gbmusic_active) - - function startEmulator() { - if (typeof Bluetooth==="undefined") { // emulator! - Bluetooth = { - println: (line) => {console.log("Bluetooth:", line)}, - } - // some example info - GB({"t": "musicinfo", "artist": "Some Artist Name", "album": "The Album Name", "track": "The Track Title Goes Here", "dur": 241, "c": 2, "n": 2}) - GB({"t": "musicstate", "state": "play", "position": 0, "shuffle": 1, "repeat": 1}) - } - } - function startWatches() { - startVolWatches() - startLauncherWatch() - startTouchWatches() - } - function start() { - // start listening for music updates - const _GB = global.GB - global.GB = (event) => { - // we eat music events! - switch(event.t) { - case "musicinfo": - info = event - delete (info.t) - break - case "musicstate": - state = event.state - break - default: - // pass on other events - if (_GB) { - setTimeout(_GB, 0, event) - } - return // no drawMusic - } - updateMusic() - } - startWatches() - drawMusic() - clock.start() - startEmulator() - } - - let saved = require("Storage").readJSON("gbmusic.load.json", true) - require("Storage").erase("gbmusic.load.json") +function init() { + let saved = require("Storage").readJSON("gbmusic.load.json", true); + require("Storage").erase("gbmusic.load.json"); if (saved) { // autoloaded: load state was saved by widget - info = saved.info - state = saved.state - delete (saved) - autoClose = true - start() + info = saved.info; + stat = saved.state; + delete saved; + auto = true; + start(); } else { - const s = require("Storage").readJSON("gbmusic.json", 1) || {} + delete saved; + let s = require("Storage").readJSON("gbmusic.json", 1) || {}; if (!("autoStart" in s)) { // user opened the app, but has not picked a setting yet // ask them about autoloading now E.showPrompt( "Automatically load\n"+ "when playing music?\n", - ).then(function(autoStart) { - s.autoStart = autoStart - require("Storage").writeJSON("gbmusic.json", s) - setTimeout(start, 0) - }) + ).then(choice => { + s.autoStart = choice; + require("Storage").writeJSON("gbmusic.json", s); + delete s; + setTimeout(start, 0); + }); } else { - start() + delete s; + start(); } } } +init(); + diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index 8023409b5..f4837d60a 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -21,3 +21,4 @@ 0.19: Support for call incoming/start/end 0.20: Reduce memory usage 0.21: Fix HRM setting +0.22: Respect Quiet Mode diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index efc620e36..b4ce71907 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -154,7 +154,9 @@ case "notify-": if (event.t === "notify") { require("notify").show(prettifyNotificationEvent(event)); - Bangle.buzz(); + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + } } else { // notify- require("notify").hide(event); } @@ -174,7 +176,9 @@ body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))} if (event.cmd === "incoming") { require("notify").show(note); - Bangle.buzz(); + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + } } else if (event.cmd === "start") { require("notify").show(Object.assign(note, { bgColor : "#008000", titleBgColor : "#00C000", @@ -194,6 +198,7 @@ delete state.find; } if (event.n) + // Ignore quiet mode: we always want to find our watch state.find = setInterval(_=>{ Bangle.buzz(); setTimeout(_=>Bangle.beep(), 1000); diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index 39dcd2dc1..412dbe9d3 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -20,3 +20,4 @@ 0.16: Add gpsrec app to Settings menu 0.17: Disable recording if storage is full (fix #574) 0.18: Period counter now uses GPS time rather than counting packets (allows use with GPS Setup) +0.19: Fix memory usage issues inside track viewer app diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js index cf3591151..29594289d 100644 --- a/apps/gpsrec/app.js +++ b/apps/gpsrec/app.js @@ -51,7 +51,7 @@ function showMainMenu() { updateSettings(); } }, - 'View Tracks': viewTracks, + 'View Tracks': ()=>{viewTracks();}, '< Back': ()=>{load();} }; return E.showMenu(mainmenu); @@ -65,13 +65,13 @@ function viewTracks() { for (var n=0;n<36;n++) { var f = require("Storage").open(getFN(n),"r"); if (f.readLine()!==undefined) { - menu["Track "+n] = viewTrack.bind(null,n,false); + menu["Track "+n] = (n=>viewTrack(n)).bind(null,n,false); found = true; } } if (!found) menu["No Tracks found"] = function(){}; - menu['< Back'] = showMainMenu; + menu['< Back'] = () => { showMainMenu(); }; return E.showMenu(menu); } @@ -161,7 +161,7 @@ function viewTrack(n, info) { viewTrack(n, info); }); }; - menu['< Back'] = viewTracks; + menu['< Back'] = () => { viewTracks(); }; return E.showMenu(menu); } diff --git a/apps/hardalarm/ChangeLog b/apps/hardalarm/ChangeLog index b8b4561b8..7e9b17f2a 100644 --- a/apps/hardalarm/ChangeLog +++ b/apps/hardalarm/ChangeLog @@ -1 +1,2 @@ 0.01: Add a number to match to turn off alarm +0.02: Respect Quiet Mode diff --git a/apps/hardalarm/hardalarm.js b/apps/hardalarm/hardalarm.js index c3623a193..e33bd39cc 100644 --- a/apps/hardalarm/hardalarm.js +++ b/apps/hardalarm/hardalarm.js @@ -62,6 +62,7 @@ function showPrompt(msg, buzzCount, alarm) { } function showAlarm(alarm) { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence var msg = formatTime(alarm.hr); var buzzCount = 20; if (alarm.msg) diff --git a/apps/heart/ChangeLog b/apps/heart/ChangeLog index 70134af27..4751f0d10 100644 --- a/apps/heart/ChangeLog +++ b/apps/heart/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Don't overwrite existing settings on app update Clean up recordings on app removal +0.03: added graphing feature of 164 latest measurements +0.04: Fix memory usage when viewing HRM traces diff --git a/apps/heart/app.js b/apps/heart/app.js index 366a1068d..75edf8c4e 100644 --- a/apps/heart/app.js +++ b/apps/heart/app.js @@ -1,8 +1,25 @@ +const GraphXZero = 40; +const GraphYZero = 200; +const GraphY100 = 80; + +const GraphMarkerOffset = 5; +const MaxValueCount = 164; +const GraphXMax = GraphXZero + MaxValueCount; + Bangle.loadWidgets(); Bangle.drawWidgets(); var settings = require("Storage").readJSON("heart.json",1)||{}; +var globalSettings = require('Storage').readJSON('setting.json', true) || {timezone: 0}; +require('DateExt').locale({ + str: "0D.0M. 0h:0m", + offset: [ + globalSettings.timezone * 60, + globalSettings.timezone * 60 + ] +}); + function getFileNbr(n) { return ".heart"+n.toString(36); } @@ -35,7 +52,8 @@ function showMainMenu() { updateSettings(); } }, - 'View Records': viewRecords, + 'View Records': ()=>{viewRecords()}, + 'Graph Records': ()=>{graphRecords()}, '< Back': ()=>{load();} }; return E.showMenu(mainMenu); @@ -55,7 +73,7 @@ function viewRecords() { } if (!found) menu["No Records Found"] = function(){}; - menu['< Back'] = showMainMenu; + menu['< Back'] = ()=>{showMainMenu()}; return E.showMenu(menu); } @@ -92,9 +110,188 @@ function viewRecord(n) { viewRecord(n); }); }; - menu['< Back'] = viewRecords; + menu['< Back'] = ()=>{viewRecords()}; print(menu); return E.showMenu(menu); } +function graphRecords() { + const menu = { + '': { 'title': 'Heart Records' } + }; + var found = false; + for (var n=0;n<36;n++) { + var f = require("Storage").open(getFileNbr(n),"r"); + var line = f.readLine(); + if (line!==undefined) { + menu["#"+n+" "+Date(line.split(",")[0]*1000).as().str] = graphRecord.bind(null,n); + found = true; + } + } + if (!found) + menu["No Records Found"] = function(){}; + menu['< Back'] = ()=>{showMainMenu()}; + return E.showMenu(menu); +} + +// based on batchart +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + +function renderChart() { + // Left Y axis (Battery) + g.setColor(1, 1, 0); + g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); + + g.setFontAlign(1, -1, 0); + g.drawString("150", 35, GraphY100 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100); + + g.drawString("125", 35, GraphYZero - 110 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("100", 35, GraphYZero - 100 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("90", 35, GraphYZero - 90 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("80", 35, GraphYZero - 70 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("70", 35, GraphYZero - 50 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("60", 35, GraphYZero - 30 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("50", 35, GraphYZero - 20 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("40", 35, GraphYZero - 10 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 150, GraphXZero, 150); + + g.drawString("30", 35, GraphYZero - GraphMarkerOffset); + + g.setColor(1, 1, 1); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero); + + console.log("Finished drawing chart"); +} + +// as drawing starts at 30 HRM decreasing measrure by 30 +// recalculate for range 110-150 as only 20 pixels are available +function getY(measure) { + positionY = GraphYZero - measure + 30; + if (100 < measure < 150) { + positionY = GraphYZero - ( 100 + Math.round((measure - 100)/2) ) + 30; + g.setColor(1, 0, 0); + } else if (60 < measrure < 100) { + positionY = GraphYZero - ( 30 + Math.round((measure - 30)/2) ) + 30; + g.setColor(0, 1, 0); + } + if (positionY > GraphYZero) { + positionY = GraphYZero; + g.setColor(1, 0, 0); + } + if (positionY < GraphY100) { + positionY = GraphY100; + g.setColor(1, 0, 0); + } + return positionY; +} + +function stop() { + E.showMenu(); + load(); +} + +function graphRecord(n) { + E.showMenu({'': 'Heart Record '+n}); + E.showMessage( + "Loading Data ...\n\nMay take a while,\nwill vibrate\nwhen done.", + 'Heart Record '+n + ); + g.setFont("Vector", 10); + + var lastPixel; + var lineCount = 0; + var positionX = GraphXZero; + var positionY = GraphYZero; + var startLine = 1; + var tempCount = 0; + var f = require("Storage").open(getFileNbr(n),"r"); + var line = f.readLine(); + var times = Array(2); + console.log("Counting lines"); + while (line !== undefined) { + lineCount++; + line = f.readLine(); + } + console.log(`Line count: ${lineCount}`); + if (lineCount > MaxValueCount) { + startLine = lineCount - MaxValueCount; + } + console.log(`start: ${startLine}`); + + f = require("Storage").open(getFileNbr(n),"r"); + line = f.readLine(); + while (line !== undefined) { + currentLine = line; + line = f.readLine(); + tempCount++; + if (tempCount == startLine) { + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + renderHomeIcon(); + renderChart(); + } else if (tempCount > startLine) { + positionX++; + if (parseInt(currentLine.split(",")[2]) >= 70) { + g.setColor(1, 1, 1); + oldPositionY = positionY; + positionY = getY(parseInt(currentLine.split(",")[1])); + if (times[0] === undefined) { + times[0] = parseInt(currentLine.split(",")[0]); + } + if (tempCount == startLine + 1) { + g.setPixel(positionX, positionY); + } else { + g.drawLine(positionX - 1, oldPositionY, positionX, positionY); + times[1] = parseInt(currentLine.split(",")[0]); + } + } + } + g.flip(); + } + + g.setColor(1, 1, 0); + g.setFont("Vector", 10); + console.log('start: ' + times[0]); + console.log('end: ' + times[1]); + if (times[0] !== undefined) { + g.setFontAlign(-1, -1, 0); + var startdate = new Date(times[0]*1000); + g.drawString(startdate.local().as("0h:0m").str, 15, GraphYZero + 12); + } + if (times[1] !== undefined) { + g.setFontAlign(1, -1, 0); + var enddate = new Date(times[1]*1000); + g.drawString(enddate.local().as().str, GraphXMax, GraphYZero + 12); + } + console.log("Finished rendering data"); + Bangle.buzz(200, 0.3); + setWatch(stop, BTN2, {edge:"falling", debounce:50, repeat:false}); +} + showMainMenu(); + +// vim: et ts=2 sw=2 diff --git a/apps/hourstrike/ChangeLog b/apps/hourstrike/ChangeLog new file mode 100644 index 000000000..73b8cb168 --- /dev/null +++ b/apps/hourstrike/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App +0.02: Add different strike intervals and support for quiet time +0.03: Bug fixes for setting attributes +0.04: Add more time to strike and the strength +0.05: Add display for the next strike time +0.06: Move the next strike time to the first row of display +0.07: Change the boot function to avoid reloading the entire watch diff --git a/apps/hourstrike/README.md b/apps/hourstrike/README.md new file mode 100644 index 000000000..67a131f8a --- /dev/null +++ b/apps/hourstrike/README.md @@ -0,0 +1,25 @@ +# Hour Strike + +![icon](app-icon.png) + +Time passes too fast! + +This app configures your `Bangle.js` so that it buzzes on the hour or on the half hour. + +This app is slightly different from [Hour Chime](https://github.com/espruino/BangleApps/tree/master/apps/widchime). `Hour Chimee` runs as a widget but `Hour Strike` runs as a background task, without showing a widget. + +## Features + +- Strike the hour, the half hour, the quarter hour, and more +- Set up a range of hours that clock will strike +- Set up the strength of the strike +- Preview when the next strike will happen + +## Known Issues + +- This app does not know or check whether your clock already chimes on the hour. + +## Creator + +[Weiming Hu](https://weiming-hu.github.io/), using coding from the [Default Alarm](https://github.com/espruino/BangleApps/tree/master/apps/alarm). + diff --git a/apps/hourstrike/app-icon.js b/apps/hourstrike/app-icon.js new file mode 100644 index 000000000..7f2040745 --- /dev/null +++ b/apps/hourstrike/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkGswAHogAEBxAAHsgXFowXPCwowQFwwwQCIUjn/zmQwPFwUj/4ACDAQwMBwNDCgPwh4DBmgwMFwU/C4vzGBgMBoRECC4f/kgwLIwgXFJBgLBl4XH+QXNLwQXFMAQXPmEDC6K8DiEBAoYXSgA1DI6MxgETL6gYBgIGBC5ynDCYMQAwKnOa4YABmTXQoQXEAAUkC5dkMAx2EowXJJBBGNAAMUBwMjMAgHBoIXLiIPBDAM/+YWCokRC5gwCAAtBC5owDAAgJBC5dBiAwGoMBigXLokQgIXFA4QXMoEAAANNqAECggXR7oXXAYQXSgvUC6sEC60N6oXW6AXUinu8IXTgPuAAMQC6UEC4SsDC58OC4XgC9RHXO66nCoLXVAAoXmABQX1A")) diff --git a/apps/hourstrike/app-icon.png b/apps/hourstrike/app-icon.png new file mode 100644 index 000000000..f7ca232cf Binary files /dev/null and b/apps/hourstrike/app-icon.png differ diff --git a/apps/hourstrike/app.js b/apps/hourstrike/app.js new file mode 100644 index 000000000..c70fa2d41 --- /dev/null +++ b/apps/hourstrike/app.js @@ -0,0 +1,48 @@ +const storage = require('Storage'); +let settings; + +function updateSettings() { + storage.write('hourstrike.json', settings); +} + +function resetSettings() { + settings = { + interval: 3600, + start: 9, + end: 21, + vlevel: 0.5, + next_hour: -1, + next_minute: -1, + }; + updateSettings(); +} + +settings = storage.readJSON('hourstrike.json', 1); +if (!settings) resetSettings(); + +function showMainMenu() { + var mode_txt = ['Off','1 min','5 min','10 min','1/4 h','1/2 h','1 h']; + var mode_interval = [-1,60,300,600,900,1800,3600]; + const mainmenu = {'': { 'title': 'Hour Strike' }}; + mainmenu['Next strike '+settings.next_hour+':'+settings.next_minute] = function(){}; + mainmenu['Notify every'] = { + value: mode_interval.indexOf(settings.interval), + min: 0, max: 6, format: v => mode_txt[v], + onchange: v => { + settings.interval = mode_interval[v]; + if (v===0) {settings.next_hour = -1; settings.next_minute = -1;} + updateSettings();}}; + mainmenu.Start = { + value: settings.start, min: 0, max: 23, format: v=>v+':00', + onchange: v=> {settings.start = v; updateSettings();}}; + mainmenu.End = { + value: settings.end, min: 0, max: 23, format: v=>v+':59', + onchange: v=> {settings.end = v; updateSettings();}}; + mainmenu.Strength = { + value: settings.vlevel*10, min: 1, max: 10, format: v=>v/10, + onchange: v=> {settings.vlevel = v/10; updateSettings();}}; + mainmenu['< Back'] = ()=>load(); + return E.showMenu(mainmenu); +} + +showMainMenu(); diff --git a/apps/hourstrike/boot.js b/apps/hourstrike/boot.js new file mode 100644 index 000000000..8ddad31af --- /dev/null +++ b/apps/hourstrike/boot.js @@ -0,0 +1,39 @@ +(function() { + function setup () { + var settings = require('Storage').readJSON('hourstrike.json',1)||[]; + var t = new Date(); + var t_min_sec = t.getMinutes()*60+t.getSeconds(); + var wait_msec = settings.interval>0?(settings.interval-t_min_sec%settings.interval)*1000:-1; + if (wait_msec>0) { + t.setMilliseconds(t.getMilliseconds()+wait_msec); + var t_hour = t.getHours(); + if (t_hoursettings.end) { + var strike = new Date(t.getTime()); + strike.setHours(settings.start); + strike.setMinutes(0); + if (t_hour>settings.end) { + strike.setDate(strike.getDate()+1); + } + wait_msec += strike-t; + settings.next_hour = strike.getHours(); + settings.next_minute = strike.getMinutes(); + } else { + settings.next_hour = t_hour; + settings.next_minute = t.getMinutes(); + } + setTimeout(strike_func, wait_msec); + } else { + settings.next_hour = -1; + settings.next_minute = -1; + } + require('Storage').write('hourstrike.json', settings); + } + function strike_func () { + var setting = require('Storage').readJSON('hourstrike.json',1)||[]; + Bangle.buzz(200, setting.vlevel||0.5) + .then(() => new Promise(resolve => setTimeout(resolve,200))) + .then(() => Bangle.buzz(200, setting.vlevel||0.5)); + setup(); + } + setup(); +})(); diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog index 96aa1c45b..b3026a817 100644 --- a/apps/kitchen/ChangeLog +++ b/apps/kitchen/ChangeLog @@ -1,2 +1,3 @@ 0.01: First version 0.02: compass disable BTN1,BTN2 while waiting for GPS to reach RUNNING status +0.03: Don't buzz for GPS fix in Quiet Mode \ No newline at end of file diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js index c13b0eb10..1bc666426 100644 --- a/apps/kitchen/kitchen.app.js +++ b/apps/kitchen/kitchen.app.js @@ -246,7 +246,9 @@ GPS.prototype.processFix = function(fix) { if (fix.fix) { //this.log_debug("Got fix - setting state to GPS_RUNNING"); this.gpsState = this.GPS_RUNNING; - if (!this.last_fix.fix) Bangle.buzz(); // buzz on first position + if (!this.last_fix.fix && !(require("Storage").readJSON("setting.json", 1) || {}).quiet) { + Bangle.buzz(); // buzz on first position + } this.last_fix = fix; } }; diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index 66e4ab800..276c65c6b 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -12,3 +12,4 @@ 0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%) 0.13: Fix drawPyramid function so pyramids are drawn in correct Y position 0.14: Add jumping frame for characters +0.15: Disable notification buzz during Quiet Mode diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 20d1dfd85..6289a2568 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -1,718 +1,720 @@ -/** - * BangleJS MARIO CLOCK - * - * + Original Author: Paul Cockrell https://github.com/paulcockrell - * + Created: April 2020 - * + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock - * + Online Image convertor: https://www.espruino.com/Image+Converter, Use transparency + compression + 8bit Web + export as Image String - * + Images must be drawn as PNGs with transparent backgrounds - */ - -const locale = require("locale"); -const storage = require('Storage'); -const settings = (storage.readJSON('setting.json', 1) || {}); -const timeout = settings.timeout || 10; -const is12Hour = settings["12hour"] || false; - -// Screen dimensions -let W, H; -// Screen brightness -let brightness = 1; - -let intervalRef, displayTimeoutRef = null; - -// Colours -const LIGHTEST = "#effedd"; -const LIGHT = "#add795"; -const DARK = "#588d77"; -const DARKEST = "#122d3e"; -const NIGHT = "#001818"; - -// Character names -const DAISY = "daisy"; -const TOAD = "toad"; -const MARIO = "mario"; - -const characterSprite = { - frameIdx: 0, - x: 33, - y: 55, - jumpCounter: 0, - jumpIncrement: Math.PI / 6, - isJumping: false, - character: MARIO, -}; - -const coinSprite = { - frameIdx: 0, - x: 34, - y: 18, - isAnimating: false, - yDefault: 18, -}; - -const pyramidSprite = { - x: 90, - height: 34, -}; - -const ONE_SECOND = 1000; -const DATE_MODE = "date"; -const BATT_MODE = "batt"; -const TEMP_MODE = "temp"; -const PHON_MODE = "gbri"; - -let timer = 0; -let backgroundArr = []; -let nightMode = false; -let infoMode = DATE_MODE; - -// Used to stop values flapping when displayed on screen -let lastBatt = 0; -let lastTemp = 0; - -const phone = { - get status() { - return NRF.getSecurityStatus().connected ? "Yes" : "No"; - }, - message: null, - messageTimeout: null, - messageScrollX: null, - messageType: null, -}; - -const SETTINGS_FILE = "marioclock.json"; - -function readSettings() { - return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; -} - -function writeSettings(newSettings) { - require("Storage").writeJSON(SETTINGS_FILE, newSettings); -} - -function phoneOutbound(msg) { - Bluetooth.println(JSON.stringify(msg)); -} - -function phoneClearMessage() { - if (phone.message === null) return; - - if (phone.messageTimeout) { - clearTimeout(phone.messageTimeout); - phone.messageTimeout = null; - } - phone.message = null; - phone.messageScrollX = null; - phone.messageType = null; -} - -function phoneNewMessage(type, msg) { - Bangle.buzz(); - - phoneClearMessage(); - phone.messageTimeout = setTimeout(() => phone.message = null, ONE_SECOND * 30); - phone.message = msg; - phone.messageType = type; - - // Notify user and active screen - if (!Bangle.isLCDOn()) { - clearTimers(); - Bangle.setLCDPower(true); - } -} - -function truncStr(str, max) { - if (str.length > max) { - return str.substr(0, max) + '...'; - } - return str; -} - -function phoneInbound(evt) { - switch (evt.t) { - case 'notify': - const sender = truncStr(evt.sender, 10); - const subject = truncStr(evt.subject, 15); - phoneNewMessage("notify", `${sender} - '${subject}'`); - break; - case 'call': - if (evt.cmd === "accept") { - let nameOrNumber = "Unknown"; - if (evt.name !== null || evt.name !== "") { - nameOrNumber = evt.name; - } else if (evt.number !== null || evt.number !== "") { - nameOrNumber = evt.number; - } - phoneNewMessage("call", nameOrNumber); - } - break; - default: - return null; - } -} - -function genRanNum(min, max) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function switchCharacter() { - const curChar = characterSprite.character; - - let newChar; - switch(curChar) { - case DAISY: - newChar = MARIO; - break; - case TOAD: - newChar = DAISY; - break; - case MARIO: - default: - newChar = TOAD; - } - - characterSprite.character = newChar; -} - -function toggleNightMode() { - if (!nightMode) { - nightMode = true; - return; - } - - brightness -= 0.30; - if (brightness <= 0) { - brightness = 1; - nightMode = false; - } - Bangle.setLCDBrightness(brightness); -} - -function incrementTimer() { - if (timer > 100) { - timer = 0; - } - else { - timer += 10; - } -} - -function drawBackground() { - "ram" - - // Clear screen - const bgColor = (nightMode) ? NIGHT : LIGHTEST; - g.setColor(bgColor); - g.fillRect(0, 10, W, H); - - // set cloud colors and draw clouds - const cloudColor = (nightMode) ? DARK : LIGHT; - g.setColor(cloudColor); - g.fillRect(0, 10, W, 15); - g.fillRect(0, 17, W, 17); - g.fillRect(0, 19, W, 19); - g.fillRect(0, 21, W, 21); - - // Date bar - g.setColor(DARKEST); - g.fillRect(0, 0, W, 9); -} - -function drawFloor() { - const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image - for (let x = 0; x < 4; x++) { - g.drawImage(fImg, x * 20, g.getHeight() - 5); - } -} - -function drawPyramid() { - "ram" - - const pPol = [pyramidSprite.x + 10, H - 5, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 5]; // Pyramid poly - - const color = (nightMode) ? DARK : LIGHT; - g.setColor(color); - g.fillPoly(pPol); - - pyramidSprite.x -= 1; - // Reset and randomize pyramid if off-screen - if (pyramidSprite.x < - 100) { - pyramidSprite.x = 90; - pyramidSprite.height = genRanNum(25, 60); - } -} - -function drawTreesFrame(x, y) { - const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image - - g.drawImage(tImg, x, y); - g.setColor(DARKEST); - g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6); -} - -function generateTreeSprite() { - return { - x: 90, - y: genRanNum(30, 60) - }; -} - -function drawTrees() { - // remove first sprite if offscreen - let firstBackgroundSprite = backgroundArr[0]; - if (firstBackgroundSprite) { - if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1); - } - - // set background sprite if array empty - let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1]; - if (!lastBackgroundSprite) { - const newSprite = generateTreeSprite(); - lastBackgroundSprite = newSprite; - backgroundArr.push(lastBackgroundSprite); - } - - // add random sprites - if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) { - const randIdx = Math.floor(Math.random() * 25); - if (randIdx < 2) { - const newSprite = generateTreeSprite(); - backgroundArr.push(newSprite); - } - } - - for (x = 0; x < backgroundArr.length; x++) { - let scenerySprite = backgroundArr[x]; - scenerySprite.x -= 5; - drawTreesFrame(scenerySprite.x, scenerySprite.y); - } -} - -function drawCoinFrame(x, y) { - const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image - g.drawImage(cImg, x, y); -} - -function drawCoin() { - if (!coinSprite.isAnimating) return; - - coinSprite.y -= 8; - if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) { - coinSprite.isAnimating = false; - coinSprite.y = coinSprite.yDefault; - return; - } - - drawCoinFrame(coinSprite.x, coinSprite.y); -} - -function drawDaisyFrame(idx, x, y) { - var frame; - - switch(idx) { - case 2: - frame = require("heatshrink").decompress(atob("h0UxH+AAkrAIgAH60rAIQNIBQIABDZErAAwMMBwo0CBxQNEHAQGCBpIPCBoQJCDRIXDBpA7DBIQACw5yCJQgZDP4gNErlcJAZ6GAgNcw+HRI4CCDgNcU44ZDDYSYGDIYACB4QaEDYgMFJAg3DFQ5mFBQYA==")); // daisy jumping - break; - case 0: - frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgWHw6TIAQXWrlcWZAqBDQIeBBxQaBDxIcCHIQ8JDAIAFWJLPHA==")); - break; - case 1: - default: - frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgQvBSZACCBwNcWZQcCAAIPIDgYACFw4YBDYIOCD4waEDYI+HaBQ=")); - } - - g.drawImage(frame, x, y); -} - -function drawMarioFrame(idx, x, y) { - var frame; - - switch(idx) { - case 2: - frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYFCBo9cAAIEB63WB4gMDB4YOFBowfDw4xDBAYADA4YcDGwYACDoYAEBYYBBw4NDCoYOFDIweFFwoZFAQYIDLAQWGEwqgECI6ECJ4JeGQYS9EB4QTHBwImCBYRtDSAwrFawqkFWY7PEBxoMFKoZaELoYICAAg")); // Mario frame jumping - break; - case 0: - frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGGQgNCw4ACLwgFBBwgKECQpZCCgRqDFQikEJIriIBgzwIdxjiGBxIuEBIo=")); // Mario Frame 1 - break; - case 1: - default: - frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIeJDAQODSwaVHUhwOLfg4FHe4wASA=")); // Mario frame 2 - } - - g.drawImage(frame, x, y); -} - -function drawToadFrame(idx, x, y) { - var frame; - - switch(idx) { - case 2: - frame = require("heatshrink").decompress(atob("iEUxH+ACkrAAoNJrnWAAQRGlfWrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPENwoTEH4crw4EDAAgGDB4YABAYIBDP4YLEAAIPHCAQHCCAQTDD4gHDEA4PFGAY3EbooPECob8IPooPFCATGEf44hFAAYLDA==")); // toad jumping - break; - case 0: - frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYYrCCAwbFFwgQEM4gAEeA4OIH4ghFAAYLD")); // Toad Frame 1 - break; - case 1: - default: - frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2 - } - - g.drawImage(frame, x, y); -} - -// Mario speach bubble -function drawNotice(x, y) { - if (phone.message === null) return; - - let img; - switch (phone.messageType) { - case "call": - img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); - break; - case "notify": - img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); - break; - case "lowBatt": - img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); - break; - } - - if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); -} - -function drawCharacter(date, character) { - "ram" - - // calculate jumping - const seconds = date.getSeconds(), - milliseconds = date.getMilliseconds(); - - if (seconds == 59 && milliseconds > 800 && !characterSprite.isJumping) { - characterSprite.isJumping = true; - } - - if (characterSprite.isJumping) { - characterSprite.y = (Math.sin(characterSprite.jumpCounter) * -12) + 50 /* Character Y base value */; - characterSprite.jumpCounter += characterSprite.jumpIncrement; - - if (parseInt(characterSprite.jumpCounter) === 2 && !coinSprite.isAnimating) { - coinSprite.isAnimating = true; - } - - if (characterSprite.jumpCounter.toFixed(1) >= 4) { - characterSprite.jumpCounter = 0; - characterSprite.isJumping = false; - } - } - - // calculate animation timing - if (timer % 20 === 0) { - // shift to next frame - if (characterSprite.isJumping) { - characterSprite.frameIdx = 2; - } else { - characterSprite.frameIdx = characterSprite.frameIdx == 0 ? 1 : 0; - } - } - - switch(characterSprite.character) { - case DAISY: - drawDaisyFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); - break; - case TOAD: - drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); - break; - case MARIO: - default: - drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); - } -} - -function drawBrickFrame(x, y) { - const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A")); - g.drawImage(brk, x, y); -} - -function drawTime(date) { - // draw hour brick - drawBrickFrame(20, 25); - // draw minute brick - drawBrickFrame(42, 25); - - const h = date.getHours(); - const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2); - const mins = ("0" + date.getMinutes()).substr(-2); - - g.setFont("6x8"); - g.setColor(DARKEST); - g.drawString(hours, 25, 29); - g.drawString(mins, 47, 29); -} - -function buildDateStr(date) { - let dateStr = locale.date(date, true); - dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,""); - dateStr = locale.dow(date, true) + " " + dateStr; - - return dateStr; -} - -function buildBatStr() { - let batt = parseInt(E.getBattery()); - const battDiff = Math.abs(lastBatt - batt); - - // Suppress flapping values - // Only update batt if it moves greater than +-2 - if (battDiff > 2) { - lastBatt = batt; - } else { - batt = lastBatt; - } - - const battStr = `Bat: ${batt}%`; - - return battStr; -} - -function buildTempStr() { - let temp = parseInt(E.getTemperature()); - const tempDiff = Math.abs(lastTemp - temp); - - // Suppress flapping values - // Only update temp if it moves greater than +-2 - if (tempDiff > 2) { - lastTemp = temp; - } else { - temp = lastTemp; - } - const tempStr = `Temp: ${temp}'c`; - - return tempStr; -} - -function buildPhonStr() { - return `Phone: ${phone.status}`; -} - -function drawInfo(date) { - let xPos; - let str = ""; - - if (phone.message !== null) { - str = phone.message; - const strLen = g.stringWidth(str); - if (strLen > W) { - if (phone.messageScrollX === null || (phone.messageScrollX <= (strLen * -1))) { - phone.messageScrollX = W; - resetDisplayTimeout(); - } else { - phone.messageScrollX -= 2; - } - xPos = phone.messageScrollX; - } else { - xPos = (W - g.stringWidth(str)) / 2; - } - } else { - switch(infoMode) { - case PHON_MODE: - str = buildPhonStr(); - break; - case TEMP_MODE: - str = buildTempStr(); - break; - case BATT_MODE: - str = buildBatStr(); - break; - case DATE_MODE: - default: - str = buildDateStr(date); - } - xPos = (W - g.stringWidth(str)) / 2; - } - - g.setFont("6x8"); - g.setColor(LIGHTEST); - g.drawString(str, xPos, 1); -} - -function changeInfoMode() { - phoneClearMessage(); - - switch(infoMode) { - case BATT_MODE: - infoMode = TEMP_MODE; - break; - case TEMP_MODE: - infoMode = PHON_MODE; - break; - case PHON_MODE: - infoMode = DATE_MODE; - break; - case DATE_MODE: - default: - infoMode = BATT_MODE; - } -} - -function redraw() { - const date = new Date(); - - // Update timers - incrementTimer(); - - // Draw frame - drawBackground(); - drawFloor(); - drawPyramid(); - drawTrees(); - drawTime(date); - drawInfo(date); - drawCharacter(date); - drawNotice(); - drawCoin(); - - // Render new frame - g.flip(); -} - -function clearTimers(){ - if(intervalRef) { - clearInterval(intervalRef); - intervalRef = null; - } - - if(displayTimeoutRef) { - clearInterval(displayTimeoutRef); - displayTimeoutRef = null; - } -} - -function resetDisplayTimeout() { - if (displayTimeoutRef) clearInterval(displayTimeoutRef); - - displayTimeoutRef = setInterval(() => { - if (Bangle.isLCDOn()) Bangle.setLCDPower(false); - clearTimers(); - }, ONE_SECOND * timeout); -} - -function startTimers(){ - if(intervalRef) clearTimers(); - intervalRef = setInterval(redraw, 50); - - resetDisplayTimeout(); - - redraw(); -} - -function loadSettings() { - const settings = readSettings(); - if (!settings) return; - - if (settings.character) characterSprite.character = settings.character; - if (settings.nightMode) nightMode = settings.nightMode; - if (settings.brightness) { - brightness = settings.brightness; - Bangle.setLCDBrightness(brightness); - } -} - -function updateSettings() { - const newSettings = { - character: characterSprite.character, - nightMode: nightMode, - brightness: brightness, - }; - writeSettings(newSettings); -} - -function checkBatteryLevel() { - if (Bangle.isCharging()) return; - if (E.getBattery() > 10) return; - if (phone.message !== null) return; - - phoneNewMessage("lowBatt", "Warning, battery is low"); -} - -// Main -function init() { - loadSettings(); - - clearInterval(); - - // Initialise display - Bangle.setLCDMode("80x80"); - - // Store screen dimensions - W = g.getWidth(); - H = g.getHeight(); - - // Get Mario to jump! - setWatch(() => { - if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true; - resetDisplayTimeout(); - phoneClearMessage(); // Clear any phone messages and message timers - }, BTN3, {repeat: true}); - - // Close watch and load launcher app - setWatch(() => { - Bangle.setLCDMode(); - Bangle.showLauncher(); - }, BTN2, {repeat: false, edge: "falling"}); - - // Change info mode - setWatch(() => { - changeInfoMode(); - }, BTN1, {repeat: true}); - - Bangle.on('lcdPower', (on) => on ? startTimers() : clearTimers()); - - Bangle.on('faceUp', (up) => { - if (up && !Bangle.isLCDOn()) { - clearTimers(); - Bangle.setLCDPower(true); - } - }); - - Bangle.on('swipe', (sDir) => { - resetDisplayTimeout(); - - switch(sDir) { - // Swipe right (1) - change character (on a loop) - case 1: - switchCharacter(); - break; - // Swipe left (-1) - change day/night mode (on a loop) - case -1: - default: - toggleNightMode(); - } - - updateSettings(); - }); - - // Phone connectivity - try { NRF.wake(); } catch (e) {} - - NRF.on('disconnect', () => { - phoneNewMessage(null, "Phone disconnected"); - }); - - NRF.on('connect', () => { - setTimeout(() => { - phoneOutbound({ t: "status", bat: E.getBattery() }); - }, ONE_SECOND * 2); - phoneNewMessage(null, "Phone connected"); - }); - - GB = (evt) => phoneInbound(evt); - - startTimers(); - - setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); - checkBatteryLevel(); -} - -// Initialise! -init(); \ No newline at end of file +/** + * BangleJS MARIO CLOCK + * + * + Original Author: Paul Cockrell https://github.com/paulcockrell + * + Created: April 2020 + * + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock + * + Online Image convertor: https://www.espruino.com/Image+Converter, Use transparency + compression + 8bit Web + export as Image String + * + Images must be drawn as PNGs with transparent backgrounds + */ + +const locale = require("locale"); +const storage = require('Storage'); +const settings = (storage.readJSON('setting.json', 1) || {}); +const timeout = settings.timeout || 10; +const is12Hour = settings["12hour"] || false; + +// Screen dimensions +let W, H; +// Screen brightness +let brightness = 1; + +let intervalRef, displayTimeoutRef = null; + +// Colours +const LIGHTEST = "#effedd"; +const LIGHT = "#add795"; +const DARK = "#588d77"; +const DARKEST = "#122d3e"; +const NIGHT = "#001818"; + +// Character names +const DAISY = "daisy"; +const TOAD = "toad"; +const MARIO = "mario"; + +const characterSprite = { + frameIdx: 0, + x: 33, + y: 55, + jumpCounter: 0, + jumpIncrement: Math.PI / 6, + isJumping: false, + character: MARIO, +}; + +const coinSprite = { + frameIdx: 0, + x: 34, + y: 18, + isAnimating: false, + yDefault: 18, +}; + +const pyramidSprite = { + x: 90, + height: 34, +}; + +const ONE_SECOND = 1000; +const DATE_MODE = "date"; +const BATT_MODE = "batt"; +const TEMP_MODE = "temp"; +const PHON_MODE = "gbri"; + +let timer = 0; +let backgroundArr = []; +let nightMode = false; +let infoMode = DATE_MODE; + +// Used to stop values flapping when displayed on screen +let lastBatt = 0; +let lastTemp = 0; + +const phone = { + get status() { + return NRF.getSecurityStatus().connected ? "Yes" : "No"; + }, + message: null, + messageTimeout: null, + messageScrollX: null, + messageType: null, +}; + +const SETTINGS_FILE = "marioclock.json"; + +function readSettings() { + return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; +} + +function writeSettings(newSettings) { + require("Storage").writeJSON(SETTINGS_FILE, newSettings); +} + +function phoneOutbound(msg) { + Bluetooth.println(JSON.stringify(msg)); +} + +function phoneClearMessage() { + if (phone.message === null) return; + + if (phone.messageTimeout) { + clearTimeout(phone.messageTimeout); + phone.messageTimeout = null; + } + phone.message = null; + phone.messageScrollX = null; + phone.messageType = null; +} + +function phoneNewMessage(type, msg) { + + phoneClearMessage(); + phone.messageTimeout = setTimeout(() => phone.message = null, ONE_SECOND * 30); + phone.message = msg; + phone.messageType = type; + + // Notify user and active screen + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + if (!Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDPower(true); + } + } +} + +function truncStr(str, max) { + if (str.length > max) { + return str.substr(0, max) + '...'; + } + return str; +} + +function phoneInbound(evt) { + switch (evt.t) { + case 'notify': + const sender = truncStr(evt.sender, 10); + const subject = truncStr(evt.subject, 15); + phoneNewMessage("notify", `${sender} - '${subject}'`); + break; + case 'call': + if (evt.cmd === "accept") { + let nameOrNumber = "Unknown"; + if (evt.name !== null || evt.name !== "") { + nameOrNumber = evt.name; + } else if (evt.number !== null || evt.number !== "") { + nameOrNumber = evt.number; + } + phoneNewMessage("call", nameOrNumber); + } + break; + default: + return null; + } +} + +function genRanNum(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function switchCharacter() { + const curChar = characterSprite.character; + + let newChar; + switch(curChar) { + case DAISY: + newChar = MARIO; + break; + case TOAD: + newChar = DAISY; + break; + case MARIO: + default: + newChar = TOAD; + } + + characterSprite.character = newChar; +} + +function toggleNightMode() { + if (!nightMode) { + nightMode = true; + return; + } + + brightness -= 0.30; + if (brightness <= 0) { + brightness = 1; + nightMode = false; + } + Bangle.setLCDBrightness(brightness); +} + +function incrementTimer() { + if (timer > 100) { + timer = 0; + } + else { + timer += 10; + } +} + +function drawBackground() { + "ram" + + // Clear screen + const bgColor = (nightMode) ? NIGHT : LIGHTEST; + g.setColor(bgColor); + g.fillRect(0, 10, W, H); + + // set cloud colors and draw clouds + const cloudColor = (nightMode) ? DARK : LIGHT; + g.setColor(cloudColor); + g.fillRect(0, 10, W, 15); + g.fillRect(0, 17, W, 17); + g.fillRect(0, 19, W, 19); + g.fillRect(0, 21, W, 21); + + // Date bar + g.setColor(DARKEST); + g.fillRect(0, 0, W, 9); +} + +function drawFloor() { + const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image + for (let x = 0; x < 4; x++) { + g.drawImage(fImg, x * 20, g.getHeight() - 5); + } +} + +function drawPyramid() { + "ram" + + const pPol = [pyramidSprite.x + 10, H - 5, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 5]; // Pyramid poly + + const color = (nightMode) ? DARK : LIGHT; + g.setColor(color); + g.fillPoly(pPol); + + pyramidSprite.x -= 1; + // Reset and randomize pyramid if off-screen + if (pyramidSprite.x < - 100) { + pyramidSprite.x = 90; + pyramidSprite.height = genRanNum(25, 60); + } +} + +function drawTreesFrame(x, y) { + const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image + + g.drawImage(tImg, x, y); + g.setColor(DARKEST); + g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6); +} + +function generateTreeSprite() { + return { + x: 90, + y: genRanNum(30, 60) + }; +} + +function drawTrees() { + // remove first sprite if offscreen + let firstBackgroundSprite = backgroundArr[0]; + if (firstBackgroundSprite) { + if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1); + } + + // set background sprite if array empty + let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1]; + if (!lastBackgroundSprite) { + const newSprite = generateTreeSprite(); + lastBackgroundSprite = newSprite; + backgroundArr.push(lastBackgroundSprite); + } + + // add random sprites + if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) { + const randIdx = Math.floor(Math.random() * 25); + if (randIdx < 2) { + const newSprite = generateTreeSprite(); + backgroundArr.push(newSprite); + } + } + + for (x = 0; x < backgroundArr.length; x++) { + let scenerySprite = backgroundArr[x]; + scenerySprite.x -= 5; + drawTreesFrame(scenerySprite.x, scenerySprite.y); + } +} + +function drawCoinFrame(x, y) { + const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image + g.drawImage(cImg, x, y); +} + +function drawCoin() { + if (!coinSprite.isAnimating) return; + + coinSprite.y -= 8; + if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) { + coinSprite.isAnimating = false; + coinSprite.y = coinSprite.yDefault; + return; + } + + drawCoinFrame(coinSprite.x, coinSprite.y); +} + +function drawDaisyFrame(idx, x, y) { + var frame; + + switch(idx) { + case 2: + frame = require("heatshrink").decompress(atob("h0UxH+AAkrAIgAH60rAIQNIBQIABDZErAAwMMBwo0CBxQNEHAQGCBpIPCBoQJCDRIXDBpA7DBIQACw5yCJQgZDP4gNErlcJAZ6GAgNcw+HRI4CCDgNcU44ZDDYSYGDIYACB4QaEDYgMFJAg3DFQ5mFBQYA==")); // daisy jumping + break; + case 0: + frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgWHw6TIAQXWrlcWZAqBDQIeBBxQaBDxIcCHIQ8JDAIAFWJLPHA==")); + break; + case 1: + default: + frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgQvBSZACCBwNcWZQcCAAIPIDgYACFw4YBDYIOCD4waEDYI+HaBQ=")); + } + + g.drawImage(frame, x, y); +} + +function drawMarioFrame(idx, x, y) { + var frame; + + switch(idx) { + case 2: + frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYFCBo9cAAIEB63WB4gMDB4YOFBowfDw4xDBAYADA4YcDGwYACDoYAEBYYBBw4NDCoYOFDIweFFwoZFAQYIDLAQWGEwqgECI6ECJ4JeGQYS9EB4QTHBwImCBYRtDSAwrFawqkFWY7PEBxoMFKoZaELoYICAAg")); // Mario frame jumping + break; + case 0: + frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGGQgNCw4ACLwgFBBwgKECQpZCCgRqDFQikEJIriIBgzwIdxjiGBxIuEBIo=")); // Mario Frame 1 + break; + case 1: + default: + frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIeJDAQODSwaVHUhwOLfg4FHe4wASA=")); // Mario frame 2 + } + + g.drawImage(frame, x, y); +} + +function drawToadFrame(idx, x, y) { + var frame; + + switch(idx) { + case 2: + frame = require("heatshrink").decompress(atob("iEUxH+ACkrAAoNJrnWAAQRGlfWrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPENwoTEH4crw4EDAAgGDB4YABAYIBDP4YLEAAIPHCAQHCCAQTDD4gHDEA4PFGAY3EbooPECob8IPooPFCATGEf44hFAAYLDA==")); // toad jumping + break; + case 0: + frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYYrCCAwbFFwgQEM4gAEeA4OIH4ghFAAYLD")); // Toad Frame 1 + break; + case 1: + default: + frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2 + } + + g.drawImage(frame, x, y); +} + +// Mario speach bubble +function drawNotice(x, y) { + if (phone.message === null) return; + + let img; + switch (phone.messageType) { + case "call": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); + break; + case "notify": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + break; + case "lowBatt": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); + break; + } + + if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); +} + +function drawCharacter(date, character) { + "ram" + + // calculate jumping + const seconds = date.getSeconds(), + milliseconds = date.getMilliseconds(); + + if (seconds == 59 && milliseconds > 800 && !characterSprite.isJumping) { + characterSprite.isJumping = true; + } + + if (characterSprite.isJumping) { + characterSprite.y = (Math.sin(characterSprite.jumpCounter) * -12) + 50 /* Character Y base value */; + characterSprite.jumpCounter += characterSprite.jumpIncrement; + + if (parseInt(characterSprite.jumpCounter) === 2 && !coinSprite.isAnimating) { + coinSprite.isAnimating = true; + } + + if (characterSprite.jumpCounter.toFixed(1) >= 4) { + characterSprite.jumpCounter = 0; + characterSprite.isJumping = false; + } + } + + // calculate animation timing + if (timer % 20 === 0) { + // shift to next frame + if (characterSprite.isJumping) { + characterSprite.frameIdx = 2; + } else { + characterSprite.frameIdx = characterSprite.frameIdx == 0 ? 1 : 0; + } + } + + switch(characterSprite.character) { + case DAISY: + drawDaisyFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + break; + case TOAD: + drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + break; + case MARIO: + default: + drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + } +} + +function drawBrickFrame(x, y) { + const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A")); + g.drawImage(brk, x, y); +} + +function drawTime(date) { + // draw hour brick + drawBrickFrame(20, 25); + // draw minute brick + drawBrickFrame(42, 25); + + const h = date.getHours(); + const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2); + const mins = ("0" + date.getMinutes()).substr(-2); + + g.setFont("6x8"); + g.setColor(DARKEST); + g.drawString(hours, 25, 29); + g.drawString(mins, 47, 29); +} + +function buildDateStr(date) { + let dateStr = locale.date(date, true); + dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,""); + dateStr = locale.dow(date, true) + " " + dateStr; + + return dateStr; +} + +function buildBatStr() { + let batt = parseInt(E.getBattery()); + const battDiff = Math.abs(lastBatt - batt); + + // Suppress flapping values + // Only update batt if it moves greater than +-2 + if (battDiff > 2) { + lastBatt = batt; + } else { + batt = lastBatt; + } + + const battStr = `Bat: ${batt}%`; + + return battStr; +} + +function buildTempStr() { + let temp = parseInt(E.getTemperature()); + const tempDiff = Math.abs(lastTemp - temp); + + // Suppress flapping values + // Only update temp if it moves greater than +-2 + if (tempDiff > 2) { + lastTemp = temp; + } else { + temp = lastTemp; + } + const tempStr = `Temp: ${temp}'c`; + + return tempStr; +} + +function buildPhonStr() { + return `Phone: ${phone.status}`; +} + +function drawInfo(date) { + let xPos; + let str = ""; + + if (phone.message !== null) { + str = phone.message; + const strLen = g.stringWidth(str); + if (strLen > W) { + if (phone.messageScrollX === null || (phone.messageScrollX <= (strLen * -1))) { + phone.messageScrollX = W; + resetDisplayTimeout(); + } else { + phone.messageScrollX -= 2; + } + xPos = phone.messageScrollX; + } else { + xPos = (W - g.stringWidth(str)) / 2; + } + } else { + switch(infoMode) { + case PHON_MODE: + str = buildPhonStr(); + break; + case TEMP_MODE: + str = buildTempStr(); + break; + case BATT_MODE: + str = buildBatStr(); + break; + case DATE_MODE: + default: + str = buildDateStr(date); + } + xPos = (W - g.stringWidth(str)) / 2; + } + + g.setFont("6x8"); + g.setColor(LIGHTEST); + g.drawString(str, xPos, 1); +} + +function changeInfoMode() { + phoneClearMessage(); + + switch(infoMode) { + case BATT_MODE: + infoMode = TEMP_MODE; + break; + case TEMP_MODE: + infoMode = PHON_MODE; + break; + case PHON_MODE: + infoMode = DATE_MODE; + break; + case DATE_MODE: + default: + infoMode = BATT_MODE; + } +} + +function redraw() { + const date = new Date(); + + // Update timers + incrementTimer(); + + // Draw frame + drawBackground(); + drawFloor(); + drawPyramid(); + drawTrees(); + drawTime(date); + drawInfo(date); + drawCharacter(date); + drawNotice(); + drawCoin(); + + // Render new frame + g.flip(); +} + +function clearTimers(){ + if(intervalRef) { + clearInterval(intervalRef); + intervalRef = null; + } + + if(displayTimeoutRef) { + clearInterval(displayTimeoutRef); + displayTimeoutRef = null; + } +} + +function resetDisplayTimeout() { + if (displayTimeoutRef) clearInterval(displayTimeoutRef); + + displayTimeoutRef = setInterval(() => { + if (Bangle.isLCDOn()) Bangle.setLCDPower(false); + clearTimers(); + }, ONE_SECOND * timeout); +} + +function startTimers(){ + if(intervalRef) clearTimers(); + intervalRef = setInterval(redraw, 50); + + resetDisplayTimeout(); + + redraw(); +} + +function loadSettings() { + const settings = readSettings(); + if (!settings) return; + + if (settings.character) characterSprite.character = settings.character; + if (settings.nightMode) nightMode = settings.nightMode; + if (settings.brightness) { + brightness = settings.brightness; + Bangle.setLCDBrightness(brightness); + } +} + +function updateSettings() { + const newSettings = { + character: characterSprite.character, + nightMode: nightMode, + brightness: brightness, + }; + writeSettings(newSettings); +} + +function checkBatteryLevel() { + if (Bangle.isCharging()) return; + if (E.getBattery() > 10) return; + if (phone.message !== null) return; + + phoneNewMessage("lowBatt", "Warning, battery is low"); +} + +// Main +function init() { + loadSettings(); + + clearInterval(); + + // Initialise display + Bangle.setLCDMode("80x80"); + + // Store screen dimensions + W = g.getWidth(); + H = g.getHeight(); + + // Get Mario to jump! + setWatch(() => { + if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true; + resetDisplayTimeout(); + phoneClearMessage(); // Clear any phone messages and message timers + }, BTN3, {repeat: true}); + + // Close watch and load launcher app + setWatch(() => { + Bangle.setLCDMode(); + Bangle.showLauncher(); + }, BTN2, {repeat: false, edge: "falling"}); + + // Change info mode + setWatch(() => { + changeInfoMode(); + }, BTN1, {repeat: true}); + + Bangle.on('lcdPower', (on) => on ? startTimers() : clearTimers()); + + Bangle.on('faceUp', (up) => { + if (up && !Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDPower(true); + } + }); + + Bangle.on('swipe', (sDir) => { + resetDisplayTimeout(); + + switch(sDir) { + // Swipe right (1) - change character (on a loop) + case 1: + switchCharacter(); + break; + // Swipe left (-1) - change day/night mode (on a loop) + case -1: + default: + toggleNightMode(); + } + + updateSettings(); + }); + + // Phone connectivity + try { NRF.wake(); } catch (e) {} + + NRF.on('disconnect', () => { + phoneNewMessage(null, "Phone disconnected"); + }); + + NRF.on('connect', () => { + setTimeout(() => { + phoneOutbound({ t: "status", bat: E.getBattery() }); + }, ONE_SECOND * 2); + phoneNewMessage(null, "Phone connected"); + }); + + GB = (evt) => phoneInbound(evt); + + startTimers(); + + setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); + checkBatteryLevel(); +} + +// Initialise! +init(); diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index 6450ca2c6..2b7a4f990 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -4,3 +4,4 @@ 0.05: Adjust position of notification src text 0.06: Support background color 0.07: Auto-calculate height, and pad text down even when there's no title (so it stays on-screen) +0.08: Don't turn on screen during Quiet Mode diff --git a/apps/notify/README.md b/apps/notify/README.md index ee1bf9be0..11c493102 100644 --- a/apps/notify/README.md +++ b/apps/notify/README.md @@ -9,7 +9,7 @@ other applications or widgets to display messages. ```JS options = { - on : bool, // turn screen on, default true + on : bool, // turn screen on, default true (But not if Quiet Mode is enabled) size : int, // height of notification, default is fit to height (80 max) title : string, // optional title id // optional notification ID, used with hide() diff --git a/apps/notify/notify.js b/apps/notify/notify.js index 1373fc2bd..b5ef32d8b 100644 --- a/apps/notify/notify.js +++ b/apps/notify/notify.js @@ -127,7 +127,9 @@ exports.show = function(options) { options.render({x:x, y:y, w:w, h:h}); } - if (options.on) Bangle.setLCDPower(1); // light up + if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(1); // light up + } Bangle.setLCDMode(oldMode); // clears cliprect function anim() { diff --git a/apps/notifyfs/ChangeLog b/apps/notifyfs/ChangeLog index d4ea69cc8..974e138f7 100644 --- a/apps/notifyfs/ChangeLog +++ b/apps/notifyfs/ChangeLog @@ -5,3 +5,4 @@ 0.05: Fix `g` corruption issue if .hide gets called twice 0.06: Adjust position of notification src text and notifications without title 0.07: Support background color +0.08: Don't turn on screen during Quiet Mode diff --git a/apps/notifyfs/notify.js b/apps/notifyfs/notify.js index 0b73ad2d2..07801cedb 100644 --- a/apps/notifyfs/notify.js +++ b/apps/notifyfs/notify.js @@ -90,8 +90,9 @@ exports.show = function(options) { const area={x:x, y:y, w:w, h:h} options.render(area); } - - if (options.on) Bangle.setLCDPower(1); // light up + if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(1); // light up + } Bangle.on("touch", exports.hide); // Create a fake graphics to hide draw attempts oldg = g; @@ -115,9 +116,11 @@ exports.hide = function(options) { Bangle.removeListener("touch", exports.hide); g.clear(); Bangle.drawWidgets(); - // flipping the screen off then on often triggers a redraw - it may not! - Bangle.setLCDPower(0); - Bangle.setLCDPower(1); + if (Bangle.isLCDOn() || !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + // flipping the screen off then on often triggers a redraw - it may not! + Bangle.setLCDPower(0); + Bangle.setLCDPower(1); + } // hack for E.showMenu/showAlert/showPrompt - can force a redraw by faking next/back if (Bangle.btnWatches) { global["\xff"].watches[Bangle.btnWatches[0]].callback(); diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog new file mode 100644 index 000000000..7f837e50e --- /dev/null +++ b/apps/qmsched/ChangeLog @@ -0,0 +1 @@ +0.01: First version diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md new file mode 100644 index 000000000..1ccddbf8c --- /dev/null +++ b/apps/qmsched/README.md @@ -0,0 +1,5 @@ +# Quiet Mode Schedule + +Automatically turn Quiet Mode on or off at set times. + +![Main menu](screenshot_main.png) ![Edit Schedule menu](screenshot_edit.png) diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js new file mode 100644 index 000000000..7761be31c --- /dev/null +++ b/apps/qmsched/app.js @@ -0,0 +1,133 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const modeNames = ["Off", "Alarms", "Silent"]; +let scheds = require("Storage").readJSON("qmsched.json", 1); +/*scheds = [ + { hr : 6.5, // hours + minutes/60 + last : 0, // last day of the month we fired on - so we don't switch twice in one day! + mode : 1, // quiet mode (0/1/2) + } +];*/ +if (!scheds) { + // set default schedule on first load of app + scheds = [ + {"hr": 8, "mode": 0, "last": 25}, + {"hr": 22, "mode": 1, "last": 25}, + ]; + require("Storage").writeJSON("qmsched.json", scheds); +} + +function formatTime(t) { + const hrs = 0|t; + const mins = Math.round((t-hrs)*60); + return (" "+hrs).substr(-2)+":"+("0"+mins).substr(-2); +} + +function getCurrentHr() { + const time = new Date(); + return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); +} + +function showMainMenu() { + const menu = { + "": {"title": "Quiet Mode"}, + "Current Mode": { + value: (require("Storage").readJSON("setting.json", 1) || {}).quiet|0, + format: v => modeNames[v], + onchange: function(v) { + if (v<0) v = 2; + if (v>2) v = 0; + require("qmsched").setMode(v); + this.value = v; + }, + }, + }; + scheds.sort((a, b) => (a.hr-b.hr)); + scheds.forEach((sched, idx) => { + const name = modeNames[sched.mode]; + const txt = formatTime(sched.hr)+" ".repeat(14-name.length)+name; + menu[txt] = function() { + showEditMenu(idx); + }; + }); + menu["Add Schedule"] = () => showEditMenu(-1); + menu["< Back"] = () => {load();}; + return E.showMenu(menu); +} + +function showEditMenu(index) { + const isNew = index<0; + let hrs = 12, mins = 0; + let mode = 1; + if (!isNew) { + const s = scheds[index]; + hrs = 0|s.hr; + mins = Math.round((s.hr-hrs)*60); + mode = s.mode; + } + const menu = { + "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, + "Hours": { + value: hrs, + onchange: function(v) { + if (v<0) v = 23; + if (v>23) v = 0; + hrs = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + "Minutes": { + value: mins, + onchange: function(v) { + if (v<0) v = 59; + if (v>59) v = 0; + mins = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + "Switch to": { + value: mode, + format: v => modeNames[v], + onchange: function(v) { + if (v<0) v = 2; + if (v>2) v = 0; + mode = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + }; + function getSched() { + const hr = hrs+(mins/60); + let day = 0; + // If schedule is for tomorrow not today (eg, in the past), set day + if (hr Save"] = function() { + if (isNew) { + scheds.push(getSched()); + } else { + scheds[index] = getSched(); + } + require("Storage").writeJSON("qmsched.json", scheds); + showMainMenu(); + }; + if (!isNew) { + menu["> Delete"] = function() { + scheds.splice(index, 1); + require("Storage").writeJSON("qmsched.json", scheds); + showMainMenu(); + }; + } + menu["< Cancel"] = showMainMenu; + return E.showMenu(menu); +} + +showMainMenu(); diff --git a/apps/qmsched/app.png b/apps/qmsched/app.png new file mode 100644 index 000000000..cf1fc29bc Binary files /dev/null and b/apps/qmsched/app.png differ diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js new file mode 100644 index 000000000..3c53ef3f7 --- /dev/null +++ b/apps/qmsched/boot.js @@ -0,0 +1,24 @@ +// apply Quiet Mode schedules +(function qm() { + let scheds = require("Storage").readJSON("qmsched.json", 1) || []; + if (!scheds.length) return; + let next,idx; + scheds.forEach(function(s, i) { + if (!next || (s.hr+s.last*24)<(next.hr+next.last*24)) { + next = s; + idx = i; + } + }); + const now = new Date(), + hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); + let t = 3600000*(next.hr-hr); + if (next.last===now.getDate()) t += 86400000; + /* update quiet mode at the correct time. */ + setTimeout(function() { + let scheds = require("Storage").readJSON("qmsched.json", 1) || []; + require("qmsched").setMode(scheds[idx].mode); + scheds[idx].last = (new Date()).getDate(); + require("Storage").writeJSON("qmsched.json", scheds); + qm(); // schedule next update + }, t); +})(); diff --git a/apps/qmsched/icon.js b/apps/qmsched/icon.js new file mode 100644 index 000000000..c0f4e2b66 --- /dev/null +++ b/apps/qmsched/icon.js @@ -0,0 +1,2 @@ +// https://icons8.com/icon/19324/no-reminders +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AElksF1wwtF4YwO0WiGFguBGFovfGB3MAAgwnFooxfGBAuJGEguLGEV/F5owh0YvpGH4vhGCQvd0YwQF7vMGCAveGCAvfGB4vgGBwvhGBouhGFLkIGEouIGEwvKGBguiGEQuNGEHN5owa5ouQ53P5/O5wyOGA3NDAIbBLyAUCAAQzCNBQwF0gVDXiQoBGQgAEEIILE0iSJdiozCFQw1FGBJgSABSVIeg7wQGSDDMFyQ0VCQQwdAAWcAAwPHGD4vPGD+iAAwRJGEgRLGEQRNeTwARF1wA/AH4AX")) \ No newline at end of file diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js new file mode 100644 index 000000000..6cdf4f181 --- /dev/null +++ b/apps/qmsched/lib.js @@ -0,0 +1,17 @@ +/** + * Set new Quiet Mode and apply Bangle options + * @param {int} mode Quiet Mode + */ +exports.setMode = function(mode) { + let s = require("Storage").readJSON("setting.json", 1) || {}; + s.quiet = mode; + require("Storage").writeJSON("setting.json", s); + if (s.options) Bangle.setOptions(s.options); + if (mode && s.qmOptions) Bangle.setOptions(s.qmOptions); + if (mode && s.qmBrightness) { + if (s.qmBrightness!=1) Bangle.setLCDBrightness(s.qmBrightness); + } else { + if (s.brightness && s.brightness!=1) Bangle.setLCDBrightness(s.brightness); + } + if (mode && s.qmTimeout) Bangle.setLCDTimeout(s.qmTimeout); +}; \ No newline at end of file diff --git a/apps/qmsched/screenshot_edit.png b/apps/qmsched/screenshot_edit.png new file mode 100644 index 000000000..88b7fcad4 Binary files /dev/null and b/apps/qmsched/screenshot_edit.png differ diff --git a/apps/qmsched/screenshot_main.png b/apps/qmsched/screenshot_main.png new file mode 100644 index 000000000..634abd633 Binary files /dev/null and b/apps/qmsched/screenshot_main.png differ diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog index 3ac5530cd..61bf493c1 100644 --- a/apps/rclock/ChangeLog +++ b/apps/rclock/ChangeLog @@ -1,4 +1,5 @@ 0.01: First published version of app 0.02: Added support for locale and 12H clock 0.03: Added HR indication to clock -0.04: Update font size and alignment \ No newline at end of file +0.04: Update font size and alignment +0.05: Changes which circle show minutes and seconds \ No newline at end of file diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index f8b27c4fb..ceaffe910 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -23,29 +23,29 @@ // Ssettings const settings = { time: { - color: 0xD6ED17, + color: '#D6ED17', font: 'Vector', size: 60, middle: screen.middle, center: screen.center, }, date: { - color: 0xD6ED17, + color: '#D6ED17', font: 'Vector', size: 15, middle: screen.height-17, // at bottom of screen center: screen.center, }, circle: { - colormin: 0x606060, - colorsec: 0x656565, + colormin: '#ffffff', + colorsec: '#ffffff', width: 10, middle: screen.middle, center: screen.center, height: screen.height }, hr: { - color: 0x333333, + color: '#333333', size: 10, x: screen.center, y: screen.middle + 45 @@ -66,18 +66,6 @@ }; const drawMinArc = function (sections, color) { - g.setColor(color); - rad = (settings.circle.height / 2) - 20; - r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); - //g.setPixel(r[0],r[1]); - r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90); - //g.setPixel(r[0],r[1]); - g.drawLine(r1[0], r1[1], r2[0], r2[1]); - g.setColor('#333333'); - g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) - }; - - const drawSecArc = function (sections, color) { g.setColor(color); rad = (settings.circle.height / 2) - 40; r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); @@ -86,7 +74,19 @@ //g.setPixel(r[0],r[1]); g.drawLine(r1[0], r1[1], r2[0], r2[1]); g.setColor('#333333'); - g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4); + }; + + const drawSecArc = function (sections, color) { + g.setColor(color); + rad = (settings.circle.height / 2) - 20; + r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + g.drawLine(r1[0], r1[1], r2[0], r2[1]); + g.setColor('#333333'); + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4); }; const drawClock = function () { @@ -107,15 +107,13 @@ first = false; } - // Reset seconds + // Reset if (seconds == 59) { g.setColor('#000000'); - g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 40); - } - // Reset minutes - if (minutes == 59 && seconds == 59) { - g.setColor('#000000'); - g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 20); + g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2)); + for (count = 0; count <= minutes; count++) { + drawMinArc(count, settings.circle.colormin); + } } //Get date as a string diff --git a/apps/route/ChangeLog b/apps/route/ChangeLog index 5560f00bc..02779b6ea 100644 --- a/apps/route/ChangeLog +++ b/apps/route/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Change color from red->yellow to ease readability (fix #710) diff --git a/apps/route/custom.html b/apps/route/custom.html index 124c92a31..b958303d8 100644 --- a/apps/route/custom.html +++ b/apps/route/custom.html @@ -143,7 +143,7 @@ var currentDist = 0; function drawMap() { g.clearRect(0,0,239,120); g.setFontAlign(0,0); - g.setColor(1,0,0); + g.setColor(1,1,0); g.setFontVector(40); g.drawString((currentDist===undefined)?"?":(Math.round(currentDist)+"m"), 160, 30); g.setColor(1,1,1); @@ -151,7 +151,7 @@ function drawMap() { g.drawString(Math.round(totalDistance)+"m", 160, 70); g.drawString((nextPtIdx/2)+"/"+coordDistance.length, 50, 20); if (!fix.fix) { - g.setColor(1,0,0); + g.setColor(1,1,0); g.drawString("No GPS", 50, 50); g.setFont("6x8",1); g.drawString(fix.satellites+" Sats", 50, 70); @@ -161,17 +161,17 @@ function drawMap() { g.setColor(0,0,0); g.drawCircle(lastFix.s.x,lastFix.s.y,10); } - for (var i=0;i v ? "On" : "Off"; @@ -97,6 +109,7 @@ function showMainMenu() { } } }, + "Quiet Mode": ()=>showQuietModeMenu(), 'Locale': ()=>showLocaleMenu(), 'Select Clock': ()=>showClockMenu(), 'Set Time': ()=>showSetTimeMenu(), @@ -224,7 +237,9 @@ function showLCDMenu() { onchange: v => { settings.brightness = v || 1; updateSettings(); - Bangle.setLCDBrightness(settings.brightness); + if (!(settings.quiet && "qmBrightness" in settings)) { + Bangle.setLCDBrightness(settings.brightness); + } } }, 'LCD Timeout': { @@ -235,7 +250,9 @@ function showLCDMenu() { onchange: v => { settings.timeout = 0 | v; updateSettings(); - Bangle.setLCDTimeout(settings.timeout); + if (!(settings.quiet && "qmTimeout" in settings)) { + Bangle.setLCDTimeout(settings.timeout); + } } }, 'Wake on BTN1': { @@ -319,6 +336,104 @@ function showLCDMenu() { } return E.showMenu(lcdMenu) } +function showQuietModeMenu() { + // we always keep settings.quiet and settings.qmOptions + // other qm values are deleted when not set + const modes = ["Off", "Alarms", "Silent"]; + const qmDisabledFormat = v => v ? "Off" : "-"; + const qmMenu = { + "": {"title": "Quiet Mode"}, + "< Back": () => showMainMenu(), + "Quiet Mode": { + value: settings.quiet|0, + format: v => modes[v%3], + onchange: v => { + settings.quiet = v%3; + updateSettings(); + updateOptions(); + }, + }, + "LCD Brightness": { + value: settings.qmBrightness || 0, + min: 0, // 0 = use default + max: 1, + step: 0.1, + format: v => (v>0.05) ? v : "-", + onchange: v => { + if (v>0.05) { // prevent v=0.000000000000001 bugs + settings.qmBrightness = v; + } else { + delete settings.qmBrightness; + } + updateSettings(); + if (settings.qmBrightness) { // show result, even if not quiet right now + Bangle.setLCDBrightness(v); + } else { + Bangle.setLCDBrightness(settings.brightness); + } + }, + }, + "LCD Timeout": { + value: settings.qmTimeout || 0, + min: 0, // 0 = use default (no constant on for quiet mode) + max: 60, + step: 5, + format: v => v>1 ? v : "-", + onchange: v => { + if (v>1) { + settings.qmTimeout = v; + } else { + delete settings.qmTimeout; + } + updateSettings(); + if (settings.quiet && v>1) { + Bangle.setLCDTimeout(v); + } else { + Bangle.setLCDTimeout(settings.timeout); + } + }, + }, + // we disable wakeOn* events by overwriting them as false in qmOptions + // not disabled = not present in qmOptions at all + "Wake on FaceUp": { + value: "wakeOnFaceUp" in settings.qmOptions, + format: qmDisabledFormat, + onchange: () => { + if ("wakeOnFaceUp" in settings.qmOptions) { + delete settings.qmOptions.wakeOnFaceUp; + } else { + settings.qmOptions.wakeOnFaceUp = false; + } + updateOptions(); + }, + }, + "Wake on Touch": { + value: "wakeOnTouch" in settings.qmOptions, + format: qmDisabledFormat, + onchange: () => { + if ("wakeOnTouch" in settings.qmOptions) { + delete settings.qmOptions.wakeOnTouch; + } else { + settings.qmOptions.wakeOnTouch = false; + } + updateOptions(); + }, + }, + "Wake on Twist": { + value: "wakeOnTwist" in settings.qmOptions, + format: qmDisabledFormat, + onchange: () => { + if ("wakeOnTwist" in settings.qmOptions) { + delete settings.qmOptions.wakeOnTwist; + } else { + settings.qmOptions.wakeOnTwist = false; + } + updateOptions(); + }, + }, + }; + return E.showMenu(qmMenu); +} function showLocaleMenu() { const localemenu = { diff --git a/apps/setting/settings.min.json b/apps/setting/settings.min.json new file mode 100644 index 000000000..984054c11 --- /dev/null +++ b/apps/setting/settings.min.json @@ -0,0 +1 @@ +{"ble":true,"blerepl":true,"log":false,"timeout":10,"vibrate":true,"beep":"vib","timezone":0,"HID":false,"clock":null,"12hour":false,"brightness":1,"options":{"wakeOnBTN1":true,"wakeOnBTN2":true,"wakeOnBTN3":true,"wakeOnFaceUp":false,"wakeOnTouch":false,"wakeOnTwist":true,"twistThreshold":819.2,"twistMaxY":-800,"twistTimeout":1000}} \ No newline at end of file diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog index 5560f00bc..47448167e 100644 --- a/apps/sleepphasealarm/ChangeLog +++ b/apps/sleepphasealarm/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Respect Quiet Mode diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index 1f8bf92ae..0de0b9afc 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -88,6 +88,7 @@ function drawApp() { var buzzCount = 19; function buzz() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence Bangle.setLCDPower(1); Bangle.buzz().then(()=>{ if (buzzCount--) { diff --git a/apps/slidingtext/ChangeLog b/apps/slidingtext/ChangeLog index d53df991b..01e6b06c3 100644 --- a/apps/slidingtext/ChangeLog +++ b/apps/slidingtext/ChangeLog @@ -1 +1,2 @@ 0.01: Initial Release +0.02: Color Themes, Smoother scrolling diff --git a/apps/slidingtext/README.md b/apps/slidingtext/README.md index 00f716e4a..cc802638a 100644 --- a/apps/slidingtext/README.md +++ b/apps/slidingtext/README.md @@ -1,18 +1,34 @@ # Sliding Text Clock - See the time in different languages -Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently only English, French and Japanese are supported +Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Please use the upload page to choose which languages you want loaded. ![](app.png) ## Usage +### Button 1 + Use Button 1 (the top right button) to change the language +| English | English (Traditional) | French | Japanese (Romanji) | +| ---- | ---- | ---- | ---- | +| ![](./format-01.jpg) | ![](format-02.jpg) | ![](format-03.jpg) |![](format-04.jpg) | + +### Button 3 +Button 3 (bottom right button) is used to change the colour + +| Black | Red | Gray | Purple | +| ---- | ---- | ---- | ---- | +| ![](./color-01.jpg) | ![](color-02.jpg) | ![](color-03.jpg) | ![](color-04.jpg) | + +## Further Details + +For further details of design and working please visit [The Project Page](https://www.notion.so/adrianwkirk/Sliding-Text-Clock-a8fe556f03624a619656ddbc4f36f41b) ## Requests -[Reach out to Adrian](https://www.github.com/awkirk71) if you have feature requests or notice bugs. +Reach out to adrian@adriankirk.com if you have feature requests or notice bugs. ## Creator -Made by [Adrian Kirk](https://www.github.com/awkirk71). +Made by [Adrian Kirk](mailto:adrian@adriankirk.com) diff --git a/apps/slidingtext/color-01.jpg b/apps/slidingtext/color-01.jpg new file mode 100644 index 000000000..49efb0481 Binary files /dev/null and b/apps/slidingtext/color-01.jpg differ diff --git a/apps/slidingtext/color-02.jpg b/apps/slidingtext/color-02.jpg new file mode 100644 index 000000000..446491cc4 Binary files /dev/null and b/apps/slidingtext/color-02.jpg differ diff --git a/apps/slidingtext/color-03.jpg b/apps/slidingtext/color-03.jpg new file mode 100644 index 000000000..0b26419a5 Binary files /dev/null and b/apps/slidingtext/color-03.jpg differ diff --git a/apps/slidingtext/color-04.jpg b/apps/slidingtext/color-04.jpg new file mode 100644 index 000000000..385c42a90 Binary files /dev/null and b/apps/slidingtext/color-04.jpg differ diff --git a/apps/slidingtext/custom.html b/apps/slidingtext/custom.html new file mode 100644 index 000000000..8c9edada1 --- /dev/null +++ b/apps/slidingtext/custom.html @@ -0,0 +1,69 @@ + + + + + + +

Please select watch languages

+ + + + + + +
EnabledName
+ +

Click

+ + + + + + diff --git a/apps/slidingtext/format-01.jpg b/apps/slidingtext/format-01.jpg new file mode 100644 index 000000000..b8bc4552e Binary files /dev/null and b/apps/slidingtext/format-01.jpg differ diff --git a/apps/slidingtext/format-02.jpg b/apps/slidingtext/format-02.jpg new file mode 100644 index 000000000..c8b7a5e60 Binary files /dev/null and b/apps/slidingtext/format-02.jpg differ diff --git a/apps/slidingtext/format-03.jpg b/apps/slidingtext/format-03.jpg new file mode 100644 index 000000000..717153852 Binary files /dev/null and b/apps/slidingtext/format-03.jpg differ diff --git a/apps/slidingtext/format-04.jpg b/apps/slidingtext/format-04.jpg new file mode 100644 index 000000000..19b01fd64 Binary files /dev/null and b/apps/slidingtext/format-04.jpg differ diff --git a/apps/slidingtext/slidingtext.dtfmt.js b/apps/slidingtext/slidingtext.dtfmt.js new file mode 100644 index 000000000..865ea47e6 --- /dev/null +++ b/apps/slidingtext/slidingtext.dtfmt.js @@ -0,0 +1,15 @@ +class DateFormatter { + /** + * A pure virtual class which all the other date formatters will + * inherit from. + * The name will be used to declare the date format when selected + * and the date formatDate methid will return the time formated + * to the lines of text on the screen + */ + name(){return "no name";} + formatDate(date){ + return ["no","date","defined"]; + } +} + +module.exports = DateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.js b/apps/slidingtext/slidingtext.js index 267cef822..352484d2b 100644 --- a/apps/slidingtext/slidingtext.js +++ b/apps/slidingtext/slidingtext.js @@ -1,21 +1,102 @@ /** -* Adrian Kirk 2021-02 -* Sliding text clock inspired by the Pebble -* clock with the same name -*/ + * Adrian Kirk 2021-02 + * Sliding text clock inspired by the Pebble + * clock with the same name + */ +const color_schemes = [ + { + name: "black", + background : [0.0,0.0,0.0], + main_bar: [1.0,1.0,1.0], + other_bars: [0.85,0.85,0.85], + }, + { + name: "red", + background : [1.0,0.0,0.0], + main_bar: [1.0,1.0,0.0], + other_bars: [0.85,0.85,0.85] + }, + { + name: "grey", + background : [0.5,0.5,0.5], + main_bar: [1.0,1.0,1.0], + other_bars: [0.0,0.0,0.0], + }, + { + name: "purple", + background : [1.0,0.0,1.0], + main_bar: [1.0,1.0,0.0], + other_bars: [0.85,0.85,0.85] + }, + { + name: "blue", + background : [0.4,0.7,1.0], + main_bar: [1.0,1.0,1.0], + other_bars: [0.9,0.9,0.9] + } +]; + +let color_scheme_index = 0; + + +/** + * The Watch Display + */ + +function bg_color(){ + return color_schemes[color_scheme_index].background; +} + +function main_color(){ + return color_schemes[color_scheme_index].main_bar; +} + +function other_color(){ + return color_schemes[color_scheme_index].other_bars; +} + +let command_stack_high_priority = []; +let command_stack_low_priority = []; + +function next_command(){ + command = command_stack_high_priority.pop(); + if(command == null){ + //console.log("Low priority command"); + command = command_stack_low_priority.pop(); + } else { + //console.log("High priority command"); + } + if(command != null){ + command.call(); + } else { + //console.log("no command"); + } +} + +function reset_commands(){ + command_stack_high_priority = []; + command_stack_low_priority = []; +} + +function has_commands(){ + return command_stack_high_priority.length > 0 || + command_stack_low_priority.lenth > 0; +} class ShiftText { /** - * Class Responsible for shifting text around the screen - * - * This is a object that initializes itself with a position and - * text after which you can tell it where you want to move to - * using the moveTo method and it will smoothly move the text across - * at the selected frame rate and speed - */ + * Class Responsible for shifting text around the screen + * + * This is a object that initializes itself with a position and + * text after which you can tell it where you want to move to + * using the moveTo method and it will smoothly move the text across + * at the selected frame rate and speed + */ constructor(x,y,txt,font_name, - font_size,speed_x,speed_y,freq_millis, color){ + font_size,speed_x,speed_y,freq_millis, + color, + bg_color){ this.x = x; this.tgt_x = x; this.init_x = x; @@ -29,29 +110,44 @@ class ShiftText { this.speed_x = Math.abs(speed_x); this.speed_y = Math.abs(speed_y); this.freq_millis = freq_millis; - this.colour = color; + this.color = color; + this.bg_color = bg_color; this.finished_callback=null; this.timeoutId = null; } + setColor(color){ + this.color = color; + } + setBgColor(bg_color){ + this.bg_color = bg_color; + } reset(){ + //console.log("reset"); this.hide(); this.x = this.init_x; this.y = this.init_y; this.txt = this.init_txt; this.show(); if(this.timeoutId != null){ - clearTimeout(this.timeoutId); + clearTimeout(this.timeoutId); } } show() { g.setFont(this.font_name,this.font_size); - g.setColor(this.colour[0],this.colour[1],this.colour[2]); + g.setColor(this.color[0],this.color[1],this.color[2]); g.drawString(this.txt, this.x, this.y); } hide(){ g.setFont(this.font_name,this.font_size); - g.setColor(0,0,0); + //console.log("bgcolor:" + this.bg_color); + g.setColor(this.bg_color[0],this.bg_color[1],this.bg_color[2]); g.drawString(this.txt, this.x, this.y); + /*g.fillPoly([this.x - 1, this.y, + 240, this.y, + 240, this.y + this.font_size, + this.x -1 , this.y + this.font_size, + ]); + */ } setText(txt){ this.txt = txt; @@ -92,15 +188,15 @@ class ShiftText { this.finished_callback = finished_callback; } /** - * private internal method for directing the text move. - * It will see how far away we are from the target coords - * and move towards the target at the defined speed. - */ + * private internal method for directing the text move. + * It will see how far away we are from the target coords + * and move towards the target at the defined speed. + */ _doMove(){ this.hide(); // move closer to the target in the x direction - diff_x = this.tgt_x - this.x; - finished_x = false; + var diff_x = this.tgt_x - this.x; + var finished_x = false; if(Math.abs(diff_x) <= this.speed_x){ this.x = this.tgt_x; finished_x = true; @@ -112,8 +208,8 @@ class ShiftText { } } // move closer to the target in the y direction - diff_y = this.tgt_y - this.y; - finished_y = false; + var diff_y = this.tgt_y - this.y; + var finished_y = false; if(Math.abs(diff_y) <= this.speed_y){ this.y = this.tgt_y; finished_y = true; @@ -126,235 +222,90 @@ class ShiftText { } this.show(); this.timeoutId = null; - finished = finished_x & finished_y; + var finished = finished_x & finished_y; if(!finished){ this.timeoutId = setTimeout(this._doMove.bind(this), this.freq_millis); } else if(this.finished_callback != null){ + //console.log("finished - calling:" + this.finished_callback); this.finished_callback.call(); this.finished_callback = null; } } } -class DateFormatter { - /** - * A pure virtual class which all the other date formatters will - * inherit from. - * The name will be used to declare the date format when selected - * and the date formatDate methid will return the time formated - * to the lines of text on the screen - */ - name(){"no name";} - formatDate(date){ - return ["","",""]; - } -} - -/** -* English date formatting -*/ - -// English String Numbers -const numberStr = ["ZERO","ONE", "TWO", "THREE", "FOUR", "FIVE", - "SIX", "SEVEN","EIGHT", "NINE", "TEN", - "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", - "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", - "NINETEEN", "TWENTY"]; -const tensStr = ["ZERO", "TEN", "TWENTY", "THIRTY", "FOURTY", - "FIFTY"]; - -function hoursToText(hours){ - hours = hours % 12; - if(hours == 0){ - hours = 12; - } - return numberStr[hours]; -} - -function numberToText(value){ - word1 = ''; - word2 = ''; - if(value > 20){ - tens = (value / 10 | 0); - word1 = tensStr[tens]; - remainder = value - tens * 10; - if(remainder > 0){ - word2 = numberStr[remainder]; - } - } else if(value > 0) { - word1 = numberStr[value]; - } - return [word1,word2]; -} - -class EnglishDateFormatter extends DateFormatter{ - name(){return "English";} - formatDate(date){ - hours_txt = hoursToText(date.getHours()); - mins_txt = numberToText(date.getMinutes()); - return [hours_txt,mins_txt[0],mins_txt[1]]; - } -} - -/** -* French date formatting -*/ -const frenchNumberStr = [ "ZERO", "UNE", "DEUX", "TROIS", "QUATRE", - "CINQ", "SIX", "SEPT", "HUIT", "NEUF", "DIX", - "ONZE", "DOUZE", "TREIZE", "QUATORZE","QUINZE", - "SEIZE", "DIX SEPT", "DIX HUIT","DIX NEUF", "VINGT", - "VINGT ET UN", "VINGT DEUX", "VINGT TROIS", - "VINGT QUATRE", "VINGT CINQ", "VINGT SIX", - "VINGT SEPT", "VINGT HUIT", "VINGT NEUF" - ]; - -function frenchHoursToText(hours){ - hours = hours % 12; - if(hours == 0){ - hours = 12; - } - return frenchNumberStr[hours]; -} - -function frenchHeures(hours){ - if(hours % 12 == 1){ - return 'HEURE'; - } else { - return 'HEURES'; - } -} - -class FrenchDateFormatter extends DateFormatter { - constructor() { - super(); - } - name(){return "French";} - formatDate(date){ - hours = frenchHoursToText(date.getHours()); - heures = frenchHeures(date.getHours()); - mins = date.getMinutes(); - if(mins == 0){ - if(hours == 0){ - return ["MINUIT", "",""]; - } else if(hours == 12){ - return ["MIDI", "",""]; - } else { - return [hours, heures,""]; - } - } else if(mins == 30){ - return [hours, heures,'ET DEMIE']; - } else if(mins == 15){ - return [hours, heures,'ET QUERT']; - } else if(mins == 45){ - next_hour = date.getHours() + 1; - hours = frenchHoursToText(next_hour); - heures = frenchHeures(next_hour); - return [hours, heures,"MOINS",'LET QUERT']; - } - if(mins > 30){ - to_mins = 60-mins; - mins_txt = frenchNumberStr[to_mins]; - next_hour = date.getHours() + 1; - hours = frenchHoursToText(next_hour); - heures = frenchHeures(next_hour); - return [ hours, heures , "MOINS", mins_txt ]; - } else { - mins_txt = frenchNumberStr[mins]; - return [ hours, heures , mins_txt ]; - } - } -} - -/** -* Japanese date formatting -*/ -const japaneseHourStr = [ "ZERO", "ICHII", "NI", "SAN", "YO", - "GO", "ROKU", "SHICHI", "HACHI", "KU", "JUU", - 'JUU ICHI', 'JUU NI']; -const tensPrefixStr = [ "", - "JUU", - 'NIJUU', - 'SAN JUU', - 'YON JUU', - 'GO JUU']; - -const japaneseMinuteStr = [ ["", "PUN"], - ["IP","PUN" ], - ["NI", "FUN"], - ["SAN", "PUN"], - ["YON","FUN"], - ["GO", "HUN"], - ["RO", "PUN"], - ["NANA", "FUN"], - ["HAP", "PUN"], - ["KYU","FUN"], - ["JUP", "PUN"] - ]; - -function japaneseHoursToText(hours){ - hours = hours % 12; - if(hours == 0){ - hours = 12; - } - return japaneseHourStr[hours]; -} - -function japaneseMinsToText(mins){ - if(mins == 0){ - return ["",""]; - } else if(mins == 30) - return ["HAN",""]; - else { - units = mins % 10; - mins_txt = japaneseMinuteStr[units]; - tens = mins /10 | 0; - if(tens > 0){ - tens_txt = tensPrefixStr[tens]; - return [tens_txt + ' ' + mins_txt[0], mins_txt[1]]; - } else { - return [mins_txt[0], mins_txt[1]]; - } - } -} - -class JapaneseDateFormatter extends DateFormatter { - constructor() { - super(); - } - name(){return "Japanese (Romanji)";} - formatDate(date){ - hours_txt = japaneseHoursToText(date.getHours()); - mins_txt = japaneseMinsToText(date.getMinutes()); - return [hours_txt,"JI", mins_txt[0], mins_txt[1] ]; - } -} - -/** -* The Watch Display -*/ - -// a list of display rows -let row_displays = [ - new ShiftText(240,60,'',"Vector",40,10,10,40,[1,1,1]), - new ShiftText(240,100,'',"Vector",20,10,10,50,[0.85,0.85,0.85]), - new ShiftText(240,120,'',"Vector",20,10,10,60,[0.85,0.85,0.85]), - new ShiftText(240,140,'',"Vector",20,10,10,70,[0.85,0.85,0.85]) +const CLOCK_TEXT_SPEED_X = 10; +// a list of display rows +let row_displays = [ + new ShiftText(240,50,'',"Vector",40,CLOCK_TEXT_SPEED_X,1,10,main_color(),bg_color()), + new ShiftText(240,90,'',"Vector",30,CLOCK_TEXT_SPEED_X,1,10,other_color(),bg_color()), + new ShiftText(240,120,'',"Vector",30,CLOCK_TEXT_SPEED_X,1,10,other_color(),bg_color()), + new ShiftText(240,150,'',"Vector",30,CLOCK_TEXT_SPEED_X,1,10,other_color(),bg_color()), + new ShiftText(240,180,'',"Vector",40,CLOCK_TEXT_SPEED_X,1,10,main_color(),bg_color()) ]; -// a list of the formatters to cycle through -let date_formatters = [ - new EnglishDateFormatter(), - new FrenchDateFormatter(), - new JapaneseDateFormatter() - ]; +function nextColorTheme(){ + //console.log("next color theme"); + color_scheme_index += 1; + if(color_scheme_index >= row_displays.length){ + color_scheme_index = 0; + } + var color_scheme = color_schemes[color_scheme_index]; + setColor(color_scheme.main_bar, + color_scheme.other_bars, + color_scheme.background); + reset_clock(); + draw_clock(); +} + +function setColor(main_color,other_color,bg_color){ + row_displays[0].setColor(main_color); + row_displays[0].setBgColor(bg_color); + for(var i=1; i= date_formatters.length){ @@ -364,61 +315,204 @@ function changeFormatter(){ date_formatter = date_formatters[date_formatter_idx]; reset_clock(); draw_clock(); - // now announce the formatter by name - format_name_display.setTextYPosition(date_formatter.name(),-10); - format_name_display.moveToY(15); - // and then move back - format_name_display.onFinished( - function(){ - format_name_display.moveToY(-10); + command_stack_high_priority.unshift( + function() { + //console.log("move in new:" + txt); + // first select the top or bottom to display the formatter name + // We choose the first spare row without text + var format_name_display = row_displays[row_displays.length - 1]; + if (format_name_display.txt != '') { + format_name_display = row_displays[0]; + } + if (format_name_display.txt != ''){ + return; + } + format_name_display.speed_x = 3; + format_name_display.onFinished(function(){ + format_name_display.speed_x = CLOCK_TEXT_SPEED_X; + console.log("return speed to:" + format_name_display.speed_x) + next_command(); + }); + format_name_display.setTextXPosition(date_formatter.name(),220); + format_name_display.moveToX(-date_formatter.name().length * format_name_display.font_size); } - ); + ); + } + function reset_clock(){ //console.log("reset_clock"); - var i; - for (i = 0; i < row_displays.length; i++) { + for (var i = 0; i < row_displays.length; i++) { + row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; row_displays[i].reset(); } + reset_commands(); } +let last_draw_time = null; +const next_minute_boundary_secs = 7.5; + function draw_clock(){ - //console.log("draw_clock"); - date = new Date(); - rows = date_formatter.formatDate(date); - var i; - for (i = 0; i < rows.length; i++) { + var date = new Date(); + if(last_draw_time != null && + date.getTime() - last_draw_time.getTime() < next_minute_boundary_secs * 1000 && + has_commands() ){ + console.log("skipping draw clock"); + return; + } else { + last_draw_time = date; + } + reset_commands(); + console.log("draw_clock:" + date.toISOString()); + // we don't want the time to be displayed + // and then immediately be trigger another time + if(date.getSeconds() > 60 - next_minute_boundary_secs){ + console.log("forwarding to next minute"); + date = new Date(date.getTime() + next_minute_boundary_secs * 1000); + } + //date.setMinutes(37); + var rows = date_formatter.formatDate(date); + var display; + for (var i = 0; i < rows.length; i++) { display = row_displays[i]; - txt = rows[i]; + var txt = rows[i]; + //console.log(i + "->" + txt); display_row(display,txt); } - // If the dateformatter has not returned enough + // If the dateformatter has not returned enough // rows then treat the reamining rows as empty - for (j = i; j < row_displays.length; j++) { + for (var j = i; j < row_displays.length; j++) { display = row_displays[j]; + //console.log(i + "->''(empty)"); display_row(display,''); } + next_command(); //console.log(date); } function display_row(display,txt){ - if(display.txt == ''){ - display.setTextXPosition(txt,240); - display.moveToX(20); - } else if(txt != display.txt){ - display.moveToX(-100); - display.onFinished( - function(){ - display.setTextXPosition(txt,240); - display.moveToX(20); - } + if(display == null) { + console.log("no display for text:" + txt) + return; + } + + if(display.txt == null || display.txt == ''){ + if(txt != '') { + command_stack_high_priority.unshift( + function () { + //console.log("move in new:" + txt); + display.onFinished(next_command); + display.setTextXPosition(txt, 240); + display.moveToX(20); + } + ); + } + } else if(txt != display.txt && display.txt != null){ + command_stack_high_priority.push( + function(){ + //console.log("move out:" + txt); + display.onFinished(next_command); + display.moveToX(-display.txt.length * display.font_size); + } + ); + command_stack_low_priority.push( + function(){ + //console.log("move in:" + txt); + display.onFinished(next_command); + display.setTextXPosition(txt,240); + display.moveToX(20); + } ); } else { - display.setTextXPosition(txt,20); + command_stack_high_priority.push( + function(){ + //console.log("move in2:" + txt); + display.setTextXPosition(txt,20); + next_command(); + } + ); } } +/** + * called from load_settings on startup to + * set the color scheme to named value + */ +function set_colorscheme(colorscheme_name){ + console.log("setting color scheme:" + colorscheme_name); + for (var i=0; i < color_schemes.length; i++) { + if(color_schemes[i].name == colorscheme_name){ + color_scheme_index = i; + console.log("match"); + var color_scheme = color_schemes[color_scheme_index]; + setColor(color_scheme.main_bar, + color_scheme.other_bars, + color_scheme.background); + break; + } + } +} + +function set_dateformat(dateformat_name){ + console.log("setting date format:" + dateformat_name); + for (var i=0; i < date_formatters.length; i++) { + if(date_formatters[i].name() == dateformat_name){ + date_formatter_idx = i; + date_formatter = date_formatters[date_formatter_idx]; + console.log("match"); + } + } +} + +const PREFERENCE_FILE = "slidingtext.settings.json"; +/** + * Called on startup to set the watch to the last preference settings + */ +function load_settings(){ + try{ + settings = require("Storage").readJSON(PREFERENCE_FILE); + if(settings != null){ + console.log("loaded:" + JSON.stringify(settings)); + if(settings.color_scheme != null){ + set_colorscheme(settings.color_scheme); + } + if(settings.date_format != null){ + set_dateformat(settings.date_format); + } + } else { + console.log("no settings to load"); + } + } catch(e){ + console.log("failed to load settings:" + e); + } +} + +/** + * Called on button press to save down the last preference settings + */ +function save_settings(){ + var settings = { + date_format : date_formatter.name(), + color_scheme : color_schemes[color_scheme_index].name, + }; + console.log("saving:" + JSON.stringify(settings)); + require("Storage").writeJSON(PREFERENCE_FILE,settings); +} + +function button1pressed() { + changeFormatter(); + save_settings(); +} + +function button3pressed() { + console.log("button3pressed"); + nextColorTheme(); + reset_clock(); + draw_clock(); + save_settings(); +} + // The interval reference for updating the clock let intervalRef = null; @@ -430,9 +524,9 @@ function clearTimers(){ } function startTimers(){ - let date = new Date(); - let secs = date.getSeconds(); - let nextMinuteStart = 60 - secs; + var date = new Date(); + var secs = date.getSeconds(); + var nextMinuteStart = 60 - secs; //console.log("scheduling clock draw in " + nextMinuteStart + " seconds"); setTimeout(scheduleDrawClock,nextMinuteStart * 1000); draw_clock(); @@ -457,6 +551,7 @@ Bangle.on('lcdPower', (on) => { clearTimers(); } }); + Bangle.on('faceUp',function(up){ //console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); if (up && !Bangle.isLCDOn()) { @@ -467,9 +562,17 @@ Bangle.on('faceUp',function(up){ }); g.clear(); +load_settings(); Bangle.loadWidgets(); Bangle.drawWidgets(); + startTimers(); // Show launcher when middle button pressed setWatch(Bangle.showLauncher, BTN2,{repeat:false,edge:"falling"}); -setWatch(changeFormatter, BTN1,{repeat:true,edge:"falling"}); + + +// Handle button 1 being pressed +setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"}); + +// Handle button 3 being pressed +setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"}); diff --git a/apps/slidingtext/slidingtext.locale.en.js b/apps/slidingtext/slidingtext.locale.en.js new file mode 100644 index 000000000..7d37fcae1 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.en.js @@ -0,0 +1,15 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); +const hoursToText = require("slidingtext.utils.en.js").hoursToText; +const numberToText = require("slidingtext.utils.en.js").numberToText; + +class EnglishDateFormatter extends DateFormatter { + constructor() { super();} + name(){return "English";} + formatDate(date){ + var hours_txt = hoursToText(date.getHours()); + var mins_txt = numberToText(date.getMinutes()); + return [hours_txt,mins_txt[0],mins_txt[1]]; + } +} + +module.exports = EnglishDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.en2.js b/apps/slidingtext/slidingtext.locale.en2.js new file mode 100644 index 000000000..cd07e8848 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.en2.js @@ -0,0 +1,53 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); +const hoursToText = require("slidingtext.utils.en.js").hoursToText; +const numberToText = require("slidingtext.utils.en.js").numberToText; + +class EnglishTraditionalDateFormatter extends DateFormatter { + constructor() { + super(); + } + name(){return "English (Traditional)";} + formatDate(date){ + var mins = date.getMinutes(); + var hourOfDay = date.getHours(); + if(mins > 30){ + hourOfDay += 1; + } + var hours = hoursToText(hourOfDay); + // Deal with the special times first + if(mins == 0){ + return [hours,"", "O'","CLOCK"]; + } else if(mins == 30){ + return ["","HALF", "PAST", "", hours]; + } else if(mins == 15){ + return ["","QUARTER", "PAST", "", hours]; + } else if(mins == 45) { + return ["", "QUARTER", "TO", "", hours]; + } + var mins_txt; + var from_to; + var mins_value; + if(mins > 30){ + mins_value = 60-mins; + from_to = "TO"; + mins_txt = numberToText(mins_value); + } else { + mins_value = mins; + from_to = "PAST"; + mins_txt = numberToText(mins_value); + } + if(mins_txt[1] != '') { + return ['', mins_txt[0], mins_txt[1], from_to, hours]; + } else { + if(mins_value % 5 == 0) { + return ['', mins_txt[0], from_to, '', hours]; + } else if(mins_value == 1){ + return ['', mins_txt[0], 'MINUTE', from_to, hours]; + } else { + return ['', mins_txt[0], 'MINUTES', from_to, hours]; + } + } + } +} + +module.exports = EnglishTraditionalDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.fr.js b/apps/slidingtext/slidingtext.locale.fr.js new file mode 100644 index 000000000..3cdfe9de1 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.fr.js @@ -0,0 +1,70 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); + +/** + * French date formatting + */ +const frenchNumberStr = [ "ZERO", "UNE", "DEUX", "TROIS", "QUATRE", + "CINQ", "SIX", "SEPT", "HUIT", "NEUF", "DIX", + "ONZE", "DOUZE", "TREIZE", "QUATORZE","QUINZE", + "SEIZE", "DIX SEPT", "DIX HUIT","DIX NEUF", "VINGT", + "VINGT ET UN", "VINGT DEUX", "VINGT TROIS", + "VINGT QUATRE", "VINGT CINQ", "VINGT SIX", + "VINGT SEPT", "VINGT HUIT", "VINGT NEUF" +]; + +function frenchHoursToText(hours){ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return frenchNumberStr[hours]; +} + +function frenchHeures(hours){ + if(hours % 12 == 1){ + return 'HEURE'; + } else { + return 'HEURES'; + } +} + +class FrenchDateFormatter extends DateFormatter { + constructor() { super(); } + name(){return "French";} + formatDate(date){ + var hours = frenchHoursToText(date.getHours()); + var heures = frenchHeures(date.getHours()); + var mins = date.getMinutes(); + if(mins == 0){ + if(hours == 0){ + return ["MINUIT", "",""]; + } else if(hours == 12){ + return ["MIDI", "",""]; + } else { + return [hours, heures,""]; + } + } else if(mins == 30){ + return [hours, heures,'ET DEMIE']; + } else if(mins == 15){ + return [hours, heures,'ET QUERT']; + } else if(mins == 45){ + var next_hour = date.getHours() + 1; + hours = frenchHoursToText(next_hour); + heures = frenchHeures(next_hour); + return [hours, heures,"MOINS",'LET QUERT']; + } + if(mins > 30){ + var to_mins = 60-mins; + var mins_txt = frenchNumberStr[to_mins]; + next_hour = date.getHours() + 1; + hours = frenchHoursToText(next_hour); + heures = frenchHeures(next_hour); + return [ hours, heures , "MOINS", mins_txt ]; + } else { + mins_txt = frenchNumberStr[mins]; + return [ hours, heures , mins_txt ]; + } + } +} + +module.exports = FrenchDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.jp.js b/apps/slidingtext/slidingtext.locale.jp.js new file mode 100644 index 000000000..c28780e88 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.jp.js @@ -0,0 +1,71 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); + +/** + * Japanese date formatting + */ +const japaneseHourStr = [ "ZERO", "ICHII", "NI", "SAN", "YO", + "GO", "ROKU", "SHICHI", "HACHI", "KU", "JUU", + 'JUU ICHI', 'JUU NI']; +const tensPrefixStr = [ "", + "JUU", + 'NIJUU', + 'SAN JUU', + 'YON JUU', + 'GO JUU']; + +const japaneseMinuteStr = [ ["", "PUN"], + ["IP","PUN" ], + ["NI", "FUN"], + ["SAN", "PUN"], + ["YON","FUN"], + ["GO", "HUN"], + ["RO", "PUN"], + ["NANA", "FUN"], + ["HAP", "PUN"], + ["KYU","FUN"], + ["JUP", "PUN"] +]; + +function japaneseHoursToText(hours){ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return japaneseHourStr[hours]; +} + +function japaneseMinsToText(mins){ + if(mins == 0){ + return ["",""]; + } else if(mins == 30) + return ["HAN",""]; + else { + var units = mins % 10; + var mins_txt = japaneseMinuteStr[units]; + var tens = mins /10 | 0; + if(tens > 0){ + var tens_txt = tensPrefixStr[tens]; + var minutes_txt; + if(mins_txt[0] != ''){ + minutes_txt = mins_txt[0] + ' ' + mins_txt[1]; + } else { + minutes_txt = mins_txt[1]; + } + return [tens_txt, minutes_txt]; + } else { + return [mins_txt[0], mins_txt[1]]; + } + } +} + +class JapaneseDateFormatter extends DateFormatter { + constructor() { super(); } + name(){return "Japanese (Romanji)";} + formatDate(date){ + var hours_txt = japaneseHoursToText(date.getHours()); + var mins_txt = japaneseMinsToText(date.getMinutes()); + return [hours_txt,"JI", mins_txt[0], mins_txt[1] ]; + } +} + +module.exports = JapaneseDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.utils.en.js b/apps/slidingtext/slidingtext.utils.en.js new file mode 100644 index 000000000..a91fcbd16 --- /dev/null +++ b/apps/slidingtext/slidingtext.utils.en.js @@ -0,0 +1,34 @@ +const numberStr = ["ZERO","ONE", "TWO", "THREE", "FOUR", "FIVE", + "SIX", "SEVEN","EIGHT", "NINE", "TEN", + "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", + "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", + "NINETEEN", "TWENTY"]; +const tensStr = ["ZERO", "TEN", "TWENTY", "THIRTY", "FOURTY", + "FIFTY"]; + +const hoursToText = (hours)=>{ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return numberStr[hours]; +} + +const numberToText = (value)=> { + var word1 = ''; + var word2 = ''; + if(value > 20){ + var tens = (value / 10 | 0); + word1 = tensStr[tens]; + var remainder = value - tens * 10; + if(remainder > 0){ + word2 = numberStr[remainder]; + } + } else if(value > 0) { + word1 = numberStr[value]; + } + return [word1,word2]; +} + +exports.hoursToText = hoursToText; +exports.numberToText = numberToText; \ No newline at end of file diff --git a/apps/sweepclock/ChangeLog b/apps/sweepclock/ChangeLog index d53df991b..d5cf3753c 100644 --- a/apps/sweepclock/ChangeLog +++ b/apps/sweepclock/ChangeLog @@ -1 +1,2 @@ 0.01: Initial Release +0.02: Added Colour Themes diff --git a/apps/sweepclock/README.md b/apps/sweepclock/README.md index 51920f8f1..8ddb7decb 100644 --- a/apps/sweepclock/README.md +++ b/apps/sweepclock/README.md @@ -6,7 +6,24 @@ The Sweep Clock provides a clock with a perfectly smooth sweep second hand with ## Usage -Use Button 1 (the top right button) to change the numeral types (currently European and Roman) +### Button 1 + +Use Button 1 (the top right button) to change the numeral type + +| Default clock face | Roman Numeral Font | No Digits | +| ---- | ---- | ---- | +| ![](./numeral-01.jpg) | ![](numeral-02.jpg) | ![](numeral-03.jpg) | + + + +### Button 3 +Button 3 (bottom right button) is used to change the colour + +| Red | Grey | Purple | +| ---- | ---- | ---- | +| ![](./color-01.jpg) | ![](color-02.jpg) | ![](color-03.jpg) | + + ## Further Details @@ -14,8 +31,8 @@ For further details of design and working please visit [The Project Page](https: ## Requests -[Reach out to Adrian](https://www.github.com/awkirk71) if you have feature requests or notice bugs. +Reach out to adrian@adriankirk.com if you have feature requests or notice bugs. ## Creator -Made by [Adrian Kirk](https://www.github.com/awkirk71). +Made by [Adrian Kirk](mailto:adrian@adriankirk.com) diff --git a/apps/sweepclock/app.png b/apps/sweepclock/app.png index 1310ca904..763c00b1d 100644 Binary files a/apps/sweepclock/app.png and b/apps/sweepclock/app.png differ diff --git a/apps/sweepclock/color-01.jpg b/apps/sweepclock/color-01.jpg new file mode 100644 index 000000000..fe937a7d8 Binary files /dev/null and b/apps/sweepclock/color-01.jpg differ diff --git a/apps/sweepclock/color-02.jpg b/apps/sweepclock/color-02.jpg new file mode 100644 index 000000000..ce798dc99 Binary files /dev/null and b/apps/sweepclock/color-02.jpg differ diff --git a/apps/sweepclock/color-03.jpg b/apps/sweepclock/color-03.jpg new file mode 100644 index 000000000..5bcc124ec Binary files /dev/null and b/apps/sweepclock/color-03.jpg differ diff --git a/apps/sweepclock/color-04.jpg b/apps/sweepclock/color-04.jpg new file mode 100644 index 000000000..c89f26b26 Binary files /dev/null and b/apps/sweepclock/color-04.jpg differ diff --git a/apps/sweepclock/numeral-01.jpg b/apps/sweepclock/numeral-01.jpg new file mode 100644 index 000000000..423af90dc Binary files /dev/null and b/apps/sweepclock/numeral-01.jpg differ diff --git a/apps/sweepclock/numeral-02.jpg b/apps/sweepclock/numeral-02.jpg new file mode 100644 index 000000000..603b0a6ee Binary files /dev/null and b/apps/sweepclock/numeral-02.jpg differ diff --git a/apps/sweepclock/numeral-03.jpg b/apps/sweepclock/numeral-03.jpg new file mode 100644 index 000000000..98af2e407 Binary files /dev/null and b/apps/sweepclock/numeral-03.jpg differ diff --git a/apps/sweepclock/sweepclock-icon.js b/apps/sweepclock/sweepclock-icon.js index 20a114caa..d9bdd8c65 100644 --- a/apps/sweepclock/sweepclock-icon.js +++ b/apps/sweepclock/sweepclock-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("lEowkE/4AdmU/CaMgCaUQCaPzgQmR+UDCaPxCaUwj525gJ2/CZ0vMSJ2SkEACaCJBgEPRKEAgJ3Q+cxE6Myn8yO6EggMjMSR3Q+ASBgCdRO4KJQCaMwCaPzCQQTPEwYTOJoYTO+cQCaKHCAAizLkEQYgQTF+YTHmMAkITEj//l8wl4UHkcwiIRBAQMD/8/CZKLBiUAiEigMCBAMzCwMyPI8zPQMzgMBn/yh/zgZSICoMxn5kC+ZmBPhY3CHAIAbA")) +require("heatshrink").decompress(atob("lEowkA/4AGmYIHABHzmVCCaE0kUin4TPmUimQTQ+UzmcvJ6EjCaP/kYABCaEymYTl+Q7SMgITTmQTQPAK0RMgITm+QTS+ciPCcikQpPY4MjmYTO+czmcyHh4TCmcvJ54nCPCBjBJx4oECc8zJ6ATTn48RE4YTTHh4SDH4ImRFBwTGFBgTGFBgSGFBYmHUgITRmcyFBASFAoUjE5PzkQLBHJxiDAQP/GxAA==")) diff --git a/apps/sweepclock/sweepclock.js b/apps/sweepclock/sweepclock.js index 286e0bf8e..9c53efa55 100644 --- a/apps/sweepclock/sweepclock.js +++ b/apps/sweepclock/sweepclock.js @@ -5,10 +5,55 @@ */ const screen_center_x = g.getWidth()/2; -const screen_center_y = g.getHeight()/2; +const screen_center_y = 10 + g.getHeight()/2; require("FontCopasetic40x58Numeric").add(Graphics); +const color_schemes = [ + { + name: "black", + background : [0.0,0.0,0.0], + second_hand: [1.0,0.0,0.0], + minute_hand: [1.0,1.0,1.0], + hour_hand: [1.0,1.0,1.0], + numeral:[1.0,1.0,1.0] + }, + { + name: "red", + background : [1.0,0.0,0.0], + second_hand: [1.0,1.0,0.0], + minute_hand: [1.0,1.0,1.0], + hour_hand: [1.0,1.0,1.0], + numeral:[1.0,1.0,1.0] + }, + { + name: "grey", + background : [0.5,0.5,0.5], + second_hand: [0.0,0.0,0.0], + minute_hand: [1.0,1.0,1.0], + hour_hand: [1.0,1.0,1.0], + numeral:[1.0,1.0,1.0] + }, + { + name: "purple", + background : [1.0,0.0,1.0], + second_hand: [1.0,1.0,0.0], + minute_hand: [1.0,1.0,1.0], + hour_hand: [1.0,1.0,1.0], + numeral:[1.0,1.0,1.0] + }, + { + name: "blue", + background : [0.4,0.7,1.0], + second_hand: [0.5,0.5,0.5], + minute_hand: [1.0,1.0,1.0], + hour_hand: [1.0,1.0,1.0], + numeral:[1.0,1.0,1.0] + } + ]; + +let color_scheme_index = 0; + class Hand { /** * Pure virtual class for all Hand classes to extend. @@ -28,16 +73,12 @@ class ThinHand extends Hand { length, tolerance, draw_test, - red, - green, - blue){ + color_theme){ super(); this.centerX = centerX; this.centerY = centerY; this.length = length; - this.red = red; - this.green = green; - this.blue = blue; + this.color_theme = color_theme; // The last x and y coordinates (not the centre) of the last draw this.last_x = centerX; this.last_y = centerY; @@ -60,13 +101,14 @@ class ThinHand extends Hand { // and then call the predicate to see if a redraw is needed this.draw_test(this.angle,this.last_draw_time) ){ // rub out the old hand line - g.setColor(0,0,0); + background = color_schemes[color_scheme_index].background; + g.setColor(background[0],background[1],background[2]); g.drawLine(this.centerX, this.centerY, this.last_x, this.last_y); // Now draw the new hand line - g.setColor(this.red,this.green,this.blue); + hand_color = color_schemes[color_scheme_index][this.color_theme]; + g.setColor(hand_color[0],hand_color[1],hand_color[2]); x2 = this.centerX + this.length*Math.sin(angle); y2 = this.centerY - this.length*Math.cos(angle); - g.setColor(this.red,this.green,this.blue); g.drawLine(this.centerX, this.centerY, x2, y2); // and store the last draw details for the next call this.last_x = x2; @@ -90,18 +132,14 @@ class ThickHand extends Hand { length, tolerance, draw_test, - red, - green, - blue, + color_theme, base_height, thickness){ super(); this.centerX = centerX; this.centerY = centerY; this.length = length; - this.red = red; - this.green = green; - this.blue = blue; + this.color_theme = color_theme; this.thickness = thickness; this.base_height = base_height; // angle from the center to the top corners of the rectangle @@ -132,7 +170,8 @@ class ThickHand extends Hand { // method to move the hand to a new angle moveTo(angle){ if(Math.abs(angle - this.angle) > this.tolerance || this.draw_test(this.angle - this.delta_base,this.angle + this.delta_base ,this.last_draw_time) ){ - g.setColor(0,0,0); + background = color_schemes[color_scheme_index].background; + g.setColor(background[0],background[1],background[2]); g.fillPoly([this.last_x1, this.last_y1, this.last_x2, @@ -142,7 +181,6 @@ class ThickHand extends Hand { this.last_x4, this.last_y4 ]); - g.setColor(this.red,this.green,this.blue); // bottom left x1 = this.centerX + this.vertex_radius_base*Math.sin(angle - this.delta_base); @@ -157,7 +195,8 @@ class ThickHand extends Hand { // top left x4 = this.centerX + this.vertex_radius_top*Math.sin(angle - this.delta_top); y4 = this.centerY - this.vertex_radius_top*Math.cos(angle - this.delta_top); - g.setColor(this.red,this.green,this.blue); + hand_color = color_schemes[color_scheme_index][this.color_theme]; + g.setColor(hand_color[0],hand_color[1],hand_color[2]); g.fillPoly([x1,y1, x2,y2, x3,y3, @@ -184,10 +223,10 @@ let force_redraw = false; // The seconds hand is the main focus and is set to redraw on every cycle let seconds_hand = new ThinHand(screen_center_x, screen_center_y, - 100, + 95, 0, (angle, last_draw_time) => false, - 1.0,0.0,0.0); + "second_hand"); // The minute hand is set to redraw at a 250th of a circle, // when the second hand is ontop or slighly overtaking // or when a force_redraw is called @@ -201,7 +240,7 @@ let minutes_hand = new ThinHand(screen_center_x, 80, 2*Math.PI/250, minutes_hand_redraw, - 1.0,1.0,1.0); + "minute_hand"); // The hour hand is a thick hand so we have to redraw when the minute hand // overlaps from its behind andle coverage to its ahead angle coverage. let hour_hand_redraw = function(angle_from, angle_to, last_draw_time){ @@ -214,12 +253,13 @@ let hours_hand = new ThickHand(screen_center_x, 40, 2*Math.PI/600, hour_hand_redraw, - 1.0,1.0,1.0, + "hour_hand", 5, 4); function draw_clock(){ date = new Date(); + draw_background(); draw_hour_digit(date); draw_seconds(date); draw_mins(date); @@ -242,7 +282,7 @@ function draw_mins(date,seconds_angle){ mins_angle = 2*Math.PI*mins_frac; redraw = minutes_hand.moveTo(mins_angle); if(redraw){ - console.log("redraw mins"); + //console.log("redraw mins"); } } @@ -252,7 +292,7 @@ function draw_hours(date){ hours_angle = 2*Math.PI*hours_frac; redraw = hours_hand.moveTo(hours_angle); if(redraw){ - console.log("redraw hours"); + //console.log("redraw hours"); } } @@ -275,6 +315,18 @@ class NumeralFont { * method to draw text at the required coordinates */ draw(hour_txt,x,y){ return "";} + /** + * Called from the settings loader to identify the font + */ + getName(){return "";} +} + +class NoFont extends NumeralFont{ + constructor(){super();} + getDimensions(hour){return [0,0];} + hour_txt(hour){ return ""; } + draw(hour_txt,x,y){ return "";} + getName(){return "NoFont";} } class CopasetFont extends NumeralFont{ @@ -314,6 +366,7 @@ class CopasetFont extends NumeralFont{ g.setFontCopasetic40x58Numeric(); g.drawString(hour_txt,x,y); } + getName(){return "Copaset";} } @@ -358,6 +411,7 @@ class RomanNumeralFont extends NumeralFont{ g.setFont("Vector",40); g.drawString(hour_txt,x,y); } + getName(){return "Roman";} } // The problem with the trig inverse functions on @@ -419,11 +473,12 @@ class HourScriber { drawHour(hours){ changed = false; if(this.curr_hours != hours || this.curr_numeral_font !=this.numeral_font){ - g.setColor(0,0,0); + background = color_schemes[color_scheme_index].background; + g.setColor(background[0],background[1],background[2]); this.curr_numeral_font.draw(this.curr_hour_str, this.curr_hour_x, this.curr_hour_y); - console.log("erasing old hour"); + //console.log("erasing old hour"); hours_frac = hours / 12; angle = 2*Math.PI*hours_frac; dimensions = this.numeral_font.getDimensions(hours); @@ -477,15 +532,16 @@ class HourScriber { } if(changed || this.draw_test(this.angle_from, this.angle_to, this.last_draw_time) ){ - g.setColor(1,1,1); + numeral_color = color_schemes[color_scheme_index].numeral; + g.setColor(numeral_color[0],numeral_color[1],numeral_color[2]); this.numeral_font.draw(this.curr_hour_str,this.curr_hour_x,this.curr_hour_y); this.last_draw_time = new Date(); - console.log("redraw digit"); + //console.log("redraw digit"); } } } -let numeral_fonts = [new CopasetFont(), new RomanNumeralFont()]; +let numeral_fonts = [new CopasetFont(), new RomanNumeralFont(), new NoFont()]; let numeral_fonts_index = 0; /** * predicate for deciding when the digit has to be redrawn @@ -540,7 +596,93 @@ function draw_hour_digit(date){ hour_scriber.drawHour(hours); } -// Boiler plate code for setting up the clock +function draw_background(){ + if(force_redraw){ + background = color_schemes[color_scheme_index].background; + g.setColor(background[0],background[1],background[2]); + g.fillPoly([0,25, + 0,240, + 240,240, + 240,25 + ]); + } +} + +function next_colorscheme(){ + color_scheme_index += 1; + color_scheme_index = color_scheme_index % color_schemes.length; + //console.log("color_scheme_index=" + color_scheme_index); + force_redraw = true; +} + +/** +* called from load_settings on startup to +* set the color scheme to named value +*/ +function set_colorscheme(colorscheme_name){ + console.log("setting color scheme:" + colorscheme_name); + for (var i=0; i < color_schemes.length; i++) { + if(color_schemes[i].name == colorscheme_name){ + color_scheme_index = i; + force_redraw = true; + console.log("match"); + break; + } + } +} + +/** +* called from load_settings on startup +* to set the font to named value +*/ +function set_font(font_name){ + console.log("setting font:" + font_name); + for (var i=0; i < numeral_fonts.length; i++) { + if(numeral_fonts[i].getName() == font_name){ + numeral_fonts_index = i; + force_redraw = true; + console.log("match"); + hour_scriber.setNumeralFont(numeral_fonts[numeral_fonts_index]); + break; + } + } +} + +/** +* Called on startup to set the watch to the last preference settings +*/ +function load_settings(){ + try{ + settings = require("Storage").readJSON("sweepclock.settings.json"); + if(settings != null){ + console.log("loaded:" + JSON.stringify(settings)); + if(settings.color_scheme != null){ + set_colorscheme(settings.color_scheme); + } + if(settings.font != null){ + set_font(settings.font); + } + } else { + console.log("no settings to load"); + } + } catch(e){ + console.log("failed to load settings:" + e); + } +} + +/** +* Called on button press to save down the last preference settings +*/ +function save_settings(){ + settings = { + font : numeral_fonts[numeral_fonts_index].getName(), + color_scheme : color_schemes[color_scheme_index].name, + }; + console.log("saving:" + JSON.stringify(settings)); + require("Storage").writeJSON("sweepclock.settings.json",settings); +} + +// Boiler plate code for setting up the clock, // below let intervalRef = null; @@ -593,6 +735,7 @@ Bangle.on('faceUp',function(up){ }); g.clear(); +load_settings(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); @@ -602,8 +745,17 @@ setWatch(Bangle.showLauncher, BTN2,{repeat:false,edge:"falling"}); function button1pressed(){ next_font(); + save_settings(); +} + +function button3pressed(){ + next_colorscheme(); + save_settings(); } // Handle button 1 being pressed setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"}); +// Handle button 3 being pressed +setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"}); + diff --git a/apps/sweepclock/sweepclock.png b/apps/sweepclock/sweepclock.png index fcf38162e..70a1cd532 100644 Binary files a/apps/sweepclock/sweepclock.png and b/apps/sweepclock/sweepclock.png differ diff --git a/apps/walkersclock/ChangeLog b/apps/walkersclock/ChangeLog index 41f1cf805..57404ac41 100644 --- a/apps/walkersclock/ChangeLog +++ b/apps/walkersclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: First version of the Walkers Clock 0.02: Fixed screen flicker -0.03: Added display of GPS fix lat/lon and course \ No newline at end of file +0.03: Added display of GPS fix lat/lon and course +0.04: Don't buzz for GPS fix in Quiet Mode diff --git a/apps/walkersclock/app.js b/apps/walkersclock/app.js index 001a3edcd..8a5e826c4 100644 --- a/apps/walkersclock/app.js +++ b/apps/walkersclock/app.js @@ -379,7 +379,9 @@ function processFix(fix) { if (fix.fix) { if (!last_fix.fix) { - Bangle.buzz(); // buzz on first position + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); // buzz on first position + } clearActivityArea = true; } gpsState = GPS_RUNNING; diff --git a/apps/widancs/ChangeLog b/apps/widancs/ChangeLog index 7844830d1..471507736 100644 --- a/apps/widancs/ChangeLog +++ b/apps/widancs/ChangeLog @@ -4,5 +4,4 @@ 0.04: Works on both standard and modified firmware 0.05: Bug fixes w.r.t. reconnection 0.06: Update README - Release version - - +0.07: Respect Quiet Mode diff --git a/apps/widancs/ancs.js b/apps/widancs/ancs.js index 84a79fbf9..50720cd23 100644 --- a/apps/widancs/ancs.js +++ b/apps/widancs/ancs.js @@ -187,9 +187,11 @@ //we may already be displaying a prompt, so clear it E.showPrompt(); if (screentimeout) clearTimeout(screentimeout); - Bangle.setLCDPower(true); + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(true); + } SCREENACCESS.request(); - if (!buzzing){ + if (!buzzing && !(require('Storage').readJSON('setting.json',1)||{}).quiet){ buzzing=true; Bangle.buzz(500).then(()=>{buzzing=false;}); } diff --git a/apps/widbatwarn/ChangeLog b/apps/widbatwarn/ChangeLog index c51b06842..5420b9706 100644 --- a/apps/widbatwarn/ChangeLog +++ b/apps/widbatwarn/ChangeLog @@ -1 +1,2 @@ -0.01: New Battery Warning! \ No newline at end of file +0.01: New Battery Warning! +0.02: Respect Quiet Mode diff --git a/apps/widbatwarn/widget.js b/apps/widbatwarn/widget.js index c5c2f2bf0..3eb603b84 100644 --- a/apps/widbatwarn/widget.js +++ b/apps/widbatwarn/widget.js @@ -39,7 +39,10 @@ .setColor(0xF800).drawString(`${E.getBattery()}%`, a.x+8+100, a.y+a.h/2); }, }); - if (setting("buzz")) Bangle.buzz(); + if (setting("buzz") + && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + } } Bangle.on("charging", check); diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index 3c62f3a09..ea146c34f 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -8,3 +8,4 @@ 0.09: Add daily goal 0.10: Fix daily goal, don't store settings in separate file 0.11: added getSteps() method for apps to retrieve step count +0.12: Respect Quiet Mode diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 58853265c..e8797f571 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -86,7 +86,8 @@ // TODO: could save this to PEDOMFILE for lastUpdate's day? stp_today = 1; } - if (stp_today === setting('goal')) { + if (stp_today === setting('goal') + && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { let b = 3, buzz = () => { if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) } diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 2c6ac0d95..8cc78ea2a 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -54,8 +54,8 @@ const APP_KEYS = [ 'sortorder', 'readme', 'custom', 'interface', 'storage', 'data', 'allow_emulator', 'dependencies' ]; -const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; -const DATA_KEYS = ['name', 'wildcard', 'storageFile']; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite']; +const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; diff --git a/core b/core index e65920a91..7d04c4884 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit e65920a91f9f7178c9d8ed6551ac7d9af0a5d6e1 +Subproject commit 7d04c488496c873f392c5a068f72a6c75df40f70