diff --git a/apps.json b/apps.json index 35430e235..1e673c330 100644 --- a/apps.json +++ b/apps.json @@ -2,7 +2,7 @@ { "id": "fwupdate", "name": "Firmware Update", - "version": "0.02", + "version": "0.03", "description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates", "icon": "app.png", "type": "RAM", @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.39", + "version": "0.41", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -29,6 +29,24 @@ ], "sortorder": -10 }, + { "id": "ac_ac", + "name": "A Configurable Analog Clock", + "shortName":"Configurable Clock", + "version":"0.03", + "description": "AC-AC, a highly customizable analog clock with several clock faces, hands and complications to choose from", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator": false, + "screenshots": [{"url":"app-screenshot.png"}], + "readme": "README.md", + "custom": "Customizer.html", + "storage": [ + {"name":"ac_ac.app.js","url":"app.js"}, + {"name":"ac_ac.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "hebrew_calendar", "name": "Hebrew Calendar", @@ -77,7 +95,7 @@ { "id": "messages", "name": "Messages", - "version": "0.16", + "version": "0.18", "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", "type": "app", @@ -99,24 +117,26 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.05", + "version": "0.06", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", - "tags": "tool,system,messages,notifications", + "tags": "tool,system,messages,notifications,gadgetbridge", "dependencies": {"messages":"app"}, "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", "storage": [ {"name":"android.app.js","url":"app.js"}, {"name":"android.settings.js","url":"settings.js"}, {"name":"android.img","url":"app-icon.js","evaluate":true}, {"name":"android.boot.js","url":"boot.js"} ], + "data": [{"name":"android.settings.json"}], "sortorder": -8 }, { "id": "ios", "name": "iOS Integration", - "version": "0.07", + "version": "0.08", "description": "Display notifications/music/etc from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", @@ -167,7 +187,7 @@ { "id": "setting", "name": "Settings", - "version": "0.39", + "version": "0.41", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -218,7 +238,7 @@ { "id": "locale", "name": "Languages", - "version": "0.14", + "version": "0.15", "description": "Translations for different countries", "icon": "locale.png", "type": "locale", @@ -307,7 +327,7 @@ "description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android' Bangle.js app instead.", "icon": "app.png", "type": "widget", - "tags": "tool,system,android,widget", + "tags": "tool,system,android,widget,gadgetbridge", "supports": ["BANGLEJS","BANGLEJS2"], "dependencies": {"notify":"type"}, "readme": "README.md", @@ -324,7 +344,7 @@ "version":"0.01", "description": "Debug info for Gadgetbridge. Run this app and when Gadgetbridge messages arrive they are displayed on-screen.", "icon": "app.png", - "tags": "", + "tags": "tool,debug,gadgetbridge", "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ @@ -768,7 +788,7 @@ "id": "recorder", "name": "Recorder (BETA)", "shortName": "Recorder", - "version": "0.05", + "version": "0.07", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -845,7 +865,7 @@ { "id": "weather", "name": "Weather", - "version": "0.14", + "version": "0.15", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], @@ -922,12 +942,13 @@ { "id": "widlock", "name": "Lock Widget", - "version": "0.03", + "version": "0.04", "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", "icon": "widget.png", "type": "widget", "tags": "widget,lock", "supports": ["BANGLEJS","BANGLEJS2"], + "sortorder": -1, "storage": [ {"name":"widlock.wid.js","url":"widget.js"} ] @@ -936,7 +957,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", - "version": "0.14", + "version": "0.16", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "icon": "widget.png", "type": "widget", @@ -1040,16 +1061,19 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.01", + "version": "0.03", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", - "type": "boot", + "type": "app", "tags": "health,bluetooth", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ + {"name":"bthrm.app.js","url":"bthrm.js"}, + {"name":"bthrm.recorder.js","url":"recorder.js"}, {"name":"bthrm.boot.js","url":"boot.js"}, - {"name":"bthrm.img","url":"app-icon.js","evaluate":true} + {"name":"bthrm.img","url":"app-icon.js","evaluate":true}, + {"name":"bthrm.settings.js","url":"settings.js"} ] }, { @@ -1324,7 +1348,7 @@ "icon": "gesture.png", "type": "app", "tags": "gesture,ai", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"gesture.app.js","url":"gesture.js"}, {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, @@ -1348,6 +1372,22 @@ {"name":"pparrot.img","url":"party-parrot-icon.js","evaluate":true} ] }, + { + "id": "hralarm", + "name": "Heart rate alarm", + "shortName":"HR Alarm", + "version":"0.01", + "description": "This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"hralarm.wid.js","url":"widget.js"}, + {"name":"hralarm.settings.js","url":"settings.js"} + ] + }, { "id": "hrings", "name": "Hypno Rings", @@ -1501,7 +1541,7 @@ { "id": "gpsinfo", "name": "GPS Info", - "version": "0.06", + "version": "0.09", "description": "An application that displays information about altitude, lat/lon, satellites and time", "icon": "gps-info.png", "type": "app", @@ -1515,13 +1555,14 @@ { "id": "assistedgps", "name": "Assisted GPS Update (AGPS)", - "version": "0.01", - "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", + "version": "0.03", + "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", "icon": "app.png", "type": "RAM", "tags": "tool,outdoors,agps", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", + "customConnect": true, "storage": [] }, { @@ -1590,7 +1631,7 @@ { "id": "widpedom", "name": "Pedometer widget", - "version": "0.20", + "version": "0.22", "description": "Daily pedometer widget", "icon": "widget.png", "type": "widget", @@ -1714,17 +1755,18 @@ { "id": "wohrm", "name": "Workout HRM", - "version": "0.08", + "version": "0.09", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "icon": "app.png", "type": "app", "tags": "hrm,workout", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}], "storage": [ {"name":"wohrm.app.js","url":"app.js"}, + {"name":"wohrm.settings.js","url":"settings.js"}, {"name":"wohrm.img","url":"app-icon.js","evaluate":true} ] }, @@ -1749,8 +1791,9 @@ "icon": "grocery.png", "type": "app", "tags": "tool,outdoors,shopping,list", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "custom": "grocery.html", + "allow_emulator": true, "storage": [ {"name":"grocery.app.js","url":"app.js"}, {"name":"grocery.img","url":"grocery-icon.js","evaluate":true} @@ -1889,13 +1932,15 @@ { "id": "widhwt", "name": "Hand Wash Timer", - "version": "0.01", - "description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.", + "version": "0.02", + "description": "On Bangle.js 1 swipe your wrist over the watch face to start your personal Bangle.js 1 hand wash timer. On Bangle.js2 the Pattern Launcher is recommended to start the timer. Start washing after the short buzz and stop after the long buzz 35sec. later.", "icon": "widget.png", "type": "widget", "tags": "widget,tool", - "supports": ["BANGLEJS"], + "allow_emulator": true, + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ + {"name":"widhwt.app.js","url":"app.js"}, {"name":"widhwt.wid.js","url":"widget.js"} ] }, @@ -2072,12 +2117,13 @@ "id": "devstopwatch", "name": "Dev Stopwatch", "shortName": "Dev Stopwatch", - "version": "0.03", + "version": "0.04", "description": "Stopwatch with 5 laps supported (cyclically replaced)", "icon": "app.png", "tags": "stopwatch,chrono,timer,chronometer", "supports": ["BANGLEJS","BANGLEJS2"], - "screenshots": [{"url":"bangle1-dev-stopwatch-screenshot.png"}], + "screenshots": [{"url":"bangle1-dev-stopwatch-screenshot.png"},{"url":"bangle2-dev-stopwatch-screenshot.png"}], + "readme": "README.md", "allow_emulator": true, "storage": [ {"name":"devstopwatch.app.js","url":"app.js"}, @@ -2250,6 +2296,20 @@ {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} ] }, + { "id": "run", + "name": "Run", + "version":"0.01", + "description": "Displays distance, time, steps, cadence, pace and more for runners.", + "icon": "app.png", + "tags": "run,running,fitness,outdoors,gps", + "supports" : ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"run.app.js","url":"app.js"}, + {"name":"run.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "banglerun", "name": "BangleRun", @@ -2429,7 +2489,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.05", + "version": "0.06", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], @@ -2967,14 +3027,28 @@ ], "data": [{"wildcard":"accellog.?.csv"}] }, + { "id": "accelgraph", + "name": "Accelerometer Graph", + "shortName":"Accel Graph", + "version":"0.01", + "description": "A simple app to draw a graph of data from the accelerometer on the screen", + "icon": "app.png", + "tags": "tool,debug", + "supports" : ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "storage": [ + {"name":"accelgraph.app.js","url":"app.js"}, + {"name":"accelgraph.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "cprassist", "name": "CPR Assist", - "version": "0.01", + "version": "0.02", "description": "Provides assistance while performing a CPR", "icon": "cprassist-icon.png", "tags": "tool,firstaid", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "screenshots": [{"url":"bangle1-CPR-assist-screenshot.png"}], @@ -3532,7 +3606,7 @@ "id": "mclockplus", "name": "Morph Clock+", "shortName": "Morph Clock+", - "version": "0.02", + "version": "0.03", "description": "Morphing Clock with more readable seconds and date and additional stopwatch", "icon": "mclockplus.png", "type": "clock", @@ -3790,7 +3864,7 @@ { "id": "simplest", "name": "Simplest Clock", - "version": "0.03", + "version": "0.05", "description": "The simplest working clock, acts as a tutorial piece", "icon": "simplest.png", "screenshots": [{"url":"screenshot_simplest.png"}], @@ -3896,8 +3970,8 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.06", - "description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.", + "version": "0.07", + "description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.", "icon": "app.png", "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, {"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}], @@ -3986,11 +4060,12 @@ "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "storage": [ - {"name":"doztime.app.js","url":"app.js"}, + {"name":"doztime.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]}, + {"name":"doztime.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}, {"name":"doztime.img","url":"app-icon.js","evaluate":true} ] }, @@ -4210,13 +4285,13 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.09", - "description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", + "version": "0.11", + "description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", "icon": "pastel.png", - "dependencies": {"mylocation":"app", "widpedom":"app"}, - "screenshots": [{"url":"screenshot_pastel.png"}], + "dependencies": {"mylocation":"app", "widpedom":"app","weather":"app"}, + "screenshots": [{"url":"screenshot_pastel.png"}, {"url":"weather_icons.png"}], "type": "clock", - "tags": "clock", + "tags": "clock, weather, tool", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ @@ -4237,8 +4312,9 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.03", - "description": "A simple clock using the bold Anton font.", + "version": "0.06", + "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", + "readme":"README.md", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", @@ -4247,8 +4323,10 @@ "allow_emulator": true, "storage": [ {"name":"antonclk.app.js","url":"app.js"}, + {"name":"antonclk.settings.js","url":"settings.js"}, {"name":"antonclk.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"antonclk.json"}] }, { "id": "waveclk", @@ -4327,8 +4405,10 @@ "allow_emulator": true, "storage": [ {"name":"ffcniftya.app.js","url":"app.js"}, - {"name":"ffcniftya.img","url":"app-icon.js","evaluate":true} - ] + {"name":"ffcniftya.img","url":"app-icon.js","evaluate":true}, + {"name":"ffcniftya.settings.js","url":"settings.js"} + ], + "data": [{"name":"ffcniftya.json"}] }, { "id": "ffcniftyb", @@ -4382,7 +4462,7 @@ { "id": "gpstouch", "name": "GPS Touch", - "version": "0.01", + "version": "0.02", "description": "A touch based GPS watch, shows OS map reference", "icon": "gpstouch.png", "screenshots": [{"url":"screenshot4.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot1.png"}], @@ -4414,7 +4494,7 @@ "name": "Q Alarm and Timer", "shortName": "Q Alarm", "icon": "app.png", - "version": "0.03", + "version": "0.04", "description": "Alarm and timer app with days of week and 'hard' option.", "tags": "tool,alarm,widget", "supports": ["BANGLEJS", "BANGLEJS2"], @@ -4472,7 +4552,7 @@ "name": "A Battery Widget (with percentage)", "shortName":"A Battery Widget", "icon": "widget.png", - "version":"1.02", + "version":"1.03", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", @@ -4487,7 +4567,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.08", + "version":"0.13", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", @@ -4621,7 +4701,7 @@ "shortName":"93 Dub", "icon": "93dub.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.05", + "version":"0.06", "description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo", "tags": "clock", "type": "clock", @@ -4710,7 +4790,7 @@ "icon": "mylocation.png", "type": "app", "screenshots": [{"url":"screenshot_1.png"}], - "version":"0.01", + "version":"0.02", "description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README", "readme": "README.md", "tags": "tool,utility", @@ -4727,7 +4807,7 @@ "id": "pebble", "name": "Pebble Clock", "shortName": "Pebble", - "version": "0.06", + "version": "0.07", "description": "A pebble style clock to keep the rebellion going", "dependencies": {"widpedom":"app"}, "readme": "README.md", @@ -4735,7 +4815,7 @@ "screenshots": [{"url":"pebble_screenshot.png"}], "type": "clock", "tags": "clock", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"pebble.app.js","url":"pebble.app.js"}, {"name":"pebble.settings.js","url":"pebble.settings.js"}, @@ -4769,7 +4849,7 @@ "screenshots": [{"url":"screenshot_widbata_1.png"}], "version":"0.01", "type": "widget", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "description": "Shows the current battery level status in the top right using the clocks colour theme", "tags": "widget,battery", @@ -4797,7 +4877,7 @@ { "id": "menuwheel", "name": "Wheel Menus", - "version": "0.01", + "version": "0.02", "description": "Replace Bangle.js 2's menus with a version that contains variable-size text and a back button", "readme": "README.md", "icon": "icon.png", @@ -4914,7 +4994,7 @@ "id": "rebble", "name": "Rebble Clock", "shortName": "Rebble", - "version": "0.03", + "version": "0.04", "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion", "readme": "README.md", "icon": "rebble.png", @@ -4982,7 +5062,7 @@ { "id": "pooqround", "name": "pooq Round watch face", "shortName":"pooq Round", - "version":"0.01", + "version":"0.02", "description": "A 24 hour analogue watchface with high legibility and a novel style.", "icon": "app.png", "type": "clock", @@ -5001,7 +5081,7 @@ { "id": "coretemp", "name": "CoreTemp", - "version": "0.02", + "version": "0.03", "description": "Display CoreTemp device sensor data", "icon": "coretemp.png", "type": "app", @@ -5011,6 +5091,7 @@ "storage": [ {"name":"coretemp.wid.js","url":"widget.js"}, {"name":"coretemp.app.js","url":"coretemp.js"}, + {"name":"coretemp.recorder.js","url":"recorder.js"}, {"name":"coretemp.settings.js","url":"settings.js"}, {"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true}, {"name":"coretemp.boot.js","url":"boot.js"} @@ -5035,7 +5116,7 @@ { "id": "lapcounter", "name": "Lap Counter", - "version": "0.01", + "version": "0.02", "description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], @@ -5070,10 +5151,10 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.03", + "version":"0.05", "description": "A clock with circles for different data at the bottom in a probably familiar style", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}], "dependencies": {"widpedom":"app"}, "type": "clock", "tags": "clock", @@ -5099,7 +5180,7 @@ "tags": "clock", "allow_emulator":true, "supports" : ["BANGLEJS2"], - "type": "clock", + "type": "clock", "storage": [ {"name":"contourclock.app.js","url":"app.js"}, {"name":"contourclock.img","url":"app-icon.js","evaluate":true} @@ -5121,6 +5202,60 @@ {"name":"ltherm.img","url":"icon.js","evaluate":true} ] }, + { + "id": "ftclock", + "name": "Four Twenty Clock", + "version": "0.02", + "description": "A clock that tells when and where it's going to be 4:20 next", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"ftclock.app.js","url":"app.js"}, + {"name":"fourTwenty","url":"fourTwenty.js"}, + {"name":"fourTwentyTz","url":"fourTwentyTz.js"}, + {"name":"ftclock.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "mmind", + "name": "Classic Mind Game", + "shortName":"Master Mind", + "icon": "mmind.png", + "version":"0.01", + "description": "This is the classic game for masterminds", + "screenshots": [{"url":"screenshot_mmind.png"}], + "type": "app", + "tags": "game", + "readme":"README.md", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"mmind.app.js","url":"mmind.app.js"}, + {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} + ] + }, + { + "id": "presentor", + "name": "Presentor", + "version": "3.0", + "description": "Use your Bangle to present!", + "icon": "app.png", + "type": "app", + "tags": "tool,bluetooth", + "interface": "interface.html", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"presentor.app.js","url":"app.js"}, + {"name":"presentor.img","url":"app-icon.js","evaluate":true}, + {"name":"presentor.json","url":"settings.json"} + ] + }, { "id": "slash", "name": "Slash Watch", @@ -5142,15 +5277,16 @@ { "id": "promenu", "name": "Pro Menu", - "version": "0.01", - "description": "Replace Bangle.js 1's built in menu function.", + "version": "0.02", + "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "icon": "icon.png", "type": "boot", "tags": "system", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"pro-menu-screenshot.png"}], "storage": [ - {"name":"promenu.boot.js","url":"boot.js"}, + {"name":"promenu.boot.js","url":"boot.js","supports": ["BANGLEJS"]}, + {"name":"promenu.boot.js","url":"bootb2.js","supports": ["BANGLEJS2"]}, {"name":"promenu.img","url":"promenuIcon.js","evaluate":true} ] }, @@ -5262,7 +5398,7 @@ { "id": "colorful_clock", "name": "Colorful Analog Clock", "shortName":"Colorful Clock", - "version":"0.02", + "version":"0.03", "description": "a colorful analog clock", "icon": "app-icon.png", "type": "clock", @@ -5334,7 +5470,7 @@ "icon": "andark_icon.png", "type": "clock", "tags": "clock", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"andark.app.js","url":"app.js"}, @@ -5357,5 +5493,188 @@ { "name": "diract.app.js", "url": "diract.js" }, { "name": "diract.img", "url": "diract-icon.js", "evaluate": true } ] + }, + { + "id": "sonicclk", + "name": "Sonic Clock", + "version": "1.01", + "description": "A classic sonic clock featuring run, stop and wait animations.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"sonicclk.app.js","url":"app.js"}, + {"name":"sonicclk.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "touchmenu", + "name": "TouchMenu", + "version": "0.01", + "description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2", + "screenshots": [{"url":"touchmenu.gif"}], + "icon": "touchmenu.png", + "type": "bootloader", + "tags": "tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"touchmenu.boot.js","url":"touchmenu.boot.js"} + ] + }, + { + "id": "puzzle15", + "name": "15 puzzle", + "version": "0.05", + "description": "A 15 puzzle game with drag gesture interface", + "readme":"README.md", + "icon": "puzzle15.app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"puzzle15.app.js","url":"puzzle15.app.js"}, + {"name":"puzzle15.settings.js","url":"puzzle15.settings.js"}, + {"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true} + ], + "data": [{"name":"puzzle15.json"}] + }, + { + "id": "flipper", + "name": "Flipper", + "version": "0.01", + "description": "Switch between dark and light theme and vice versa, combine with pattern launcher and swipe to flip.", + "readme":"README.md", + "screenshots": [{"url":"flipper.png"}], + "icon": "flipper.png", + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"flipper.app.js","url":"flipper.app.js"}, + {"name":"flipper.img","url":"flipper.icon.js","evaluate":true} + ] + }, + { "id": "ruuviwatch", + "name": "Ruuvi Watch", + "shortName":"Ruuvi Watch", + "icon": "ruuviwatch.png", + "version":"1.01", + "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", + "readme":"README.md", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"}, + {"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true} + ] + }, + { + "id": "limelight", + "name": "Limelight", + "version": "0.01", + "description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)", + "icon": "limelight.png", + "readme":"README.md", + "screenshots": [{"url":"screenshot_limelight.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"limelight.app.js","url":"limelight.app.js"}, + {"name":"limelight.settings.js","url":"limelight.settings.js"}, + {"name":"limelight.img","url":"limelight.icon.js","evaluate":true} + ] + }, + { "id": "banglexercise", + "name": "BanglExercise", + "shortName":"BanglExercise", + "version":"0.02", + "description": "Can automatically track exercises while wearing the Bangle.js watch.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "sport", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"banglexercise.app.js","url":"app.js"}, + {"name":"banglexercise.img","url":"app-icon.js","evaluate":true}, + {"name":"banglexercise.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"banglexercise.json"} + ] + }, + { + "id": "widpa", + "name": "Simple Pedometer", + "shortName":"Simple Pedometer", + "icon": "screenshot_widpa.png", + "screenshots": [{"url":"screenshot_widpa.png"}], + "version":"0.02", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in 12x16 font, requires firmware v2.11.21 or later", + "tags": "widget,battery", + "storage": [ + {"name":"widpa.wid.js","url":"widpa.wid.js"} + ] + }, + { + "id": "widpb", + "name": "Lato Pedometer", + "shortName":"Lato Pedometer", + "icon": "screenshot_widpb.png", + "screenshots": [{"url":"screenshot_widpb.png"}], + "version":"0.02", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in the Lato font, requires firmware v2.11.21 or later", + "tags": "widget,battery", + "storage": [ + {"name":"widpb.wid.js","url":"widpb.wid.js"} + ] + }, + { + "id": "timeandlife", + "name": "Time and Life", + "shortName":"Time and Lfie", + "icon": "app.png", + "version":"0.1", + "description": "A simple watchface which displays the time when the screen is tapped and decays according to the rules of Conway's game of life.", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"timeandlife.app.js","url":"app.js"}, + {"name":"timeandlife.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "acmaze", + "name": "AccelaMaze", + "shortName":"AccelaMaze", + "version":"0.01", + "description": "Tilt the watch to roll a ball through a maze", + "icon": "app.png", + "tags": "game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"screenshot.png"}], + "storage": [ + {"name":"acmaze.app.js","url":"app.js"}, + {"name":"acmaze.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog index c1b2588bb..1c18ca59b 100644 --- a/apps/93dub/ChangeLog +++ b/apps/93dub/ChangeLog @@ -3,3 +3,4 @@ 0.03: Code style cleanup 0.04: Set 00:00 to 12:00 for 12 hour time 0.05: Display time, even on Thursday +0.06: Fix light theme issue, where widgets would end up on a light strip diff --git a/apps/93dub/app.js b/apps/93dub/app.js index 1b0f69a94..f970eec5d 100644 --- a/apps/93dub/app.js +++ b/apps/93dub/app.js @@ -122,7 +122,13 @@ function draw(){ queueDraw(); } - +/** + * This watch is mostly dark, it does not make sense to respect the + * light theme as you end up with a white strip at the top for the + * widgets and black watch. So set the colours to the dark theme. + * + */ +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); draw(); //the following section is also from waveclk diff --git a/apps/ac_ac/Customizer.html b/apps/ac_ac/Customizer.html new file mode 100644 index 000000000..cc8e21d1f --- /dev/null +++ b/apps/ac_ac/Customizer.html @@ -0,0 +1,890 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Please customize your analog clock for the Bangle.js 2 according to your needs. + When finished, click on "Upload" at the bottom of this form. +

+ (Pressing "Upload" will also backup your current configuration so that you + won't have to enter the same settings over and over again when you come back + to this page later) +

+ +

Clock Size Calculation

+ +

+ Click on the desired clock size calculator (if you installed some widgets + on your Bangle.js 2, the smart one may produce larger clock faces than the + simple one): +

+ + + + + + + + +
+
+ simple +
+
+ smart +
+
+ (custom) +
+

+ If you prefer a "custom" clock size calculator, please enter the URL + of its JavaScript module below: +

+ custom URL: +

+ +

Clock Face

+ +

+ Click on the desired clock face: +

+ + + + + + + + + + + + +
+
+ none +
+
+ four-fold +
+
+ twelve-fold +
+
+ "rainbow"
colored +
+
+ (custom) +
+

+ Clock faces are drawn in the configured foreground and background colors + (you may select them at the end of this form) +

+ "Four-fold" clock faces may draw indian-arabic or roman numerals. Which do you prefer? +

+ indian-arabic (3, 6, 9, 12)
+ roman (III, VI, IX, XII) +

+ The "twelve-fold" and "rainbow"-colored faces may be drawn with or without + dots marking the position of every minute. Which variant do you prefer? +

+ without dots
+ with dots +

+ If you prefer a "custom" clock face, please enter the URL + of its JavaScript module below: +

+ custom URL: +

+ +

Clock Hands

+ +

+ Click on the desired clock hands: +

+ + + + + + + + + + +
+
+ simple +
+
+ rounded +
+
+ hollow +
+
+ (custom) +
+

+ Clock hands are drawn in the configured foreground and background colors + (you may select them at the end of this form) +

+ Hollow clock hands may optionally be filled with a given color. If you have + chosen hollow hands, please specify the desired fill mode and color below: +

+ Hollow Hand Fill Color: +

+ + + + + + + + + + +

+ Additionally, all clock hands may be drawn with or without second hands. + If you want them to be drawn, please click on their desired color below + (or choose "themed" to use your Bangle's configured theme) - if not, just + select "none": +

+ Second Hand Color: +

+ + + + + + + + + + +

+ If you prefer "custom" clock hands, please enter the URL + of their JavaScript module below: +

+ custom URL: +

+ +

Complications

+ +

+ Complications are small displays for additional information. If you want + one or multiple complications to be added to your clock, you'll have to + specify which one to be loaded and where it should be placed. +

+ Up to 6 possible positions exist (top-left, top-right, left, right, + bottom-left and bottom-right). Alternatively, the positions "top-left" and + "top-right" may be traded for a slightly larger complication at position + "top" or "bottom-left" and "bottom-right" for one at the "bottom": +

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
top-left:
  Complication: + +
custom URL:
top:
  Complication: + +
custom URL:
top-right:
  Complication: + +
custom URL:
left:
  Complication: + +
custom URL:
right:
  Complication: + +
custom URL:
bottom-left:
  Complication: + +
custom URL:
bottom:
  Complication: + +
custom URL:
bottom-right:
  Complication: + +
custom URL:
+

+ +

Settings

+ +

+ Color faces, hands and complications are often drawn using configurable + foreground and background colors. +

+ Here you may specify these colors. Click on a color to select it - or on + "themed" if you want the clock to use the currently configured theme on + your Bangle.js 2: +

+ Background Color: +

+ + + + + + + + + +

+ Foreground Color: +

+ + + + + + + + + +

+ When you are satisfied with your configuration, just click on "Upload" in + order to generate the specified clock and upload it to your Bangle.js 2: +

+ + + +

+ This application is based on the author's + Analog Clock Construction Kit (ACCK). + If you need a different "clockwork", clock size calculation or clock face, + or specific clock hands or complications, just follow the link to learn how to + implement your own clock parts. +

+ + + diff --git a/apps/ac_ac/README.md b/apps/ac_ac/README.md new file mode 100644 index 000000000..05e5f4798 --- /dev/null +++ b/apps/ac_ac/README.md @@ -0,0 +1,34 @@ +# AC-AC - A Configurable Analog Clock # + +This app implements an analog clock with various faces, hands and complications +to choose from before uploading to a Bangle.js 2. + +It is based on the [Analog Clock Construction Kit (ACCK)](https://github.com/rozek/banglejs-2-analog-clock-construction-kit) +and makes most of the currently implemented parts available with a few mouse +clicks - just click on "Upload" and you will be directed to a web form where +you compose your very own, personal analog clock. + +You currently have the choice between + +* 2 different clock sizes, +* 4 different clock faces, +* 3 different clock hands and +* 4 different complications + +Alternatively, you may specify the GitHub URL of ACCK compatible modules for +external clock sizes, faces, hands or complications. + +Additionally, you may use the currently configured global theme or configure +your own colors for clock fore- and background and second hands. + +Consequently, even without external modules you already have the choice between +102144 combinations! + + + +## License ## + +[MIT License](LICENSE) diff --git a/apps/ac_ac/RainbowClockFace.png b/apps/ac_ac/RainbowClockFace.png new file mode 100644 index 000000000..2defa759b Binary files /dev/null and b/apps/ac_ac/RainbowClockFace.png differ diff --git a/apps/ac_ac/app-icon.js b/apps/ac_ac/app-icon.js new file mode 100644 index 000000000..20caf2c8e --- /dev/null +++ b/apps/ac_ac/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/ABH+AQPvBpIAI/n8/3f5/PCp/v9oHF7w1CABffGxAYMH4f9z/514YDCxW/O4gFBxwHD/ZEL7/9GgX8GwQLCBQQXH/uP/Hf/2N44IBAgIXJ7oaD/3v/3uAYIIB9wQGAA2+/iRG5oSIM4f+1nrPYgAB3aHIAC77QYYRoCAAP676ICABXYFIntDoPf3+PC5f+BoPOX4vPNBn7IogEB/eu3QXC9wNEAAeKBIP+dgbSCDYMwgEApQVEygPCeRH8iAWBAAMHPwXDgoRGAonACwYABgN5uMAC4q8GC4U0DQsAggRF9gXFgggB/2hC4kdVAQCBVAX7xwXCVAnGCwUadAeeDYfr7IhEAAf93e+A4gpB9yRB/mqcgndRgQAHzqRE1gEC/KoCjLZEsgCB9evO4gOC/RyEgqdC2KnFO4S/KgFYsC/Ga5EBs1AX5bXHgx1C2YXEnp7GCARgB4AfE64WCnawFCgf9VAK/G/3M7zWDz4PF/maXJIAD7D8EVAP85QXN3OP/42DfoQXN/wvE/ySGABa8FAC37AgepVwQ9E1SfBAAJIEAAnrBQ39xgwJ7pRHFQX+3QECCAbyG9bPDzwXC9QMBdgQXIAAf41wEC5pLCJJBcF9fZQ5IAGYYn81q7RJQwWC/wXM9/tA4veCxooDIAPv55PEABwpB97rDAAw")) \ No newline at end of file diff --git a/apps/ac_ac/app-icon.png b/apps/ac_ac/app-icon.png new file mode 100644 index 000000000..b83541133 Binary files /dev/null and b/apps/ac_ac/app-icon.png differ diff --git a/apps/ac_ac/app-screenshot.png b/apps/ac_ac/app-screenshot.png new file mode 100644 index 000000000..0aef3fa38 Binary files /dev/null and b/apps/ac_ac/app-screenshot.png differ diff --git a/apps/ac_ac/app.js b/apps/ac_ac/app.js new file mode 100644 index 000000000..1d9b2e3c6 --- /dev/null +++ b/apps/ac_ac/app.js @@ -0,0 +1,2 @@ +let Clockwork = require('https://raw.githubusercontent.com/rozek/banglejs-2-simple-clockwork/main/Clockwork.js'); +Clockwork.windUp(); \ No newline at end of file diff --git a/apps/ac_ac/custom.png b/apps/ac_ac/custom.png new file mode 100644 index 000000000..14d797ba3 Binary files /dev/null and b/apps/ac_ac/custom.png differ diff --git a/apps/ac_ac/fourfoldClockFace.png b/apps/ac_ac/fourfoldClockFace.png new file mode 100644 index 000000000..391303b31 Binary files /dev/null and b/apps/ac_ac/fourfoldClockFace.png differ diff --git a/apps/ac_ac/hollowClockHands.png b/apps/ac_ac/hollowClockHands.png new file mode 100644 index 000000000..2dce42ef5 Binary files /dev/null and b/apps/ac_ac/hollowClockHands.png differ diff --git a/apps/ac_ac/largePlaceholders.png b/apps/ac_ac/largePlaceholders.png new file mode 100644 index 000000000..b7272e57c Binary files /dev/null and b/apps/ac_ac/largePlaceholders.png differ diff --git a/apps/ac_ac/none.png b/apps/ac_ac/none.png new file mode 100644 index 000000000..6f8d8ae14 Binary files /dev/null and b/apps/ac_ac/none.png differ diff --git a/apps/ac_ac/roundedClockHands.png b/apps/ac_ac/roundedClockHands.png new file mode 100644 index 000000000..cbd48e856 Binary files /dev/null and b/apps/ac_ac/roundedClockHands.png differ diff --git a/apps/ac_ac/simpleClockHands.png b/apps/ac_ac/simpleClockHands.png new file mode 100644 index 000000000..820606f27 Binary files /dev/null and b/apps/ac_ac/simpleClockHands.png differ diff --git a/apps/ac_ac/simpleClockSize.png b/apps/ac_ac/simpleClockSize.png new file mode 100644 index 000000000..49650586e Binary files /dev/null and b/apps/ac_ac/simpleClockSize.png differ diff --git a/apps/ac_ac/smallPlaceholders.png b/apps/ac_ac/smallPlaceholders.png new file mode 100644 index 000000000..43569e56d Binary files /dev/null and b/apps/ac_ac/smallPlaceholders.png differ diff --git a/apps/ac_ac/smartClockSize.png b/apps/ac_ac/smartClockSize.png new file mode 100644 index 000000000..6891acc89 Binary files /dev/null and b/apps/ac_ac/smartClockSize.png differ diff --git a/apps/ac_ac/twelvefoldClockFace.png b/apps/ac_ac/twelvefoldClockFace.png new file mode 100644 index 000000000..fc04d865e Binary files /dev/null and b/apps/ac_ac/twelvefoldClockFace.png differ diff --git a/apps/accelgraph/ChangeLog b/apps/accelgraph/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/accelgraph/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/accelgraph/app-icon.js b/apps/accelgraph/app-icon.js new file mode 100644 index 000000000..d45b8cc63 --- /dev/null +++ b/apps/accelgraph/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA/4AB304ief85L/ABNVAAwKCgILHoALBgoLHqALOrVVr4BEBZIFBBYiaCAAPq2oLQEYlqF5VrBZWnBZWvBZNWz4LGBoQLHJ4O///6v/1BZHa/4LFLYOlr9pR49r1ILJ09qr4ZBBY2vrWdBY5PBq2uyoLIquqBY5bBKoZTFLYILJJ4STDBY77IJ4QLUJ4QLU1QAE0oLPqoAGBZ0BBY9ABYMABY4KCAH4AGA=")) diff --git a/apps/accelgraph/app.js b/apps/accelgraph/app.js new file mode 100644 index 000000000..a59d636d2 --- /dev/null +++ b/apps/accelgraph/app.js @@ -0,0 +1,24 @@ +Bangle.loadWidgets(); +g.clear(1); +Bangle.drawWidgets(); +var R = Bangle.appRect; + +var x = 0; +var last; + +function getY(v) { + return (R.y+R.y2 + v*R.h/2)/2; +} +Bangle.on('accel', a => { + g.reset(); + if (last) { + g.setColor("#f00").drawLine(x-1,getY(last.x),x,getY(a.x)); + g.setColor("#0f0").drawLine(x-1,getY(last.y),x,getY(a.y)); + g.setColor("#00f").drawLine(x-1,getY(last.z),x,getY(a.z)); + } + last = a;x++; + if (x>=g.getWidth()) { + x = 1; + g.clearRect(R); + } +}); diff --git a/apps/accelgraph/app.png b/apps/accelgraph/app.png new file mode 100644 index 000000000..b0ba00ee7 Binary files /dev/null and b/apps/accelgraph/app.png differ diff --git a/apps/accelgraph/screenshot.png b/apps/accelgraph/screenshot.png new file mode 100644 index 000000000..404243d85 Binary files /dev/null and b/apps/accelgraph/screenshot.png differ diff --git a/apps/acmaze/ChangeLog b/apps/acmaze/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/acmaze/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/acmaze/README.md b/apps/acmaze/README.md new file mode 100644 index 000000000..4724eea3e --- /dev/null +++ b/apps/acmaze/README.md @@ -0,0 +1,17 @@ +# AccelaMaze + +Tilt the watch to roll a ball through a maze. + +![Screenshot](screenshot.png) + +## Usage + +* Use the menu to select difficulty level (or exit). +* Wait until the maze gets generated and a red ball appears. +* Tilt the watch to get the ball into the green cell. + +At any time you can click the button to return to the menu. + +## Creator + +[Nimrod Kerrett](https://zzzen.com) diff --git a/apps/acmaze/app-icon.js b/apps/acmaze/app-icon.js new file mode 100644 index 000000000..8bd043b8b --- /dev/null +++ b/apps/acmaze/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwggaXh3M53/AA3yl4IHn//+EM5nMAoIX/C4RfCC4szmcxC4QFBAAUxC4UPAwIOB+YCCiMRkAFCkIGBAAQfBC4IUEAQhHIAAQX/C5EDmcyCgUTAoYXDR4kzC4UBPoKVB+YFFAQSPBiAKBiCnDGoZECABDUCa4YX/C5qPBQwoXGkczmC/FQYSSCVQSSCEwQOCC4hKFX4QXCd5YX/C4qMEmQXITAinDPoIADTwSPFkKMBX47RGI47XIC/4XCgZ9DQYYABmKYBmIXFkczmEBRIK/CQYQIBkECSoiSCA4MQa5pEFd6IX/RgMyC6H/QASVCRIS/EAQrXFJQoX/C6kDRQIXCiYFD+QFBmIUCkYFD+CJBiSPCRwIFFSoQFCiF3u9wI4gAO+wXW+IXygAAW")) diff --git a/apps/acmaze/app.js b/apps/acmaze/app.js new file mode 100644 index 000000000..53a851b5e --- /dev/null +++ b/apps/acmaze/app.js @@ -0,0 +1,276 @@ +const MARGIN = 25; +const WALL_RIGHT = 1, WALL_DOWN = 2; +const STATUS_GENERATING = 0, STATUS_PLAYING = 1, + STATUS_SOLVED = 2, STATUS_ABORTED = -1; + +function Maze(n) { + this.n = n; + this.status = STATUS_GENERATING; + this.wall_length = Math.floor((g.getHeight()-2*MARGIN)/n); + this.total_length = this.wall_length*n; + this.margin = Math.floor((g.getHeight()-this.total_length)/2); + this.ball_x = 0; + this.ball_y = 0; + this.clearScreen = function() { + g.clearRect( + 0, this.margin, + g.getWidth(), this.margin+this.total_length + ); + }; + this.clearScreen(); + g.setColor(g.theme.fg); + for (let i=0; i<=n; i++) { + g.drawRect( + this.margin, this.margin+i*this.wall_length, + g.getWidth()-this.margin, this.margin+i*this.wall_length + ); + g.drawRect( + this.margin+i*this.wall_length, this.margin, + this.margin+i*this.wall_length, g.getHeight() - this.margin + ); + } + this.walls = new Uint8Array(n*n); + this.groups = new Uint8Array(n*n); + for (let cell = 0; cell0 && !(this.walls[n*(ball_r-1)+ball_c]&WALL_DOWN)) { + next_y--; + } else if (dy>0 && ball_r<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_DOWN)) { + next_y++; + } else if (dx<0 && ball_c>0 && !(this.walls[n*ball_r+ball_c-1]&WALL_RIGHT)) { + next_x--; + } else if (dx>0 && ball_c<(this.n-1) && !(this.walls[n*ball_r+ball_c]&WALL_RIGHT)) { + next_x++; + } else { + return false; + } + } + this.clearCell(ball_r, ball_c); + if (this.ball_x%this.wall_length) { + this.clearCell(ball_r, ball_c+1); + } + if (this.ball_y%this.wall_length) { + this.clearCell(ball_r+1, ball_c); + } + this.ball_x = next_x; + this.ball_y = next_y; + this.drawBall(this.ball_x, this.ball_y); + if (this.ball_x==(n-1)*this.wall_length && this.ball_y==(n-1)*this.wall_length) { + this.status = STATUS_SOLVED; + } + return true; + }; + this.try_move_horizontally = function(accel_x) { + if (accel_x>0.15) { + return this.move(-1, 0); + } else if (accel_x<-0.15) { + return this.move(1, 0); + } + return false; + }; + this.try_move_vertically = function(accel_y) { + if (accel_y<-0.15) { + return this.move(0,1); + } else if (accel_y>0.15) { + return this.move(0,-1); + } + return false; + }; + this.tick = function() { + accel = Bangle.getAccel(); + if (this.ball_x%this.wall_length) { + this.try_move_horizontally(accel.x); + } else if (this.ball_y%this.wall_length) { + this.try_move_vertically(accel.y); + } else { + if (Math.abs(accel.x)>Math.abs(accel.y)) { // prefer horizontally + if (!this.try_move_horizontally(accel.x)) { + this.try_move_vertically(accel.y); + } + } else { // prefer vertically + if (!this.try_move_vertically(accel.y)) { + this.try_move_horizontally(accel.x); + } + } + } + }; + this.clearCell(0,0); + this.clearCell(n-1,n-1); + this.drawBall(0,0); + this.status = STATUS_PLAYING; +} + +function timeToText(t) { // Courtesy of stopwatch app + let hrs = Math.floor(t/3600000); + let mins = Math.floor(t/60000)%60; + let secs = Math.floor(t/1000)%60; + let tnth = Math.floor(t/100)%10; + let text; + + if (hrs === 0) + text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth; + else + text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + return text; +} + +let aborting = false; +let start_time = 0; +let duration = 0; +let maze=null; +let mazeMenu = { + "": { "title": "Maze size", "selected": 1 }, + "Easy (8x8)": function() { E.showMenu(); maze = new Maze(8); }, + "Medium (10x10)": function() { E.showMenu(); maze = new Maze(10); }, + "Hard (14x14)": function() { E.showMenu(); maze = new Maze(14); }, + "< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock +}; + +g.clear(true); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setLocked(false); +Bangle.setLCDTimeout(0); +E.showMenu(mazeMenu); +let maze_interval = setInterval( + function() { + if (maze) { + if (digitalRead(BTN1) || maze.status==STATUS_ABORTED) { + console.log(`aborting ${start_time}`); + maze = null; + start_time = duration = 0; + aborting = false; + setTimeout(function() {E.showMenu(mazeMenu); }, 100); + return; + } + if (!start_time) { + start_time = Date.now(); + } + if (maze.status==STATUS_PLAYING) { + maze.tick(); + } + if (maze.status==STATUS_SOLVED && !duration) { + duration = Date.now()-start_time; + g.setFontAlign(0,0).setColor(g.theme.fg); + g.setFont("Vector",18); + g.drawString(`Solved in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true); + } + } + }, 25); diff --git a/apps/acmaze/app.png b/apps/acmaze/app.png new file mode 100644 index 000000000..0d96448b1 Binary files /dev/null and b/apps/acmaze/app.png differ diff --git a/apps/acmaze/screenshot.png b/apps/acmaze/screenshot.png new file mode 100644 index 000000000..4b7217b97 Binary files /dev/null and b/apps/acmaze/screenshot.png differ diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index c2c4ea6be..0d837fe43 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -4,3 +4,4 @@ 0.03: Handling of message actions (ok/clear) 0.04: Android icon now goes to settings page with 'find phone' 0.05: Fix handling of message actions +0.06: Option to keep messages after a disconnect (default false) (fix #1186) diff --git a/apps/android/README.md b/apps/android/README.md new file mode 100644 index 000000000..c10718aac --- /dev/null +++ b/apps/android/README.md @@ -0,0 +1,48 @@ +# Android Integration + +This app allows your Bangle.js to receive notifications [from the Gadgetbridge app on Android](http://www.espruino.com/Gadgetbridge) + +See [this link](http://www.espruino.com/Gadgetbridge) for notes on how to install +the Android app (and how it works). + +It requires the `Messages` app on Bangle.js (which should be automatically installed) to +display any notifications that are received. + +## Settings + +You can access the settings menu either from the `Android` icon in the launcher, +or from `App Settings` in the `Settings` menu. + +It contains: + +* `Connected` - shows whether there is an active Bluetooth connection or not +* `Find Phone` - opens a submenu where you can activate the `Find Phone` functionality +of Gadgetbridge - making your phone make noise so you can find it. +* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js +keep any messages it has received, or should it delete them? +* `Messages` - launches the messages app, showing a list of messages + +## How it works + +Gadgetbridge on Android connects to Bangle.js, and sends commands over the +BLE UART connection. These take the form of `GB({ ... JSON ... })\n` - so they +call a global function called `GB` which then interprets the JSON. + +Responses are sent back to Gadgetbridge simply as one line of JSON. + +More info on message formats on http://www.espruino.com/Gadgetbridge + +## Testing + +Bangle.js can only hold one connection open at a time, so it's hard to see +if there are any errors when handling Gadgetbridge messages. + +However you can: + +* Use the `Gadgetbridge Debug` app on Bangle.js to display/log the messages received from Gadgetbridge +* Connect with the Web IDE and manually enter the Gadgetbridge messages on the left-hand side to +execute them as if they came from Gadgetbridge, for instance: + +``` +GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"}) +``` diff --git a/apps/android/boot.js b/apps/android/boot.js index 59ffe006d..fff9ad444 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -4,6 +4,7 @@ Bluetooth.println(JSON.stringify(message)); } + var settings = require("Storage").readJSON("android.settings.json",1)||{}; var _GB = global.GB; global.GB = (event) => { // feed a copy to other handlers if there were any @@ -51,7 +52,8 @@ // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); } NRF.on("connect", () => setTimeout(sendBattery, 2000)); - NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect + if (!settings.keep) + NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect setInterval(sendBattery, 10*60*1000); // Health tracking Bangle.on('health', health=>{ @@ -68,4 +70,6 @@ if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); // error/warn here? }; + // remove settings object so it's not taking up RAM + delete settings; })(); diff --git a/apps/android/settings.js b/apps/android/settings.js index d241397a4..7c46a1fc0 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -2,17 +2,29 @@ function gb(j) { Bluetooth.println(JSON.stringify(j)); } + var settings = require("Storage").readJSON("android.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("android.settings.json", settings); + } var mainmenu = { "" : { "title" : "Android" }, "< Back" : back, - "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, "Find Phone" : () => E.showMenu({ "" : { "title" : "Find Phone" }, "< Back" : ()=>E.showMenu(mainmenu), - "On" : _=>gb({t:"findPhone",n:true}), - "Off" : _=>gb({t:"findPhone",n:false}), + /*LANG*/"On" : _=>gb({t:"findPhone",n:true}), + /*LANG*/"Off" : _=>gb({t:"findPhone",n:false}), }), - "Messages" : ()=>load("messages.app.js") + /*LANG*/"Keep Msgs" : { + value : !!settings.keep, + format : v=>v?/*LANG*/"Yes":/*LANG*/"No", + onchange: v => { + settings.keep = v; + updateSettings(); + } + }, + /*LANG*/"Messages" : ()=>load("messages.app.js") }; E.showMenu(mainmenu); }) diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index f88276a90..4dca8053e 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -1,3 +1,10 @@ 0.01: New App! 0.02: Load widgets after setUI so widclk knows when to hide 0.03: Clock now shows day of week under date. +0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too. +0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off) + when weekday name "Off": week #: + when weekday name "On": weekday name is cut at 6th position and .# is added +0.06: fixes #1271 - wrong settings name + when weekday name and calendar weeknumber are on then display is # + week is buffered until date or timezone changes \ No newline at end of file diff --git a/apps/antonclk/README.md b/apps/antonclk/README.md new file mode 100644 index 000000000..28a38f5fd --- /dev/null +++ b/apps/antonclk/README.md @@ -0,0 +1,79 @@ +# Anton Clock - Large font digital watch with seconds and date + +Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit. + +## Features + +The basic time representation only shows hours and minutes of the current time. However, Anton clock can show additional information: + +* Seconds can be shown, either always or only if the screen is unlocked. +* To help easy recognition, the seconds can be coloured in blue on the Bangle.js 2. +* Date can be shown in three different formats: + * ISO-8601: 2021-12-19 + * short local format: 19/12/2021, 19.12.2021 + * long local format: DEC 19 2021 +* Weekday can be shown (on seconds screen only instead of year) + +## Usage + +Install Anton clock through the Bangle.js app loader. +Configure it through the default Bangle.js configuration mechanism +(Settings app, "Apps" menu, "Anton clock" submenu). +If you like it, make it your default watch face +(Settings app, "System" menu, "Clock" submenu, select "Anton clock"). + +## Configuration + +Anton clock is configured by the standard settings mechanism of Bangle.js's operating system: +Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu. +You configure Anton clock through several "on/off" switches in two menus. + +### The main menu + +The main menu contains several settings covering Anton clock in general. + +* **Seconds...** - Opens the submenu for configuring the presentation of the current time's seconds. +* **Date** - Format of the date representation. Possible values are + * **Long** - "Long" date format in the current locale. Usually with the month as name, not number. + * **Short** - "Short" date format in the current locale. Usually with the month as number. + * **ISO8601** - Show the date in ISO-8601 format (YYYY-MM-DD), irrespective of the current locale. +* **Show Weekday** - Weekday is shown in the time presentation without seconds. +Weekday name depends on the current locale. +If seconds are shown, the weekday is never shown as there is not enough space on the watch face. +* **Show CalWeek** - Week-number (ISO-8601) is shown. (default: Off) +If "Show Weekday" is "Off" displays the week-number as "week #". +If "Show Weekday" is "On" displays "weekday name short" with " #" . +If seconds are shown, the week number is never shown as there is not enough space on the watch face. +* **Vector font** - Use the built-in vector font for dates and weekday. +This can improve readability. +Otherwise, a scaled version of the built-in 6x8 pixels font is used. + +### The "Seconds" submenu + +The "Seconds" submenu configures how (and if) seconds are shown on the "Anton" watch face. + +* **Show** - Configure when the seconds should be shown at all: + * **Never** - Seconds are never shown. +In this case, hour and minute are a bit more centered on the screen and the clock will always only update every minute. +This saves battery power. + * **Unlocked** - Seconds are shown if the display is unlocked. +On locked displays, only hour, minutes, date and optionally the weekday are shown. +_This option is highly recommended on the Bangle.js 2!_ + * **Always** - Seconds are _always_ shown, irrespective of the display's unlock state. +_Enabling this option increases power consumption as the watch face will update once per second instead of once per minute._ +* **With ":"** - If enabled, a colon ":" is prepended to the seconds. +This resembles the usual time representation "hh:mm:ss", even though the seconds are printed on a separate line. +* **Color** - If enabled, seconds are shown in blue instead of black. +If the date is shown on the seconds screen, it is colored read instead of black. +This make the visual orientation much easier on the watch face. +* **Date** - It is possible to show the date together with the seconds: + * **No** - Date is _not_ shown in the seconds screen. +In this case, the seconds are centered below hour and minute. + * **Year** - Date is shown with day, month, and year. If "Date" in the main settings is configured to _ISO8601_, this is used here, too. Otherwise, the short local format is used. + * **Weekday** - Date is shown with day, month, and weekday. + +The date is coloured in red if the "Coloured" option is chosen. + +## Compatibility + +Anton clock makes use of core Bangle.js 2 features (coloured display, display lock state). It also runs on the Bangle.js 1 but these features are not available there due to hardware restrictions. diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js index 7912dfc0f..7b40d8eb5 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -1,61 +1,230 @@ +// Clock with large digits using the "Anton" bold font + +const SETTINGSFILE = "antonclk.json"; + Graphics.prototype.setFontAnton = function(scale) { -// Actual height 69 (68 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78+(scale<<8)+(1<<16)); + // Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16)); +}; + +Graphics.prototype.setFontAntonSmall = function(scale) { + // Actual height 53 (52 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16)); +}; + +// variables defined from settings +var secondsMode; +var secondsColoured; +var secondsWithColon; +var dateOnMain; +var dateOnSecs; +var weekDay; +var calWeek; +var upperCase; +var vectorFont; + +// dynamic variables +var drawTimeout; +var queueMillis = 1000; +var secondsScreen = true; + +var isBangle1 = (process.env.HWVERSION == 1); + +//For development purposes +/* +require('Storage').writeJSON(SETTINGSFILE, { + secondsMode: "Unlocked", // "Never", "Unlocked", "Always" + secondsColoured: true, + secondsWithColon: true, + dateOnMain: "Long", // "Short", "Long", "ISO8601" + dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false + weekDay: true, + calWeek: true, + upperCase: true, + vectorFont: true, +}); +*/ + +// OR (also for development purposes) +/* +require('Storage').erase(SETTINGSFILE); +*/ + +// Load settings +function loadSettings() { + // Helper function default setting + function def (value, def) {return value !== undefined ? value : def;} + + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + secondsMode = def(settings.secondsMode, "Never"); + secondsColoured = def(settings.secondsColoured, true); + secondsWithColon = def(settings.secondsWithColon, true); + dateOnMain = def(settings.dateOnMain, "Long"); + dateOnSecs = def(settings.dateOnSecs, "Year"); + weekDay = def(settings.weekDay, true); + calWeek = def(settings.calWeek, false); + upperCase = def(settings.upperCase, true); + vectorFont = def(settings.vectorFont, false); + + // Legacy + if (dateOnSecs === true) + dateOnSecs = "Year"; + if (dateOnSecs === false) + dateOnSecs = "No"; } -// timeout used to update every minute -var drawTimeout; - -// schedule a draw for the next minute +// schedule a draw for the next second or minute function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); - }, 60000 - (Date.now() % 60000)); + }, queueMillis - (Date.now() % queueMillis)); } - -function draw() { - var x = g.getWidth()/2; - var y = g.getHeight()/2; - g.reset(); - var date = new Date(); - var timeStr = require("locale").time(date,1); - var dateStr = require("locale").date(date).toUpperCase(); - var dowStr = require("locale").dow(date).toUpperCase(); - // draw time - g.setFontAlign(0,0).setFont("Anton"); - g.clearRect(0,y-40,g.getWidth(),y+35); // clear the background - g.drawString(timeStr,x,y); - // draw date - y += 40; - g.setFontAlign(0,0).setFont("6x8",2); - g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background - g.drawString(dateStr,x,y); - //draw day of week - y += 16; - g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background - g.drawString(dowStr,x,y); - // queue draw in one minute - queueDraw(); -} - -// Clear the screen once, at startup -g.clear(); -// draw immediately at first, queue update -draw(); -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ - if (on) { +function updateState() { + if (Bangle.isLCDOn()) { + if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") { + secondsScreen = true; + queueMillis = 1000; + } else { + secondsScreen = false; + queueMillis = 60000; + } draw(); // draw immediately, queue redraw } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } +} + +function isoStr(date) { + return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).substr(-2) + "-" + ("0" + date.getDate()).substr(-2); +} + +var calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested) +function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 + dateNoTime = date; dateNoTime.setHours(0,0,0,0); + if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2]; + calWeekBuffer[0] = date.getTimezoneOffset(); + calWeekBuffer[1] = dateNoTime; + var tdt = new Date(date.valueOf()); + var dayn = (date.getDay() + 6) % 7; + tdt.setDate(tdt.getDate() - dayn + 3); + var firstThursday = tdt.valueOf(); + tdt.setMonth(0, 1); + if (tdt.getDay() !== 4) { + tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); + } + calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000); + return calWeekBuffer[2]; +} + +function doColor() { + return !isBangle1 && !Bangle.isLocked() && secondsColoured; +} + +// Actually draw the watch face +function draw() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0)); + g.reset(); + /* This is to mark the widget areas during development. + g.setColor("#888") + .fillRect(0, 0, g.getWidth(), 23) + .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset(); + /* */ + g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets) + var date = new Date(); // Actually the current date, this one is shown + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time + if (secondsScreen) { + y += 65; + var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).substr(-2); + if (doColor()) + g.setColor(0, 0, 1); + g.setFont("AntonSmall"); + if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left + g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds + y -= (vectorFont ? 15 : 13); + x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2); + var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1)); + var year; + var md; + var yearfirst; + if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year + year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1)); + md = dateStr2.slice(0, -4); + if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12) + md = md.slice(0, -1); + yearfirst = false; + } else { // formatted date begins with year + if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected... + dateStr2 = isoStr(date); // ...use ISO date format instead + year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1)); + md = dateStr2.slice(5); // never keep separator directly after year + yearfirst = true; + } + if (dateOnSecs === "Weekday" && upperCase) + year = year.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + if (doColor()) + g.setColor(1, 0, 0); + g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y)); + g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16))); + } else { + g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered + } + } else { // No seconds screen: Show date and optionally day of week + y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40); + var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1))); + if (upperCase) + dateStr = dateStr.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + g.drawString(dateStr, x, y); + if (calWeek || weekDay) { + var dowcwStr = ""; + if (calWeek) + dowcwStr = " #" + ("0" + ISO8601calWeek(date)).substring(-2); + if (weekDay) + dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01 + else //week #01 + dowcwStr = /*LANG*/"week" + dowcwStr; + if (upperCase) + dowcwStr = dowcwStr.toUpperCase(); + g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16)); + } + } + + // queue next draw + queueDraw(); +} + +// Init the settings of the app +loadSettings(); +// Clear the screen once, at startup +g.clear(); +// Set dynamic state and perform initial drawing +updateState(); +// Register hooks for LCD on/off event and screen lock on/off event +Bangle.on('lcdPower', on => { + updateState(); +}); +Bangle.on('lock', on => { + updateState(); }); // Show launcher when middle button pressed Bangle.setUI("clock"); // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); + +// end of file \ No newline at end of file diff --git a/apps/antonclk/app.png b/apps/antonclk/app.png index d96f17758..a38093c5f 100644 Binary files a/apps/antonclk/app.png and b/apps/antonclk/app.png differ diff --git a/apps/antonclk/screenshot.png b/apps/antonclk/screenshot.png index c66f8bdd8..e949b8a24 100644 Binary files a/apps/antonclk/screenshot.png and b/apps/antonclk/screenshot.png differ diff --git a/apps/antonclk/settings.js b/apps/antonclk/settings.js new file mode 100644 index 000000000..e452b02c7 --- /dev/null +++ b/apps/antonclk/settings.js @@ -0,0 +1,107 @@ +// Settings menu for the enhanced Anton clock + +(function(back) { + var FILE = "antonclk.json"; + // Load settings + var settings = Object.assign({ + secondsOnUnlock: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "Anton clock" + }, + "< Back": () => back(), + "Seconds...": () => E.showMenu(secmenu), + "Date": stringInSettings("dateOnMain", ["Short", "Long", "ISO8601"]), + "Show Weekday": { + value: (settings.weekDay !== undefined ? settings.weekDay : true), + format: v => v ? "On" : "Off", + onchange: v => { + settings.weekDay = v; + writeSettings(); + } + }, + "Show CalWeek": { + value: (settings.calWeek !== undefined ? settings.calWeek : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.calWeek = v; + writeSettings(); + } + }, + "Uppercase": { + value: (settings.upperCase !== undefined ? settings.upperCase : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.upperCase = v; + writeSettings(); + } + }, + "Vector font": { + value: (settings.vectorFont !== undefined ? settings.vectorFont : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.vectorFont = v; + writeSettings(); + } + }, + }; + + // Submenu + var secmenu = { + "": { + "title": "Show seconds..." + }, + "< Back": () => E.showMenu(mainmenu), + "Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]), + "With \":\"": { + value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.secondsWithColon = v; + writeSettings(); + } + }, + "Color": { + value: (settings.secondsColoured !== undefined ? settings.secondsColoured : false), + format: v => v ? "On" : "Off", + onchange: v => { + settings.secondsColoured = v; + writeSettings(); + } + }, + "Date": stringInSettings("dateOnSecs", ["No", "Year", "Weekday"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog index 5560f00bc..739ccf915 100644 --- a/apps/assistedgps/ChangeLog +++ b/apps/assistedgps/ChangeLog @@ -1 +1,3 @@ 0.01: New App! +0.02: Update to work with Bangle.js 2 +0.03: Select GNSS systems to use for Bangle.js 2 diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index 139c232af..80d68a71f 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -8,34 +8,72 @@

GPS can take a long time (~5 minutes) to get an accurate position the first time it is used. AGPS uploads a few hints to the GPS receiver about satellite positions that allow it to get a faster, more accurate fix - however they are only valid for a short period of time.

-

You can upload data that covers a longer period of time, but the upload will take longer.

-
- - - - - + -

Click

+ + diff --git a/apps/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog new file mode 100644 index 000000000..5f1d3bd7d --- /dev/null +++ b/apps/banglexercise/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add sit ups + Add more feedback to the user about the exercises + Clean up code diff --git a/apps/banglexercise/README.md b/apps/banglexercise/README.md new file mode 100644 index 000000000..c9f9ec38a --- /dev/null +++ b/apps/banglexercise/README.md @@ -0,0 +1,40 @@ +# BanglExercise + +Can automatically track exercises while wearing the Bangle.js watch. + +Currently only push ups, curls and sit ups are supported. + +## Disclaimer + +This app is experimental but it seems to work quiet reliable for me. +It could be and is likely that the threshold values for detecting exercises do not work for everyone. +Therefore it would be great if we could improve this app together :-) + + +## Usage + +Select the exercise type you want to practice and go for it! +Press stop to end your exercise. + + +## Screenshots +![](screenshot.png) + +## TODO +* Add other exercise types: + * Rope jumps + * Star jumps + * ... +* Save exercise summaries to file system +* Configure daily goal for exercises +* Find a nicer icon + + +## Contribute +Feel free to send in improvements and remarks. + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icons +Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/banglexercise/app-icon.js b/apps/banglexercise/app-icon.js new file mode 100644 index 000000000..e1923bf54 --- /dev/null +++ b/apps/banglexercise/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIbYh/8AYM/+EP/wFBv4FB/4FB/4FHAwIEBAv4FPAgIGCAosHAofggYFD4EABgXgOgIFLDAQWBAo0BAoOAVIV/UYQABj/4AocDCwQFTg46CEY4vFAopBBApIAVA==")) diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js new file mode 100644 index 000000000..bc6e35f07 --- /dev/null +++ b/apps/banglexercise/app.js @@ -0,0 +1,384 @@ +const Layout = require("Layout"); +const heatshrink = require('heatshrink'); +const storage = require('Storage'); + +let tStart; +let historyY = []; +let historyZ = []; +let historyAvgY = []; +let historyAvgZ = []; +let historySlopeY = []; +let historySlopeZ = []; + +let lastZeroPassCameFromPositive; +let lastZeroPassTime = 0; + +let lastExerciseCompletionTime = 0; +let lastExerciseHalfCompletionTime = 0; + +let exerciseType = { + "id": "", + "name": "" +}; + +// add new exercises here: +const exerciseTypes = [{ + "id": "pushup", + "name": "push ups", + "useYaxis": true, + "useZaxis": false, + "threshold": 2500, + "thresholdMinTime": 800, // mininmal time between two push ups in ms + "thresholdMaxTime": 5000, // maximal time between two push ups in ms + "thresholdMinDurationTime": 600, // mininmal duration of half a push up in ms + }, + { + "id": "curl", + "name": "curls", + "useYaxis": true, + "useZaxis": false, + "threshold": 2500, + "thresholdMinTime": 800, // mininmal time between two curls in ms + "thresholdMaxTime": 5000, // maximal time between two curls in ms + "thresholdMinDurationTime": 500, // mininmal duration of half a curl in ms + }, + { + "id": "situp", + "name": "sit ups", + "useYaxis": false, + "useZaxis": true, + "threshold": 3500, + "thresholdMinTime": 800, // mininmal time between two sit ups in ms + "thresholdMaxTime": 5000, // maximal time between two sit ups in ms + "thresholdMinDurationTime": 500, // mininmal duration of half a sit up in ms + } +]; +let exerciseCounter = 0; + +let layout; +let recordActive = false; + +// Size of average window for data analysis +const avgSize = 6; + +let hrtValue; + +let settings = storage.readJSON("banglexercise.json", 1) || { + 'buzz': true +}; + +function showMainMenu() { + let menu; + menu = { + "": { + title: "BanglExercise" + } + }; + + exerciseTypes.forEach(function(et) { + menu[et.name] = function() { + exerciseType = et; + E.showMenu(); + startTraining(); + }; + }); + + if (exerciseCounter > 0) { + menu["--------"] = { + value: "" + }; + menu["Last:"] = { + value: exerciseCounter + " " + exerciseType.name + }; + } + menu.exit = function() { + load(); + }; + + E.showMenu(menu); +} + +function accelHandler(accel) { + if (!exerciseType) return; + const t = Math.round(new Date().getTime()); // time in ms + const y = exerciseType.useYaxis ? accel.y * 8192 : 0; + const z = exerciseType.useZaxis ? accel.z * 8192 : 0; + //console.log(t, y, z); + + if (exerciseType.useYaxis) { + while (historyY.length > avgSize) + historyY.shift(); + + historyY.push(y); + + if (historyY.length > avgSize / 2) { + const avgY = E.sum(historyY) / historyY.length; + historyAvgY.push([t, avgY]); + while (historyAvgY.length > avgSize) + historyAvgY.shift(); + } + } + + if (exerciseType.useZaxis) { + while (historyZ.length > avgSize) + historyZ.shift(); + + historyZ.push(z); + + if (historyZ.length > avgSize / 2) { + const avgZ = E.sum(historyZ) / historyZ.length; + historyAvgZ.push([t, avgZ]); + while (historyAvgZ.length > avgSize) + historyAvgZ.shift(); + } + } + + // slope for Y + if (exerciseType.useYaxis) { + let l = historyAvgY.length; + if (l > 1) { + const p1 = historyAvgY[l - 2]; + const p2 = historyAvgY[l - 1]; + const slopeY = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000); + // we use this data for exercises which can be detected by using Y axis data + isValidExercise(slopeY, t); + } + } + + // slope for Z + if (exerciseType.useZaxis) { + l = historyAvgZ.length; + if (l > 1) { + const p1 = historyAvgZ[l - 2]; + const p2 = historyAvgZ[l - 1]; + const slopeZ = (p2[1] - p1[1]) / (p2[0] / 1000 - p1[0] / 1000); + // we use this data for some exercises which can be detected by using Z axis data + isValidExercise(slopeZ, t); + } + } +} + +/* + * Check if slope value of Y-axis or Z-axis data (depending on exercise type) looks like an exercise + * + * In detail we look for slop values which are bigger than the configured threshold for the current exercise type + * Then we look for two consecutive slope values of which one is above 0 and the other is below zero. + * If we find one pair of these values this could be part of one exercise. + * Then we look for a pair of values which cross the zero from the otherwise direction + */ +function isValidExercise(slope, t) { + if (!exerciseType) return; + + const threshold = exerciseType.threshold; + const historySlopeValues = exerciseType.useYaxis ? historySlopeY : historySlopeZ; + const thresholdMinTime = exerciseType.thresholdMinTime; + const thresholdMaxTime = exerciseType.thresholdMaxTime; + const thresholdMinDurationTime = exerciseType.thresholdMinDurationTime; + const exerciseName = exerciseType.name; + + + if (Math.abs(slope) >= threshold) { + historySlopeValues.push([t, slope]); + //console.log(t, Math.abs(slope)); + + const lSlopeHistory = historySlopeValues.length; + if (lSlopeHistory > 1) { + const p1 = historySlopeValues[lSlopeHistory - 1][1]; + const p2 = historySlopeValues[lSlopeHistory - 2][1]; + if (p1 > 0 && p2 < 0) { + if (lastZeroPassCameFromPositive == false) { + lastExerciseHalfCompletionTime = t; + console.log(t, exerciseName + " half complete..."); + + layout.progress.label = "½"; + layout.recording.label = "TRAINING"; + g.clear(); + layout.render(); + } + + lastZeroPassCameFromPositive = true; + lastZeroPassTime = t; + } + if (p2 > 0 && p1 < 0) { + if (lastZeroPassCameFromPositive == true) { + const tDiffLastExercise = t - lastExerciseCompletionTime; + const tDiffStart = t - tStart; + console.log(t, exerciseName + " maybe complete?", Math.round(tDiffLastExercise), Math.round(tDiffStart)); + + // check minimal time between exercises: + if ((lastExerciseCompletionTime <= 0 && tDiffStart >= thresholdMinTime) || tDiffLastExercise >= thresholdMinTime) { + + // check maximal time between exercises: + if (lastExerciseCompletionTime <= 0 || tDiffLastExercise <= thresholdMaxTime) { + + // check minimal duration of exercise: + const tDiffExerciseHalfCompletion = t - lastExerciseHalfCompletionTime; + if (tDiffExerciseHalfCompletion > thresholdMinDurationTime) { + //console.log(t, exerciseName + " complete!!!"); + + lastExerciseCompletionTime = t; + exerciseCounter++; + + layout.count.label = exerciseCounter; + layout.progress.label = ""; + layout.recording.label = "Good!"; + + g.clear(); + layout.render(); + + if (settings.buzz) + Bangle.buzz(200, 0.5); + } else { + console.log(t, exerciseName + " too quick for duration time threshold!"); // thresholdMinDurationTime + lastExerciseCompletionTime = t; + + layout.recording.label = "Go slower!"; + g.clear(); + layout.render(); + } + } else { + console.log(t, exerciseName + " top slow for time threshold!"); // thresholdMaxTime + lastExerciseCompletionTime = t; + + layout.recording.label = "Go faster!"; + g.clear(); + layout.render(); + } + } else { + console.log(t, exerciseName + " too quick for time threshold!"); // thresholdMinTime + lastExerciseCompletionTime = t; + + layout.recording.label = "Go slower!"; + g.clear(); + layout.render(); + } + } + + lastZeroPassCameFromPositive = false; + lastZeroPassTime = t; + } + } + } +} + + +function reset() { + historyY = []; + historyZ = []; + historyAvgY = []; + historyAvgZ = []; + historySlopeY = []; + historySlopeZ = []; + + lastZeroPassCameFromPositive = undefined; + lastZeroPassTime = 0; + lastExerciseHalfCompletionTime = 0; + lastExerciseCompletionTime = 0; + exerciseCounter = 0; + tStart = 0; +} + + +function startTraining() { + if (recordActive) return; + g.clear(1); + reset(); + Bangle.setLCDTimeout(0); // force LCD on + Bangle.setHRMPower(1, "banglexercise"); + if (!hrtValue) hrtValue = "..."; + + layout = new Layout({ + type: "v", + c: [{ + type: "txt", + id: "type", + font: "6x8:2", + label: exerciseType.name, + pad: 5 + }, + { + type: "h", + c: [{ + type: "txt", + id: "count", + font: exerciseCounter < 100 ? "6x8:9" : "6x8:8", + label: exerciseCounter, + pad: 5 + }, + { + type: "txt", + id: "progress", + font: "6x8:2", + label: "", + pad: 5 + }, + ] + }, + { + type: "h", + c: [{ + type: "img", + pad: 4, + src: function() { + return heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM")); + } + }, + { + type: "txt", + id: "hrtRate", + font: "6x8:2", + label: hrtValue, + pad: 5 + }, + ] + }, + { + type: "txt", + id: "recording", + font: "6x8:2", + label: "TRAINING", + bgCol: "#f00", + pad: 5, + fillx: 1 + }, + ] + }, { + btns: [{ + label: "STOP", + cb: () => { + stopTraining(); + } + }], + lazy: false + }); + layout.render(); + + Bangle.setPollInterval(80); // 12.5 Hz + + tStart = new Date().getTime(); + recordActive = true; + if (settings.buzz) + Bangle.buzz(200, 1); + + // delay start a little bit + setTimeout(() => { + Bangle.on('accel', accelHandler); + }, 1000); +} + +function stopTraining() { + if (!recordActive) return; + + g.clear(1); + Bangle.removeListener('accel', accelHandler); + Bangle.setHRMPower(0, "banglexercise"); + showMainMenu(); + recordActive = false; +} + +Bangle.on('HRM', function(hrm) { + hrtValue = hrm.bpm; +}); + +g.clear(1); +showMainMenu(); diff --git a/apps/banglexercise/app.png b/apps/banglexercise/app.png new file mode 100644 index 000000000..ee7332063 Binary files /dev/null and b/apps/banglexercise/app.png differ diff --git a/apps/banglexercise/screenshot.png b/apps/banglexercise/screenshot.png new file mode 100644 index 000000000..417be685b Binary files /dev/null and b/apps/banglexercise/screenshot.png differ diff --git a/apps/banglexercise/settings.js b/apps/banglexercise/settings.js new file mode 100644 index 000000000..3208c6eca --- /dev/null +++ b/apps/banglexercise/settings.js @@ -0,0 +1,21 @@ +(function(back) { + const SETTINGS_FILE = "banglexercise.json"; + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + function save(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + E.showMenu({ + '': { 'title': 'BanglExercise' }, + '< Back': back, + 'Buzz': { + value: "buzz" in settings ? settings.buzz : false, + format: () => (settings.buzz ? 'Yes' : 'No'), + onchange: () => { + settings.buzz = !settings.buzz; + save('buzz', settings.buzz); + } + } + }); +}); diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 5c929421b..702a8091e 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -43,3 +43,5 @@ 0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app 0.38: Option to log to file if settings.log==2 0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035) +0.40: Bootloader now rebuilds for new firmware versions +0.41: Add Keyboard and Mouse Bluetooth HID option diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index e338d9020..1b826de5a 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -6,11 +6,11 @@ var s = require('Storage').readJSON('setting.json',1)||{}; var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 var boot = ""; if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed - var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`; + var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } else { - var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/)); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`; + var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; boot += `E.setFlags({pretokenise:1});\n`; @@ -18,6 +18,7 @@ boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; if (s.ble!==false) { if (s.HID) { // Human interface device if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; + else if (s.HID=="com") boot += `Bangle.HID = E.toUint8Array(atob("BQEJAqEBhQEJAaEABQkZASkFFQAlAZUFdQGBApUBdQOBAwUBCTAJMQk4FYElf3UIlQOBBgUMCjgCFYElf3UIlQGBBsDABQEJBqEBhQIFBxngKecVACUBdQGVCIECdQiVAYEBGQApcxUAJXOVBXUIgQDA"));` else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));` else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`; boot += `bleServiceOptions.hid=Bangle.HID;\n`; diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 5560f00bc..481d855c8 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -1 +1,7 @@ 0.01: New App! +0.02: Make overriding the HRM event optional + Emit BTHRM event for external sensor + Add recorder app plugin +0.03: Prevent readings from internal sensor mixing into BT values + Mark events with src property + Show actual source of event in app diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index 88e574480..fbc872630 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -2,24 +2,42 @@ var log = function() {};//print var gatt; var status; - - Bangle.isHRMOn = function() { + + var origIsHRMOn = Bangle.isHRMOn; + + Bangle.isBTHRMOn = function(){ return (status=="searching" || status=="connecting") || (gatt!==undefined); } - Bangle.setHRMPower = function(isOn, app) { + + Bangle.isHRMOn = function() { + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + + if (settings.enabled && !settings.replace){ + return origIsHRMOn(); + } else if (settings.enabled && settings.replace){ + return Bangle.isBTHRMOn(); + } + return origIsHRMOn() || Bangle.isBTHRMOn(); + } + + Bangle.setBTHRMPower = function(isOn, app) { + + + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + // Do app power handling if (!app) app="?"; - log("setHRMPower ->", isOn, app); + log("setBTHRMPower ->", isOn, app); if (Bangle._PWR===undefined) Bangle._PWR={}; - if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[]; - if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app); - if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app); - isOn = Bangle._PWR.HRM.length; + if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; + if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); + if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app); + isOn = Bangle._PWR.BTHRM.length; // so now we know if we're really on if (isOn) { - log("setHRMPower on", app); - if (!Bangle.isHRMOn()) { - log("HRM not already on"); + log("setBTHRMPower on", app); + if (!Bangle.isBTHRMOn()) { + log("BTHRM not already on"); status = "searching"; NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) { log("Found device "+device.id); @@ -49,10 +67,12 @@ if (flags&16) { var interval = dv.getUint16(idx,1); // in milliseconds }*/ - Bangle.emit('HRM',{ + + Bangle.emit(settings.replace?"HRM":"BTHRM", { bpm:bpm, - confidence:100 - }); + confidence:100, + src:settings.replace?"bthrm":undefined + }); }); return characteristic.startNotifications(); }).then(function() { @@ -65,15 +85,39 @@ }); } } else { // not on - log("setHRMPower off", app); + log("setBTHRMPower off", app); if (gatt) { - log("HRM connected - disconnecting"); + log("BTHRM connected - disconnecting"); status = undefined; try {gatt.disconnect();}catch(e) { - log("HRM disconnect error", e); + log("BTHRM disconnect error", e); } gatt = undefined; } } }; + + var origSetHRMPower = Bangle.setHRMPower; + + Bangle.setHRMPower = function(isOn, app) { + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + if (settings.enabled || !isOn){ + Bangle.setBTHRMPower(isOn, app); + } + if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){ + origSetHRMPower(isOn, app); + } + } + + var settings = require('Storage').readJSON("bthrm.json", true) || {}; + if (settings.enabled && settings.replace){ + if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){ + for (var i = 0; i < Bangle._PWR.HRM.length; i++){ + var app = Bangle._PWR.HRM[i]; + origSetHRMPower(0, app); + Bangle.setBTHRMPower(1, app); + if (Bangle._PWR.HRM===undefined) break; + } + } +} })(); diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js new file mode 100644 index 000000000..712344b11 --- /dev/null +++ b/apps/bthrm/bthrm.js @@ -0,0 +1,61 @@ +var btm = g.getHeight()-1; +var eventInt = null; +var eventBt = null; +var counterInt = 0; +var counterBt = 0; + + +function draw(y, event, type, counter) { + var px = g.getWidth()/2; + g.reset(); + g.setFontAlign(0,0); + g.clearRect(0,y,g.getWidth(),y+75); + if (type == null || event == null || counter == 0) return; + var str = event.bpm + ""; + g.setFontVector(40).drawString(str,px,y+20); + str = "Confidence: " + event.confidence; + g.setFontVector(12).drawString(str,px,y+50); + str = "Event: " + type; + if (type == "HRM") str += " Source: " + (event.src ? event.src : "internal"); + g.setFontVector(12).drawString(str,px,y+60); +} + +function onBtHrm(e) { + print("Event for BT " + JSON.stringify(e)); + counterBt += 5; + eventBt = e; +} + +function onHrm(e) { + print("Event for Int " + JSON.stringify(e)); + counterInt += 5; + eventInt = e; +} + +Bangle.on('BTHRM', onBtHrm); +Bangle.on('HRM', onHrm); + +Bangle.setHRMPower(1,'bthrm') + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.reset().setFont("6x8",2).setFontAlign(0,0); +g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); + +function drawInt(){ + counterInt--; + if (counterInt < 0) counterInt = 0; + if (counterInt > 5) counterInt = 5; + draw(24, eventInt, "HRM", counterInt); +} +function drawBt(){ + counterBt--; + if (counterBt < 0) counterBt = 0; + if (counterBt > 5) counterBt = 5; + draw(100, eventBt, "BTHRM", counterBt); +} + +var interval = setInterval(drawInt, 1000); +var interval = setInterval(drawBt, 1000); diff --git a/apps/bthrm/recorder.js b/apps/bthrm/recorder.js new file mode 100644 index 000000000..b1c27660d --- /dev/null +++ b/apps/bthrm/recorder.js @@ -0,0 +1,27 @@ +(function(recorders) { + recorders.bthrm = function() { + var bpm = ""; + function onHRM(h) { + bpm = h.bpm; + } + return { + name : "BTHR", + fields : ["BT Heartrate"], + getValues : () => { + result = [bpm]; + bpm = ""; + return result; + }, + start : () => { + Bangle.on('BTHRM', onHRM); + Bangle.setBTHRMPower(1,"recorder"); + }, + stop : () => { + Bangle.removeListener('BTHRM', onHRM); + Bangle.setBTHRMPower(0,"recorder"); + }, + draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) + }; + } +}) + diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js new file mode 100644 index 000000000..8cb00614e --- /dev/null +++ b/apps/bthrm/settings.js @@ -0,0 +1,33 @@ +(function(back) { + var FILE = "bthrm.json"; + + var settings = Object.assign({ + enabled: true, + replace: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Bluetooth HRM' }, + '< Back': back, + 'Use BT HRM': { + value: !!settings.enabled, + format: v => settings.enabled ? "On" : "Off", + onchange: v => { + settings.enabled = v; + writeSettings(); + } + }, + 'Use HRM event': { + value: !!settings.replace, + format: v => settings.replace ? "On" : "Off", + onchange: v => { + settings.replace = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 3deec00e0..beba4ed95 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -3,3 +3,4 @@ 0.03: Add setting to start week on Sunday 0.04: Add setting to switch color schemes. On Bangle 2 non-dithering colors will be used by default. Use localized names for months and days of the week (Language app needed). 0.05: Update calendar weekend colors for start on Sunday +0.06: Use larger font for dates diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 00e7d54cc..62702e349 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -206,6 +206,8 @@ function drawCalendar(date) { y2 - 1 ); } + require("Font8x12").add(Graphics); + g.setFont("8x12", fontSize); g.setColor(day < 50 ? fgOtherMonth : fgSameMonth); g.drawString( (day > 50 ? day - 50 : day).toString(), diff --git a/apps/calendar/screenshot_calendar.png b/apps/calendar/screenshot_calendar.png index 8285932c4..7ef5986d4 100644 Binary files a/apps/calendar/screenshot_calendar.png and b/apps/calendar/screenshot_calendar.png differ diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index c0aa4e2f8..5464a8103 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -1,3 +1,9 @@ 0.01: New clock 0.02: Fix icon & add battery warn functionality 0.03: Theming support & minor fixes +0.04: Make configurable what to show in each circle + Add step distance and weather + Allow switching visibility of widgets + Make circles and text slightly bigger +0.05: Show correct percentage values in circles + Show humidity as weather circle data diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 66d9afe08..c3704e3d7 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -2,19 +2,25 @@ A clock with circles for different data at the bottom in a probably familiar style -It shows besides time, date and day of week the following information: +By default the time, date and day of week is shown. + +It can show the following information (this can be configured): * Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer)) - * Heart rate (when screen is on and unlocked) - * Battery (including charging and battery low) + * Steps distance (depending on steps) + * Heart rate (automatically updates when screen is on and unlocked) + * Battery (including charging status and battery low warning) + * Weather (requires [weather app](https://banglejs.com/apps/#weather)) + * Humidity as circle progress + * Temperature inside circle + * Condition as icon below circle -## Screenshot +## Screenshots +![Screenshot dark theme](screenshot-dark.png) +![Screenshot light theme](screenshot-light.png) -![Screenshot](screenshot.png) - -## TODO -* Show weather information -* Configure which information to show in each circle -* Configure visibility of widgets +# TODO +* Add sunrise and sunset +* Display moon instead of sun during night on weather circle ## Creator Marco ([myxor](https://github.com/myxor)) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 026b47cc6..88a04d4b9 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,19 +1,37 @@ const locale = require("locale"); const heatshrink = require("heatshrink"); +const storage = require("Storage"); const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA=")); +const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD")); const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM")); const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI")); const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI")); const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA")); +const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA")); +const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA=")); +const weatherPartlyCloudy = heatshrink.decompress(atob("iEQwYQNv0AjgGDn4EDh///gFChwREC4MfxwIBv0//+AC4X4j4FCv/AgfwgED/wIBuAaBBwgFDgP4gf/AAXABwIEBDQQAEA==")); +const weatherRainy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AocAnAFBFIU4EAM//gRBEAIOBhw1C/AmDAosAC4JNIAAg")); +const weatherPartlyRainy = heatshrink.decompress(atob("h0OwYJGjkAnAFCj+AAgU//4FCuEA8EAg8ch/4gEB4////AAoIIBCIMD/wgCg4bBg/8BwMD+AgBh4ZBDQf/FIIABh4IBgAA==")); +const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN/4EDx/4j/x4EAgIIBwAXBAogRFDoopFGoxBGABIA=")); +const weatherFoggy = heatshrink.decompress(atob("iEQwYROn/8AgUB/EfwAFBh/AgfwgED/wIBuEABwd/4EcDQgFDgE4Fosf///8f//A/Lj/xCQIRNA=")); +const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AoX8gE4AoQpBnAdBF4IRBDQMH/kOHgY7DAo4AOA==")); + let settings; function loadSettings() { - settings = require("Storage").readJSON("circlesclock.json", 1) || { + settings = storage.readJSON("circlesclock.json", 1) || { + 'minHR': 40, 'maxHR': 200, 'stepGoal': 10000, - 'batteryWarn': 30 + 'stepDistanceGoal': 8000, + 'stepLength': 0.8, + 'batteryWarn': 30, + 'showWidgets': false, + 'circle1': 'hr', + 'circle2': 'steps', + 'circle3': 'battery' }; // Load step goal from pedometer widget as fallback if (settings.stepGoal == undefined) { @@ -21,122 +39,229 @@ function loadSettings() { settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; } } +loadSettings(); +const showWidgets = settings.showWidgets || false; +let hrtValue; + +// layout values: const colorFg = g.theme.dark ? '#fff' : '#000'; const colorBg = g.theme.dark ? '#000' : '#fff'; const colorGrey = '#808080'; const colorRed = '#ff0000'; -const colorGreen = '#00ff00'; - -let hrtValue; - -const h = g.getHeight(); +const colorGreen = '#008000'; +const colorBlue = '#0000ff'; +const colorYellow = '#ffff00'; +const widgetOffset = showWidgets ? 24 : 0; +const h = g.getHeight() - widgetOffset; const w = g.getWidth(); -const hOffset = 30; +const hOffset = 30 - widgetOffset; const h1 = Math.round(1 * h / 5 - hOffset); const h2 = Math.round(3 * h / 5 - hOffset); -const h3 = Math.round(8 * h / 8 - hOffset); -const w1 = Math.round(w / 6); -const w2 = Math.round(3 * w / 6); -const w3 = Math.round(5 * w / 6); -const radiusOuter = 22; -const radiusInner = 16; +const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position +const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions +const radiusOuter = 25; +const radiusInner = 20; +const circleFont = "Vector:15"; +const circleFontBig = "Vector:16"; +const circleFontSmall = "Vector:13"; function draw() { - g.reset(); + g.clear(true); + + if (!showWidgets) { + /* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * area to the top bar doesn't get cleared. + */ + if (WIDGETS && typeof WIDGETS === "object") { + for (let wd of WIDGETS) { + wd.draw = () => {}; + wd.area = ""; + } + } + } else { + Bangle.drawWidgets(); + } + g.setColor(colorBg); - g.fillRect(0, 0, w, h); + g.fillRect(0, widgetOffset, w, h); // time g.setFont("Vector:50"); - g.setFontAlign(-1, -1); + g.setFontAlign(0, -1); g.setColor(colorFg); - g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8); + g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8); // date & dow - g.setFont("Vector:20"); + g.setFont("Vector:21"); g.setFontAlign(-1, 0); - g.drawString(locale.date(new Date()), w / 10, h2); - g.drawString(locale.dow(new Date()), w / 10, h2 + 22); + g.drawString(locale.date(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2); + g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + 22); - // Steps circle - drawSteps(); - - // Heart circle - drawHeartRate(); - - // Battery circle - drawBattery(); + drawCircle(1); + drawCircle(2); + drawCircle(3); } +const defaultCircleTypes = ["steps", "hr", "battery"]; +function drawCircle(index) { + let type = settings['circle' + index]; + if (!type) type = defaultCircleTypes[index - 1]; + const w = getCirclePosition(type); + switch (type) { + case "steps": + drawSteps(w); + break; + case "stepsDist": + drawStepsDistance(w); + break; + case "hr": + drawHeartRate(w); + break; + case "battery": + drawBattery(w); + break; + case "weather": + drawWeather(w); + break; + } +} -function drawSteps() { +function getCirclePosition(type) { + for (let i = 1; i <= 3; i++) { + const setting = settings['circle' + i]; + if (setting == type) return circlePosX[i - 1]; + } + for (let i = 0; i < defaultCircleTypes.length; i++) { + if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) { + return circlePosX[i]; + } + } + return undefined; +} + +function isCircleEnabled(type) { + return getCirclePosition(type) != undefined; +} + +function drawSteps(w) { + if (!w) w = getCirclePosition("steps"); const steps = getSteps(); - const blue = '#0000ff'; + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.setColor(colorGrey); - g.fillCircle(w1, h3, radiusOuter); + g.fillCircle(w, h3, radiusOuter); const stepGoal = settings.stepGoal || 10000; if (stepGoal > 0) { let percent = steps / stepGoal; if (stepGoal < steps) percent = 1; - drawGauge(w1, h3, percent, blue); + drawGauge(w, h3, percent, colorBlue); } g.setColor(colorBg); - g.fillCircle(w1, h3, radiusInner); + g.fillCircle(w, h3, radiusInner); - g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]); + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); - g.setFont("Vector:12"); + g.setFont(circleFont); g.setFontAlign(0, 0); g.setColor(colorFg); - g.drawString(shortValue(steps), w1 + 2, h3); + g.drawString(shortValue(steps), w + 2, h3); - g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6); + g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6); } -function drawHeartRate() { +function drawStepsDistance(w) { + if (!w) w = getCirclePosition("steps"); + const steps = getSteps(); + const stepDistance = settings.stepLength || 0.8; + const stepsDistance = Math.round(steps * stepDistance); + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.setColor(colorGrey); - g.fillCircle(w2, h3, radiusOuter); + g.fillCircle(w, h3, radiusOuter); + + const stepDistanceGoal = settings.stepDistanceGoal || 8000; + if (stepDistanceGoal > 0) { + let percent = stepsDistance / stepDistanceGoal; + if (stepDistanceGoal < stepsDistance) percent = 1; + drawGauge(w, h3, percent, colorGreen); + } + + g.setColor(colorBg); + g.fillCircle(w, h3, radiusInner); + + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); + + g.setFont(circleFont); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(shortValue(stepsDistance), w + 2, h3); + + g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6); +} + +function drawHeartRate(w) { + if (!w) w = getCirclePosition("hr"); + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + + g.setColor(colorGrey); + g.fillCircle(w, h3, radiusOuter); if (hrtValue != undefined && hrtValue > 0) { - const minHR = 40; + const minHR = settings.minHR || 40; const percent = (hrtValue - minHR) / (settings.maxHR - minHR); - drawGauge(w2, h3, percent, colorRed); + drawGauge(w, h3, percent, colorRed); } g.setColor(colorBg); - g.fillCircle(w2, h3, radiusInner); + g.fillCircle(w, h3, radiusInner); - g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]); + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); - g.setFont("Vector:12"); + g.setFont(circleFontBig); g.setFontAlign(0, 0); g.setColor(colorFg); - g.drawString(hrtValue != undefined ? hrtValue : "-", w2, h3); + g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3); - g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6); + g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6); } -function drawBattery() { +function drawBattery(w) { + if (!w) w = getCirclePosition("battery"); const battery = E.getBattery(); - const yellow = '#ffff00'; + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.setColor(colorGrey); - g.fillCircle(w3, h3, radiusOuter); + g.fillCircle(w, h3, radiusOuter); if (battery > 0) { const percent = battery / 100; - drawGauge(w3, h3, percent, yellow); + drawGauge(w, h3, percent, colorYellow); } g.setColor(colorBg); - g.fillCircle(w3, h3, radiusInner); + g.fillCircle(w, h3, radiusInner); - g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]); + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); - g.setFont("Vector:12"); + g.setFont(circleFont); g.setFontAlign(0, 0); let icon = powerIcon; @@ -144,17 +269,100 @@ function drawBattery() { if (Bangle.isCharging()) { color = colorGreen; icon = powerIconGreen; - } - else { + } else { if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) { color = colorRed; icon = powerIconRed; } } g.setColor(color); - g.drawString(battery + '%', w3, h3); + g.drawString(battery + '%', w, h3); - g.drawImage(icon, w3 - 6, h3 + radiusOuter - 6); + g.drawImage(icon, w - 6, h3 + radiusOuter - 6); +} + +function drawWeather(w) { + if (!w) w = getCirclePosition("weather"); + const weather = getWeather(); + const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; + const humidity = weather ? weather.hum : undefined; + const code = weather ? weather.code : -1; + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + + g.setColor(colorGrey); + g.fillCircle(w, h3, radiusOuter); + + if (humidity >= 0) { + drawGauge(w, h3, humidity / 100, colorYellow); + } + + g.setColor(colorBg); + g.fillCircle(w, h3, radiusInner); + + g.fillPoly([w, h3, w - 25, h3 + radiusOuter + 5, w + 25, h3 + radiusOuter + 5]); + + const content = tempString ? tempString : "?"; + g.setFont(content.length < 4 ? circleFont : circleFontSmall); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(content, w, h3); + + if (code > 0) { + const icon = getWeatherIconByCode(code); + if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10); + } +} + +/* + * Choose weather icon to display based on weather conditition code + * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + */ +function getWeatherIconByCode(code) { + const codeGroup = Math.round(code / 100); + switch (codeGroup) { + case 2: + return weatherStormy; + case 3: + return weatherCloudy; + case 5: + switch (code) { + case 511: + return weatherSnowy; + case 520: + return weatherPartlyRainy; + case 521: + return weatherPartlyRainy; + case 522: + return weatherPartlyRainy; + case 531: + return weatherPartlyRainy; + default: + return weatherRainy; + } + break; + case 6: + return weatherSnowy; + case 7: + return weatherFoggy; + case 8: + switch (code) { + case 800: + return weatherSunny; + case 801: + return weatherPartlyCloudy; + case 802: + return weatherPartlyCloudy; + default: + return weatherCloudy; + } + break; + default: + return undefined; + } + return undefined; } function radians(a) { @@ -162,22 +370,21 @@ function radians(a) { } function drawGauge(cx, cy, percent, color) { - let offset = 30; - let end = 300; - var i = 0; - var r = radiusInner + 3; + const offset = 15; + const end = 345; + const r = radiusInner + 3; if (percent <= 0) return; if (percent > 1) percent = 1; - var startrot = -offset; - var endrot = startrot - ((end - offset) * percent) - 15; + const startrot = -offset; + const endrot = startrot - ((end - offset) * percent); g.setColor(color); - const size = 4; + const size = radiusOuter - radiusInner - 2; // draw gauge - for (i = startrot; i > endrot - size; i -= size) { + for (let i = startrot; i > endrot - size; i -= size) { x = cx + r * Math.sin(radians(i)); y = cy + r * Math.cos(radians(i)); g.fillCircle(x, y, size); @@ -198,54 +405,56 @@ function shortValue(v) { } function getSteps() { - if (WIDGETS.wpedom !== undefined) { + if (WIDGETS && WIDGETS.wpedom !== undefined) { return WIDGETS.wpedom.getSteps(); } return 0; } -Bangle.on('lock', function(isLocked) { - if (!isLocked) { - Bangle.setHRMPower(1, "watch"); - if (hrtValue == undefined) { - hrtValue = '...'; - drawHeartRate(); - } - } else { - Bangle.setHRMPower(0, "watch"); - } - drawHeartRate(); - drawSteps(); -}); +function getWeather() { + const jsonWeather = storage.readJSON('weather.json'); + return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; +} -Bangle.on('HRM', function(hrm) { - //if(hrm.confidence > 90){ - hrtValue = hrm.bpm; - if (Bangle.isLCDOn()) +function enableHRMSensor() { + Bangle.setHRMPower(1, "circleclock"); + if (hrtValue == undefined) { + hrtValue = '...'; drawHeartRate(); - //} else { - // hrtValue = undefined; - //} -}); - -Bangle.on('charging', function(charging) { - drawBattery(); -}); - -g.clear(); -Bangle.loadWidgets(); -/* - * we are not drawing the widgets as we are taking over the whole screen - * so we will blank out the draw() functions of each widget and change the - * area to the top bar doesn't get cleared. - */ -if (typeof WIDGETS === "object") { - for (let wd of WIDGETS) { - wd.draw = () => {}; - wd.area = ""; } } -loadSettings(); -setInterval(draw, 60000); -draw(); + +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + if (isCircleEnabled("hr")) { + enableHRMSensor(); + } + draw(); + } else { + Bangle.setHRMPower(0, "circleclock"); + } +}); + + +Bangle.on('HRM', function(hrm) { + if (isCircleEnabled("hr")) { + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + } +}); + + Bangle.setUI("clock"); +Bangle.loadWidgets(); + +draw(); +setInterval(draw, 60000); + +Bangle.on('charging', function(charging) { + if (isCircleEnabled("battery")) drawBattery(); +}); + +if (isCircleEnabled("hr")) { + enableHRMSensor(); +} diff --git a/apps/circlesclock/screenshot-dark.png b/apps/circlesclock/screenshot-dark.png new file mode 100644 index 000000000..00c0e3399 Binary files /dev/null and b/apps/circlesclock/screenshot-dark.png differ diff --git a/apps/circlesclock/screenshot-light.png b/apps/circlesclock/screenshot-light.png new file mode 100644 index 000000000..af47b30a4 Binary files /dev/null and b/apps/circlesclock/screenshot-light.png differ diff --git a/apps/circlesclock/screenshot.png b/apps/circlesclock/screenshot.png deleted file mode 100644 index 94ff885fa..000000000 Binary files a/apps/circlesclock/screenshot.png and /dev/null differ diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index ffda51538..ac4215a8a 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -6,13 +6,26 @@ settings[key] = value; storage.write(SETTINGS_FILE, settings); } + var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"]; + var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"]; E.showMenu({ '': { 'title': 'circlesclock' }, + '< Back': back, + 'min heartrate': { + value: "minHR" in settings ? settings.minHR : 40, + min: 0, + max : 250, + step: 5, + format: x => { + return x; + }, + onchange: x => save('minHR', x), + }, 'max heartrate': { value: "maxHR" in settings ? settings.maxHR : 200, min: 20, max : 250, - step: 10, + step: 5, format: x => { return x; }, @@ -28,7 +41,27 @@ }, onchange: x => save('stepGoal', x), }, - 'battery warn lvl': { + 'step length': { + value: "stepLength" in settings ? settings.stepLength : 0.8, + min: 0.1, + max : 1.5, + step: 0.01, + format: x => { + return x; + }, + onchange: x => save('stepLength', x), + }, + 'step dist goal': { + value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000, + min: 2000, + max : 30000, + step: 1000, + format: x => { + return x; + }, + onchange: x => save('stepDistanceGoal', x), + }, + 'battery warn': { value: "batteryWarn" in settings ? settings.batteryWarn : 30, min: 10, max : 100, @@ -38,6 +71,28 @@ }, onchange: x => save('batteryWarn', x), }, - '< Back': back, + 'show widgets': { + value: "showWidgets" in settings ? settings.showWidgets : false, + format: () => (settings.showWidgets ? 'Yes' : 'No'), + onchange: x => save('showWidgets', x), + }, + 'left': { + value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0, + min: 0, max: 4, + format: v => namesCircleTypes[v], + onchange: x => save('circle1', valuesCircleTypes[x]), + }, + 'middle': { + value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2, + min: 0, max: 4, + format: v => namesCircleTypes[v], + onchange: x => save('circle2', valuesCircleTypes[x]), + }, + 'right': { + value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3, + min: 0, max: 4, + format: v => namesCircleTypes[v], + onchange: x => save('circle3', valuesCircleTypes[x]), + } }); }); diff --git a/apps/colorful_clock/LICENSE b/apps/colorful_clock/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/colorful_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/configurable_clock/BangleApps__apps__variable_clock__README.md b/apps/configurable_clock/BangleApps__apps__variable_clock__README.md new file mode 100644 index 000000000..da5bed56d --- /dev/null +++ b/apps/configurable_clock/BangleApps__apps__variable_clock__README.md @@ -0,0 +1,27 @@ +# Variable Analog Clock # + +This app implements an analog clock with various faces, hands and colors to +choose from. + +You have the choice between: + +* 4 different clock faces ![](Screenshot_01.png) ![](Screenshot_02.png) ![](Screenshot_03.png) ![](Screenshot_04.png) and +* 3 different clock hands (optionally with or without second hands) ![](Screenshot_11.png) ![](Screenshot_12.png) ![](Screenshot_13.png) + +Additionally, you may use the currently configured global theme or configure +your own colors for clock fore- and background and second hands. + +Just swipe up or down to switch from clock display to configuration screen + +![](Screenshot_21.png) ![](Screenshot_22.png) ![](Screenshot_23.png) +![](Screenshot_24.png) ![](Screenshot_25.png) + +Chosen settings will be written to the Bangle.js's flash memory and restored +whenever the clock is started again. + +This clock also acts as an example for the building blocks found in the author's +[GitHub repository](https://github.com/rozek/banglejs-2-activities) + +## License ## + +[MIT License](LICENSE) diff --git a/apps/configurable_clock/LICENSE b/apps/configurable_clock/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/configurable_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/configurable_clock/README.md b/apps/configurable_clock/README.md new file mode 100644 index 000000000..faddd092a --- /dev/null +++ b/apps/configurable_clock/README.md @@ -0,0 +1,29 @@ +# Configurable Analog Clock # + +This app implements an analog clock with various faces, hands and colors to +choose from. + +You have the choice between: + +* 4 different clock faces
![](Screenshot-01.png)   ![](Screenshot-02.png)   ![](Screenshot-03.png)   ![](Screenshot-04.png) and +* 3 different clock hands (optionally with or without second hands)
![](Screenshot-11.png)   ![](Screenshot-12.png)   ![](Screenshot-13.png) + +Additionally, you may use the currently configured global theme or configure +your own colors for clock fore- and background and second hands. + +Just swipe up or down to switch from clock display to the first configuration +screen and continue from there + +![](Screenshot-21.png)   ![](Screenshot-22.png)   +![](Screenshot-23.png)   ![](Screenshot-24.png)   +![](Screenshot-25.png) + +Chosen settings will be written to the Bangle.js's flash memory and restored +whenever the clock is started again. + +This clock also acts as an example for the building blocks found in the author's +[GitHub repository](https://github.com/rozek/banglejs-2-activities) + +## License ## + +[MIT License](LICENSE) diff --git a/apps/configurable_clock/Screenshot-01.png b/apps/configurable_clock/Screenshot-01.png new file mode 100644 index 000000000..b2367784c Binary files /dev/null and b/apps/configurable_clock/Screenshot-01.png differ diff --git a/apps/configurable_clock/Screenshot-02.png b/apps/configurable_clock/Screenshot-02.png new file mode 100644 index 000000000..909a2a04a Binary files /dev/null and b/apps/configurable_clock/Screenshot-02.png differ diff --git a/apps/configurable_clock/Screenshot-03.png b/apps/configurable_clock/Screenshot-03.png new file mode 100644 index 000000000..80407c84f Binary files /dev/null and b/apps/configurable_clock/Screenshot-03.png differ diff --git a/apps/configurable_clock/Screenshot-04.png b/apps/configurable_clock/Screenshot-04.png new file mode 100644 index 000000000..175476c81 Binary files /dev/null and b/apps/configurable_clock/Screenshot-04.png differ diff --git a/apps/configurable_clock/Screenshot-11.png b/apps/configurable_clock/Screenshot-11.png new file mode 100644 index 000000000..bca534613 Binary files /dev/null and b/apps/configurable_clock/Screenshot-11.png differ diff --git a/apps/configurable_clock/Screenshot-12.png b/apps/configurable_clock/Screenshot-12.png new file mode 100644 index 000000000..973b6da5e Binary files /dev/null and b/apps/configurable_clock/Screenshot-12.png differ diff --git a/apps/configurable_clock/Screenshot-13.png b/apps/configurable_clock/Screenshot-13.png new file mode 100644 index 000000000..b87d97712 Binary files /dev/null and b/apps/configurable_clock/Screenshot-13.png differ diff --git a/apps/configurable_clock/Screenshot-21.png b/apps/configurable_clock/Screenshot-21.png new file mode 100644 index 000000000..46d799e6d Binary files /dev/null and b/apps/configurable_clock/Screenshot-21.png differ diff --git a/apps/configurable_clock/Screenshot-22.png b/apps/configurable_clock/Screenshot-22.png new file mode 100644 index 000000000..7ee02568e Binary files /dev/null and b/apps/configurable_clock/Screenshot-22.png differ diff --git a/apps/configurable_clock/Screenshot-23.png b/apps/configurable_clock/Screenshot-23.png new file mode 100644 index 000000000..f3248993b Binary files /dev/null and b/apps/configurable_clock/Screenshot-23.png differ diff --git a/apps/configurable_clock/Screenshot-24.png b/apps/configurable_clock/Screenshot-24.png new file mode 100644 index 000000000..8a7753bfc Binary files /dev/null and b/apps/configurable_clock/Screenshot-24.png differ diff --git a/apps/configurable_clock/Screenshot-25.png b/apps/configurable_clock/Screenshot-25.png new file mode 100644 index 000000000..c2950d7b2 Binary files /dev/null and b/apps/configurable_clock/Screenshot-25.png differ diff --git a/apps/configurable_clock/app-icon.js b/apps/configurable_clock/app-icon.js new file mode 100644 index 000000000..b0cf74241 --- /dev/null +++ b/apps/configurable_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgZC/AB1RgkQsAQMyUKAYMIkAPJgNFiEBgACBg0YCRMogEJkGSAwMSEZNAAQMAEAMGgBKHgXAlECwMgzcAmkAhgRGilRssUgMEEYcBwARFiBHBgQKB7AjCawIQEgoCCigDBjEBwwEBEwIAGlmSEYYABI4PAEYhEBNYIjCAYVtwCSElG2xdoAwQjDhpZEEAMUqAHDCIaPBEYlAiwjItkAgYjFqJHDCIdhI4j1CAAhlEZoTUEAAcGEYZKEEYWgCIgjEWYkBoqwCCITLBgcMmPXhgjCgUB2iFDm3pw0YLAMygEgc4QjF49cmA3BbQQjDgGkI5OwNZZ9FEYoRLEYxmBCI5jBEYQACyQRHgmAEYsEEZEka4kAhEEEY8BCIMJCIYjKgGChAFDCwKzDNYyKEJgUDlgRBAoPDRQQjEZQZzEjScIhgjBEwQjEH4aXEgIjBjYCBjQCBMYYADmAjDFIjcGKocAjBKCgJRCAAwaCEARQBmARIhBrEgSMEAApEBmHAAQJrCABUCjFhwwQMI4oA7")) \ No newline at end of file diff --git a/apps/configurable_clock/app-icon.png b/apps/configurable_clock/app-icon.png new file mode 100644 index 000000000..58f50365d Binary files /dev/null and b/apps/configurable_clock/app-icon.png differ diff --git a/apps/configurable_clock/app-screenshot.png b/apps/configurable_clock/app-screenshot.png new file mode 100644 index 000000000..528721759 Binary files /dev/null and b/apps/configurable_clock/app-screenshot.png differ diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js new file mode 100644 index 000000000..157d57741 --- /dev/null +++ b/apps/configurable_clock/app.js @@ -0,0 +1,1380 @@ + let Layout = require('Layout'); + + let Caret = require("heatshrink").decompress(atob("hEUgMAsFgmEwjEYhkMg0GAYIHBBYIPBgAA==")); + + let ScreenWidth = g.getWidth(), CenterX; + let ScreenHeight = g.getHeight(), CenterY, outerRadius; + + Bangle.loadWidgets(); + +/**** updateClockFaceSize ****/ + + function updateClockFaceSize () { + CenterX = ScreenWidth/2; + CenterY = ScreenHeight/2; + + outerRadius = Math.min(CenterX,CenterY); + + if (global.WIDGETS == null) { return; } + + let WidgetLayouts = { + tl:{ x:0, y:0, Direction:0 }, + tr:{ x:ScreenWidth-1, y:0, Direction:1 }, + bl:{ x:0, y:ScreenHeight-24, Direction:0 }, + br:{ x:ScreenWidth-1, y:ScreenHeight-24, Direction:1 } + }; + + for (let Widget of WIDGETS) { + let WidgetLayout = WidgetLayouts[Widget.area]; // reference, not copy! + if (WidgetLayout == null) { continue; } + + Widget.x = WidgetLayout.x - WidgetLayout.Direction * Widget.width; + Widget.y = WidgetLayout.y; + + WidgetLayout.x += Widget.width * (1-2*WidgetLayout.Direction); + } + + let x,y, dx,dy; + let cx = CenterX, cy = CenterY, r = outerRadius, r2 = r*r; + + x = WidgetLayouts.tl.x; y = WidgetLayouts.tl.y+24; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy-24); + } + + x = WidgetLayouts.tr.x; y = WidgetLayouts.tr.y+24; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy-24); + } + + x = WidgetLayouts.bl.x; y = WidgetLayouts.bl.y; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy); + } + + x = WidgetLayouts.br.x; y = WidgetLayouts.br.y; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy); + } + + CenterX = cx; CenterY = cy; outerRadius = r - 4; + } + + updateClockFaceSize(); + +/**** custom version of Bangle.drawWidgets (does not clear the widget areas) ****/ + + Bangle.drawWidgets = function () { + var w = g.getWidth(), h = g.getHeight(); + + var pos = { + tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left + tr:{x:w-1, y:0, r:1, c:0}, + bl:{x:0, y:h-24, r:0, c:0}, + br:{x:w-1, y:h-24, r:1, c:0} + }; + + if (global.WIDGETS) { + for (var wd of WIDGETS) { + var p = pos[wd.area]; + if (!p) continue; + + wd.x = p.x - p.r*wd.width; + wd.y = p.y; + + p.x += wd.width*(1-2*p.r); + p.c++; + } + + g.reset(); // also loads the current theme + + if (pos.tl.c || pos.tr.c) { + g.setClipRect(0,h-24,w-1,h-1); + g.reset(); // also (re)loads the current theme + } + + if (pos.bl.c || pos.br.c) { + g.setClipRect(0,h-24,w-1,h-1); + g.reset(); // also (re)loads the current theme + } + + try { + for (wd of WIDGETS) { + g.clearRect(wd.x,wd.y, wd.x+wd.width-1,23); + wd.draw(wd); + } + } catch (e) { print(e); } + } + }; + +/**** EventConsumerAtPoint ****/ + + let activeLayout; + + function EventConsumerAtPoint (HandlerName, x,y) { + let Layout = (activeLayout || {}).l; + if (Layout == null) { return; } + + function ConsumerIn (Control) { + if ( + (x < Control.x) || (x >= Control.x + Control.w) || + (y < Control.y) || (y >= Control.y + Control.h) + ) { return undefined; } + + if (typeof Control[HandlerName] === 'function') { return Control; } + + if (Control.c != null) { + let ControlList = Control.c; + for (let i = 0, l = ControlList.length; i < l; i++) { + let Consumer = ConsumerIn(ControlList[i]); + if (Consumer != null) { return Consumer; } + } + } + + return undefined; + } + + return ConsumerIn(Layout); + } + +/**** dispatchTouchEvent ****/ + + function dispatchTouchEvent (DefaultHandler) { + function handleTouchEvent (Button, xy) { + if (activeLayout == null) { + if (typeof DefaultHandler === 'function') { + DefaultHandler(); + } + } else { + let Control = EventConsumerAtPoint('onTouch', xy.x,xy.y); + if (Control != null) { + Control.onTouch(Control, Button, xy); + } + } + } + Bangle.on('touch',handleTouchEvent); + } + dispatchTouchEvent(); + +/**** dispatchStrokeEvent ****/ + + function dispatchStrokeEvent (DefaultHandler) { + function handleStrokeEvent (Coordinates) { + if (activeLayout == null) { + if (typeof DefaultHandler === 'function') { + DefaultHandler(); + } + } else { + let Control = EventConsumerAtPoint('onStroke', Coordinates.xy[0],Coordinates.xy[1]); + if (Control != null) { + Control.onStroke(Control, Coordinates); + } + } + } + Bangle.on('stroke',handleStrokeEvent); + } + dispatchStrokeEvent(); +/**** Label ****/ + + function Label (Text, Options) { + function renderLabel (Details) { + let x = Details.x, xAlignment = Details.halign || 0; + let y = Details.y, yAlignment = Details.valign || 0; + + let Width = Details.w, halfWidth = Width/2; + let Height = Details.h, halfHeight = Height/2; + + let Border = Details.border || 0, BorderColor = Details.BorderColor; + let Padding = Details.pad || 0; + let Hilite = Details.hilite || false; + let bold = Details.bold ? 1 : 0; + + if (Hilite || (Details.bgCol != null)) { + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol); + g.clearRect(x,y, x + Width-1,y + Height-1); + } + + if ((Border > 0) && (BorderColor !== null)) {// draw border of layout cell + g.setColor(BorderColor || Details.col || g.theme.fg); + + switch (Border) { + case 1: g.drawRect(x,y, x+Width-1,y+Height-1); break; + case 2: g.drawRect(x,y, x+Width-1,y+Height-1); + g.drawRect(x+1,y+1, x+Width-2,y+Height-2); break; + default: g.fillPoly([ + x,y, x+Width,y, x+Width,y+Height, x,y+Height, x,y, + x+Border,y+Border, x+Border,y+Height-Border, + x+Width-Border,y+Height-Border, x+Width-Border,y+Border, + x+Border,y+Border + ]); + } + } + + g.setClipRect( + x+Border+Padding,y+Border+Padding, + x + Width-Border-Padding-1,y + Height-Border-Padding-1 + ); + + x += halfWidth + xAlignment*(halfWidth - Border - Padding); + y += halfHeight + yAlignment*(halfHeight - Border - Padding); + + g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg); + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg); + + if (Details.font != null) { g.setFont(Details.font); } + g.setFontAlign(xAlignment,yAlignment); + + g.drawString(Details.label, x,y); + if (bold !== 0) { + g.drawString(Details.label, x+1,y); + g.drawString(Details.label, x,y+1); + g.drawString(Details.label, x+1,y+1); + } + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderLabel, label:Text || '' + }); + let Border = Result.border || 0; + let Padding = Result.pad || 0; + + let TextMetrics; + if (! Result.width || ! Result.height) { + if (Result.font == null) { + Result.font = g.getFont(); + } else { + g.setFont(Result.font); + } + TextMetrics = g.stringMetrics(Result.label); + } + + if (Result.col == null) { Result.col = g.getColor(); } + if (Result.bgCol == null) { Result.bgCol = g.getBgColor(); } + + Result.width = Result.width || TextMetrics.width + 2*Border + 2*Padding; + Result.height = Result.height || TextMetrics.height + 2*Border + 2*Padding; + return Result; + } + +/**** Image ****/ + + function Image (Image, Options) { + function renderImage (Details) { + let x = Details.x, xAlignment = Details.halign || 0; + let y = Details.y, yAlignment = Details.valign || 0; + + let Width = Details.w, halfWidth = Width/2 - Details.ImageWidth/2; + let Height = Details.h, halfHeight = Height/2 - Details.ImageHeight/2; + + let Border = Details.border || 0, BorderColor = Details.BorderColor; + let Padding = Details.pad || 0; + let Hilite = Details.hilite || false; + + if (Hilite || (Details.bgCol != null)) { + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol); + g.clearRect(x,y, x + Width-1,y + Height-1); + } + + if ((Border > 0) && (BorderColor !== null)) {// draw border of layout cell + g.setColor(BorderColor || Details.col || g.theme.fg); + + switch (Border) { + case 1: g.drawRect(x,y, x+Width-1,y+Height-1); break; + case 2: g.drawRect(x,y, x+Width-1,y+Height-1); + g.drawRect(x+1,y+1, x+Width-2,y+Height-2); break; + default: g.fillPoly([ + x,y, x+Width,y, x+Width,y+Height, x,y+Height, x,y, + x+Border,y+Border, x+Border,y+Height-Border, + x+Width-Border,y+Height-Border, x+Width-Border,y+Border, + x+Border,y+Border + ]); + } + } + + g.setClipRect( + x+Border+Padding,y+Border+Padding, + x + Width-Border-Padding-1,y + Height-Border-Padding-1 + ); + + x += halfWidth + xAlignment*(halfWidth - Border - Padding); + y += halfHeight + yAlignment*(halfHeight - Border - Padding); + + if ('rotate' in Details) { // "rotate" centers image at x,y! + x += Details.ImageWidth/2; + y += Details.ImageHeight/2; + } + + g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg); + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg); + + g.drawImage(Image, x,y, Details.ImageOptions); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderImage, Image:Image + }); + let ImageMetrics = g.imageMetrics(Image); + let Scale = Result.scale || 1; + let Border = Result.border || 0; + let Padding = Result.pad || 0; + + Result.ImageWidth = Scale * ImageMetrics.width; + Result.ImageHeight = Scale * ImageMetrics.height; + + if (('rotate' in Result) || ('scale' in Result) || ('frame' in Result)) { + Result.ImageOptions = {}; + if ('rotate' in Result) { Result.ImageOptions.rotate = Result.rotate; } + if ('scale' in Result) { Result.ImageOptions.scale = Result.scale; } + if ('frame' in Result) { Result.ImageOptions.frame = Result.frame; } + } + + Result.width = Result.width || Result.ImageWidth + 2*Border + 2*Padding; + Result.height = Result.height || Result.ImageHeight + 2*Border + 2*Padding; + return Result; + } + +/**** Drawable ****/ + + function Drawable (Callback, Options) { + function renderDrawable (Details) { + let x = Details.x, xAlignment = Details.halign || 0; + let y = Details.y, yAlignment = Details.valign || 0; + + let Width = Details.w, DrawableWidth = Details.DrawableWidth || Width; + let Height = Details.h, DrawableHeight = Details.DrawableHeight || Height; + + let halfWidth = Width/2 - DrawableWidth/2; + let halfHeight = Height/2 - DrawableHeight/2; + + let Border = Details.border || 0, BorderColor = Details.BorderColor; + let Padding = Details.pad || 0; + let Hilite = Details.hilite || false; + + if (Hilite || (Details.bgCol != null)) { + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol); + g.clearRect(x,y, x + Width-1,y + Height-1); + } + + if ((Border > 0) && (BorderColor !== null)) {// draw border of layout cell + g.setColor(BorderColor || Details.col || g.theme.fg); + + switch (Border) { + case 1: g.drawRect(x,y, x+Width-1,y+Height-1); break; + case 2: g.drawRect(x,y, x+Width-1,y+Height-1); + g.drawRect(x+1,y+1, x+Width-2,y+Height-2); break; + default: g.fillPoly([ + x,y, x+Width,y, x+Width,y+Height, x,y+Height, x,y, + x+Border,y+Border, x+Border,y+Height-Border, + x+Width-Border,y+Height-Border, x+Width-Border,y+Border, + x+Border,y+Border + ]); + } + } + + let DrawableX = x + halfWidth + xAlignment*(halfWidth - Border - Padding); + let DrawableY = y + halfHeight + yAlignment*(halfHeight - Border - Padding); + + g.setClipRect( + Math.max(x+Border+Padding,DrawableX), + Math.max(y+Border+Padding,DrawableY), + Math.min(x+Width -Border-Padding,DrawableX+DrawableWidth)-1, + Math.min(y+Height-Border-Padding,DrawableY+DrawableHeight)-1 + ); + + g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg); + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg); + + Callback(DrawableX,DrawableY, DrawableWidth,DrawableHeight, Details); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderDrawable, cb:Callback + }); + let DrawableWidth = Result.DrawableWidth || 10; + let DrawableHeight = Result.DrawableHeight || 10; + + let Border = Result.border || 0; + let Padding = Result.pad || 0; + + Result.width = Result.width || DrawableWidth + 2*Border + 2*Padding; + Result.height = Result.height || DrawableHeight + 2*Border + 2*Padding; + return Result; + } + + if (g.drawRoundedRect == null) { + g.drawRoundedRect = function drawRoundedRect (x1,y1, x2,y2, r) { + let x,y; + if (x1 > x2) { x = x1; x1 = x2; x2 = x; } + if (y1 > y2) { y = y1; y1 = y2; y2 = y; } + + r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2); + + let cx1 = x1+r, cx2 = x2-r; + let cy1 = y1+r, cy2 = y2-r; + + this.drawLine(cx1,y1, cx2,y1); + this.drawLine(cx1,y2, cx2,y2); + this.drawLine(x1,cy1, x1,cy2); + this.drawLine(x2,cy1, x2,cy2); + + x = r; y = 0; + + let dx,dy, Error = 0; + while (y <= x) { + dy = 1 + 2*y; y++; Error -= dy; + if (Error < 0) { + dx = 1 - 2*x; x--; Error -= dx; + } + + this.setPixel(cx1 - x, cy1 - y); this.setPixel(cx1 - y, cy1 - x); + this.setPixel(cx2 + x, cy1 - y); this.setPixel(cx2 + y, cy1 - x); + this.setPixel(cx2 + x, cy2 + y); this.setPixel(cx2 + y, cy2 + x); + this.setPixel(cx1 - x, cy2 + y); this.setPixel(cx1 - y, cy2 + x); + } + }; + } + + if (g.fillRoundedRect == null) { + g.fillRoundedRect = function fillRoundedRect (x1,y1, x2,y2, r) { + let x,y; + if (x1 > x2) { x = x1; x1 = x2; x2 = x; } + if (y1 > y2) { y = y1; y1 = y2; y2 = y; } + + r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2); + + let cx1 = x1+r, cx2 = x2-r; + let cy1 = y1+r, cy2 = y2-r; + + this.fillRect(x1,cy1, x2,cy2); + + x = r; y = 0; + + let dx,dy, Error = 0; + while (y <= x) { + dy = 1 + 2*y; y++; Error -= dy; + if (Error < 0) { + dx = 1 - 2*x; x--; Error -= dx; + } + + this.drawLine(cx1 - x, cy1 - y, cx2 + x, cy1 - y); + this.drawLine(cx1 - y, cy1 - x, cx2 + y, cy1 - x); + this.drawLine(cx1 - x, cy2 + y, cx2 + x, cy2 + y); + this.drawLine(cx1 - y, cy2 + x, cx2 + y, cy2 + x); + } + }; + } + + +/**** Button ****/ + + function Button (Text, Options) { + function renderButton (Details) { + let x = Details.x, Width = Details.w, halfWidth = Width/2; + let y = Details.y, Height = Details.h, halfHeight = Height/2; + + let Padding = Details.pad || 0; + let Hilite = Details.hilite || false; + + if (Details.bgCol != null) { + g.setBgColor(Details.bgCol); + g.clearRect(x,y, x + Width-1,y + Height-1); + } + + if (Hilite) { + g.setColor(g.theme.bgH); // no typo! + g.fillRoundedRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1,8); + } + + g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg); + g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg); + + if (Details.font != null) { g.setFont(Details.font); } + g.setFontAlign(0,0); + + g.drawRoundedRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1,8); + + g.setClipRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1); + + g.drawString(Details.label, x+halfWidth,y+halfHeight); + g.drawString(Details.label, x+halfWidth+1,y+halfHeight); + g.drawString(Details.label, x+halfWidth,y+halfHeight+1); + g.drawString(Details.label, x+halfWidth+1,y+halfHeight+1); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderButton, label:Text || 'Tap' + }); + let Padding = Result.pad || 0; + + let TextMetrics; + if (! Result.width || ! Result.height) { + if (Result.font == null) { + Result.font = g.getFont(); + } else { + g.setFont(Result.font); + } + TextMetrics = g.stringMetrics(Result.label); + } + + Result.width = Result.width || TextMetrics.width + 2*10 + 2*Padding; + Result.height = Result.height || TextMetrics.height + 2*5 + 2*Padding; + return Result; + } + + const Checkbox_checked = require("heatshrink").decompress(atob("ikUgMf/+GgEGoEAlEAgOAgEYsFhw8OjE54OB/EYh4OB+EYj+BwecjFw8OGg0YDocUgECsEAsP//A")); + const Checkbox_unchecked = require("heatshrink").decompress(atob("ikUgMf/+GgEGoEAlEAgOAgEYAjkUgECsEAsP//A=")); + +/**** Checkbox ****/ + + function Checkbox (Options) { + function renderCheckbox (Details) { + let x = Details.x, xAlignment = Details.halign || 0; + let y = Details.y, yAlignment = Details.valign || 0; + + let Width = Details.w, halfWidth = Width/2 - 10; + let Height = Details.h, halfHeight = Height/2 - 10; + + let Padding = Details.pad || 0; + + if (Details.bgCol != null) { + g.setBgColor(Details.bgCol); + g.clearRect(x,y, x + Width-1,y + Height-1); + } + + x += halfWidth + xAlignment*(halfWidth - Padding); + y += halfHeight + yAlignment*(halfHeight - Padding); + + g.setColor (Details.col || g.theme.fg); + g.setBgColor(Details.bgCol || g.theme.bg); + + g.drawImage( + Details.checked ? Checkbox_checked : Checkbox_unchecked, x,y + ); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderCheckbox, onTouch:toggleCheckbox + }); + let Padding = Result.pad || 0; + + Result.width = Result.width || 20 + 2*Padding; + Result.height = Result.height || 20 + 2*Padding; + + if (Result.checked == null) { Result.checked = false; } + return Result; + } + + /* private */ function toggleCheckbox (Control) { + g.reset(); + + Control.checked = ! Control.checked; + Control.render(Control); + + if (typeof Control.onChange === 'function') { + Control.onChange(Control); + } + } + +/**** toggleInnerCheckbox ****/ + + /* export */ function toggleInnerCheckbox (Control) { + if (Control.c == null) { + if (('checked' in Control) && ! ('GroupName' in Control)) { + toggleCheckbox(Control); + return true; + } + } else { + let ControlList = Control.c; + for (let i = 0, l = ControlList.length; i < l; i++) { + let done = toggleInnerCheckbox(ControlList[i]); + if (done) { return true; } + } + } + } + + const Radiobutton_checked = require("heatshrink").decompress(atob("ikUgMB/EAsFgjEBwUAgkggFEgECoEAlEPgOB/EYj+BAgmA+EUCYciDodBwEYg0GgEfwA")); + const Radiobutton_unchecked = require("heatshrink").decompress(atob("ikUgMB/EAsFgjEBwUAgkggFEgECoEAlEAgOAgEYAhEUCYciDodBwEYg0GgEfwAA=")); + +/**** Radiobutton ****/ + + function Radiobutton (Options) { + function renderRadiobutton (Details) { + let x = Details.x, xAlignment = Details.halign || 0; + let y = Details.y, yAlignment = Details.valign || 0; + + let Width = Details.w, halfWidth = Width/2 - 10; + let Height = Details.h, halfHeight = Height/2 - 10; + + let Padding = Details.pad || 0; + + if (Details.bgCol != null) { + g.setBgColor(Details.bgCol); + g.clearRect(x,y, x + Width-1,y + Height-1); + } + + x += halfWidth + xAlignment*(halfWidth - Padding); + y += halfHeight + yAlignment*(halfHeight - Padding); + + g.setColor (Details.col || g.theme.fg); + g.setBgColor(Details.bgCol || g.theme.bg); + + g.drawImage( + Details.checked ? Radiobutton_checked : Radiobutton_unchecked, x,y + ); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderRadiobutton, onTouch:checkRadiobutton + }); + let Padding = Result.pad || 0; + + Result.width = Result.width || 20 + 2*Padding; + Result.height = Result.height || 20 + 2*Padding; + + if (Result.checked == null) { Result.checked = false; } + return Result; + } + + /* private */ function checkRadiobutton (Control) { + if (! Control.checked) { + uncheckRadiobuttonsIn((activeLayout || {}).l,Control.GroupName); + toggleRadiobutton(Control); + + if (typeof Control.onChange === 'function') { + Control.onChange(Control); + } + } + } + + /* private */ function toggleRadiobutton (Control) { + g.reset(); + + Control.checked = ! Control.checked; + Control.render(Control); + } + + /* private */ function uncheckRadiobuttonsIn (Control,GroupName) { + if ((Control == null) || (GroupName == null)) { return; } + + if (Control.c == null) { + if (('checked' in Control) && (Control.GroupName === GroupName)) { + if (Control.checked) { toggleRadiobutton(Control); } + } + } else { + let ControlList = Control.c; + for (let i = 0, l = ControlList.length; i < l; i++) { + uncheckRadiobuttonsIn(ControlList[i],GroupName); + } + } + } + +/**** checkInnerRadiobutton ****/ + + /* export */ function checkInnerRadiobutton (Control) { + if (Control.c == null) { + if (('checked' in Control) && ('GroupName' in Control)) { + checkRadiobutton(Control); + return true; + } + } else { + let ControlList = Control.c; + for (let i = 0, l = ControlList.length; i < l; i++) { + let done = checkInnerRadiobutton(ControlList[i]); + if (done) { return true; } + } + } + } + + + let Theme = g.theme; + g.clear(true); + +/**** Settings ****/ + + let Settings; + + function readSettings () { + Settings = Object.assign({}, + { + Face:'1-12', colored:true, + Hands:'rounded', withSeconds:true, + Foreground:'Theme', Background:'Theme', Seconds:'#FF0000' + }, + require('Storage').readJSON('configurable_clock.json', true) || {} + ); + + prepareTransformedPolygon(); + } + + function saveSettings () { + require('Storage').writeJSON('configurable_clock.json', Settings); + prepareTransformedPolygon(); + } + + function prepareTransformedPolygon () { + switch (Settings.Hands) { + case 'simple': transformedPolygon = new Array(simpleHourHandPolygon.length); break; + case 'rounded': transformedPolygon = new Array(roundedHandPolygon.length); break; + case 'hollow': transformedPolygon = new Array(hollowHandPolygon.length); + } + } + +//readSettings(); // not yet + + +/**** Hands ****/ + + let HourHandLength = outerRadius * 0.5; + let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2; + + let MinuteHandLength = outerRadius * 0.8; + let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2; + + let SecondHandLength = outerRadius * 0.9; + let SecondHandOffset = 10; + + let twoPi = 2*Math.PI, deg2rad = Math.PI/180; + let Pi = Math.PI; + let halfPi = Math.PI/2; + + let sin = Math.sin, cos = Math.cos; + +/**** simple Hands ****/ + + let simpleHourHandPolygon = [ + -halfHourHandWidth,halfHourHandWidth, + -halfHourHandWidth,halfHourHandWidth-HourHandLength, + halfHourHandWidth,halfHourHandWidth-HourHandLength, + halfHourHandWidth,halfHourHandWidth, + ]; + + let simpleMinuteHandPolygon = [ + -halfMinuteHandWidth,halfMinuteHandWidth, + -halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength, + halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength, + halfMinuteHandWidth,halfMinuteHandWidth, + ]; + + +/**** rounded Hands ****/ + + let outerBoltRadius = halfHourHandWidth + 2; + let innerBoltRadius = outerBoltRadius - 4; + let roundedHandOffset = outerBoltRadius + 4; + + let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1]; + + let roundedHandPolygon = [ + -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3], + sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0], + sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3], + -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0], + ]; + + let roundedHourHandPolygon = new Array(roundedHandPolygon.length); + for (let i = 0, l = roundedHandPolygon.length; i < l; i+=2) { + roundedHourHandPolygon[i] = halfHourHandWidth*roundedHandPolygon[i]; + roundedHourHandPolygon[i+1] = halfHourHandWidth*roundedHandPolygon[i+1]; + if (i < l/2) { roundedHourHandPolygon[i+1] -= HourHandLength; } + if (i > l/2) { roundedHourHandPolygon[i+1] += roundedHandOffset; } + } + let roundedMinuteHandPolygon = new Array(roundedHandPolygon.length); + for (let i = 0, l = roundedHandPolygon.length; i < l; i+=2) { + roundedMinuteHandPolygon[i] = halfMinuteHandWidth*roundedHandPolygon[i]; + roundedMinuteHandPolygon[i+1] = halfMinuteHandWidth*roundedHandPolygon[i+1]; + if (i < l/2) { roundedMinuteHandPolygon[i+1] -= MinuteHandLength; } + if (i > l/2) { roundedMinuteHandPolygon[i+1] += roundedHandOffset; } + } + + +/**** hollow Hands ****/ + + let BoltRadius = 3; + let hollowHandOffset = BoltRadius + 15; + + let hollowHandPolygon = [ + -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3], + sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0], + sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3], + 0,0, + -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0] + ]; + + let hollowHourHandPolygon = new Array(hollowHandPolygon.length); + for (let i = 0, l = hollowHandPolygon.length; i < l; i+=2) { + hollowHourHandPolygon[i] = halfHourHandWidth*hollowHandPolygon[i]; + hollowHourHandPolygon[i+1] = halfHourHandWidth*hollowHandPolygon[i+1]; + if (i < l/2) { hollowHourHandPolygon[i+1] -= HourHandLength; } + if (i > l/2) { hollowHourHandPolygon[i+1] -= hollowHandOffset; } + } + hollowHourHandPolygon[25] = -BoltRadius; + + let hollowMinuteHandPolygon = new Array(hollowHandPolygon.length); + for (let i = 0, l = hollowHandPolygon.length; i < l; i+=2) { + hollowMinuteHandPolygon[i] = halfMinuteHandWidth*hollowHandPolygon[i]; + hollowMinuteHandPolygon[i+1] = halfMinuteHandWidth*hollowHandPolygon[i+1]; + if (i < l/2) { hollowMinuteHandPolygon[i+1] -= MinuteHandLength; } + if (i > l/2) { hollowMinuteHandPolygon[i+1] -= hollowHandOffset; } + } + hollowMinuteHandPolygon[25] = -BoltRadius; + + + +/**** transform polygon ****/ + + let transformedPolygon; + + function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } + } + +/**** refreshClock ****/ + + let Timer; + function refreshClock () { + activeLayout = null; + + g.setTheme({ + fg:(Settings.Foreground === 'Theme' ? Theme.fg : Settings.Foreground || '#000000'), + bg:(Settings.Background === 'Theme' ? Theme.bg : Settings.Background || '#FFFFFF') + }); + g.clear(true); // also installs the current theme + + Bangle.drawWidgets(); + renderClock(); + + let Period = (Settings.withSeconds ? 1000 : 60000); + + let Pause = Period - (Date.now() % Period); + Timer = setTimeout(refreshClock,Pause); + } + +/**** renderClock ****/ + + function renderClock () { + g.setColor (Settings.Foreground === 'Theme' ? Theme.fg : Settings.Foreground || '#000000'); + g.setBgColor(Settings.Background === 'Theme' ? Theme.bg : Settings.Background || '#FFFFFF'); + + switch (Settings.Face) { + case 'none': + break; + case '3,6,9,12': + g.setFont('Vector', 22); + + g.setFontAlign(0,-1); + g.drawString('12', CenterX,CenterY-outerRadius); + + g.setFontAlign(1,0); + g.drawString('3', CenterX+outerRadius,CenterY); + + g.setFontAlign(0,1); + g.drawString('6', CenterX,CenterY+outerRadius); + + g.setFontAlign(-1,0); + g.drawString('9', CenterX-outerRadius,CenterY); + break; + case '1-12': + let innerRadius = outerRadius * 0.9 - 10; + + let dark = g.theme.dark; + + let Saturations = [0.8,1.0,1.0,1.0,1.0,1.0,1.0,0.9,0.7,0.7,0.9,0.9]; + let Brightnesses = [1.0,0.9,0.6,0.6,0.8,0.8,0.7,1.0,1.0,1.0,1.0,1.0,]; + + for (let i = 0; i < 60; i++) { + let Phi = i * twoPi/60; + + let x = CenterX + outerRadius * sin(Phi); + let y = CenterY - outerRadius * cos(Phi); + + if (Settings.colored) { + let j = Math.floor(i / 5); + let Saturation = (dark ? Saturations[j] : 1.0); + let Brightness = (dark ? 1.0 : Brightnesses[j]); + + let Color = E.HSBtoRGB(i/60,Saturation,Brightness, true); + g.setColor(Color[0]/255,Color[1]/255,Color[2]/255); + } + + g.fillCircle(x,y, 1); + } + + g.setFont('Vector', 20); + g.setFontAlign(0,0); + + for (let i = 0; i < 12; i++) { + let Phi = i * twoPi/12; + + let Radius = innerRadius; + if (i >= 10) { Radius -= 4; } + + let x = CenterX + Radius * sin(Phi); + let y = CenterY - Radius * cos(Phi); + + if (Settings.colored) { + let Saturation = (dark ? Saturations[i] : 1.0); + let Brightness = (dark ? 1.0 : Brightnesses[i]); + + let Color = E.HSBtoRGB(i/12,Saturation,Brightness, true); + g.setColor(Color[0]/255,Color[1]/255,Color[2]/255); + } + + g.drawString(i == 0 ? '12' : '' + i, x,y); + } + } + + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + + g.setColor(Settings.Foreground === 'Theme' ? Theme.fg : Settings.Foreground || '#000000'); + + switch (Settings.Hands) { + case 'simple': + transformPolygon(simpleHourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(simpleMinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + break; + case 'rounded': + transformPolygon(roundedHourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(roundedMinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + +// g.setColor(Settings.Foreground === 'Theme' ? Theme.fg || '#000000'); + g.fillCircle(CenterX,CenterY, outerBoltRadius); + + g.setColor(Settings.Background === 'Theme' ? Theme.bg : Settings.Background || '#FFFFFF'); + g.drawCircle(CenterX,CenterY, outerBoltRadius); + g.fillCircle(CenterX,CenterY, innerBoltRadius); + break; + case 'hollow': + transformPolygon(hollowHourHandPolygon, CenterX,CenterY, HoursAngle); + g.drawPoly(transformedPolygon,true); + + transformPolygon(hollowMinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.drawPoly(transformedPolygon,true); + + g.drawCircle(CenterX,CenterY, BoltRadius); + } + + if (Settings.withSeconds) { + g.setColor(Settings.Seconds === 'Theme' ? Theme.fgH : Settings.Seconds || '#FF0000'); + + let Seconds = now.getSeconds(); + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + } + } + + +/**** MainScreen Logic ****/ + + let Changes = {}, KeysToChange; + + let fullScreen = { + x:0,y:0, w:ScreenWidth,h:ScreenHeight, x2:ScreenWidth-1,y2:ScreenHeight-1 + }; + let AppRect; + + function openMainScreen () { + if (Timer != null) { clearTimeout(Timer); Timer = undefined; } + if (AppRect == null) { AppRect = Bangle.appRect; Bangle.appRect = fullScreen; } + + Bangle.buzz(); + + KeysToChange = 'Face colored Hands withSeconds Foreground Background Seconds'; + + g.setTheme({ fg:'#000000', bg:'#FFFFFF' }); + g.clear(true); // also installs the current theme + + (activeLayout = MainScreen).render(); + } + + function applySettings () { Bangle.buzz(); saveSettings(); Bangle.appRect = AppRect; refreshClock(); } + function withdrawSettings () { Bangle.buzz(); readSettings(); Bangle.appRect = AppRect; refreshClock(); } + +/**** FacesScreen Logic ****/ + + function openFacesScreen () { + Bangle.buzz(); + + KeysToChange = 'Face colored'; + Bangle.appRect = fullScreen; + refreshFacesScreen(); + } + + function refreshFacesScreen () { + activeLayout = FacesScreen; + activeLayout['none'].checked = ((Changes.Face || Settings.Face) === 'none'); + activeLayout['3,6,9,12'].checked = ((Changes.Face || Settings.Face) === '3,6,9,12'); + activeLayout['1-12'].checked = ((Changes.Face || Settings.Face) === '1-12'); + activeLayout['colored'].checked = (Changes.colored == null ? Settings.colored : Changes.colored); + activeLayout.render(); + } + + function chooseFace (Control) { Bangle.buzz(); Changes.Face = Control.id; refreshFacesScreen(); } + function toggleColored () { Bangle.buzz(); Changes.colored = ! Changes.colored; refreshFacesScreen(); } + +/**** HandsScreen Logic ****/ + + function openHandsScreen () { + Bangle.buzz(); + + KeysToChange = 'Hands withSeconds'; + Bangle.appRect = fullScreen; + refreshHandsScreen(); + } + + function refreshHandsScreen () { + activeLayout = HandsScreen; + activeLayout['simple'].checked = ((Changes.Hands || Settings.Hands) === 'simple'); + activeLayout['rounded'].checked = ((Changes.Hands || Settings.Hands) === 'rounded'); + activeLayout['hollow'].checked = ((Changes.Hands || Settings.Hands) === 'hollow'); + activeLayout['withSeconds'].checked = (Changes.withSeconds == null ? Settings.withSeconds : Changes.withSeconds); + activeLayout.render(); + } + + function chooseHand (Control) { Bangle.buzz(); Changes.Hands = Control.id; refreshHandsScreen(); } + function toggleSeconds () { Bangle.buzz(); Changes.withSeconds = ! Changes.withSeconds; refreshHandsScreen(); } + +/**** ColorsScreen Logic ****/ + + function openColorsScreen () { + Bangle.buzz(); + + KeysToChange = 'Foreground Background Seconds'; + Bangle.appRect = fullScreen; + refreshColorsScreen(); + } + + function refreshColorsScreen () { + let Foreground = (Changes.Foreground == null ? Settings.Foreground : Changes.Foreground); + let Background = (Changes.Background == null ? Settings.Background : Changes.Background); + let Seconds = (Changes.Seconds == null ? Settings.Seconds : Changes.Seconds); + + activeLayout = ColorsScreen; + activeLayout['Foreground'].bgCol = (Foreground === 'Theme' ? Theme.fg : Foreground); + activeLayout['Background'].bgCol = (Background === 'Theme' ? Theme.bg : Background); + activeLayout['Seconds'].bgCol = (Seconds === 'Theme' ? Theme.fgH : Seconds); + activeLayout.render(); + } + + function selectForegroundColor () { ColorToChange = 'Foreground'; openColorChoiceScreen(); } + function selectBackgroundColor () { ColorToChange = 'Background'; openColorChoiceScreen(); } + function selectSecondsColor () { ColorToChange = 'Seconds'; openColorChoiceScreen(); } + +/**** ColorChoiceScreen Logic ****/ + + let ColorToChange, chosenColor; + + function openColorChoiceScreen () { + Bangle.buzz(); + + chosenColor = ( + Changes[ColorToChange] == null ? Settings[ColorToChange] : Changes[ColorToChange] + ); + Bangle.appRect = fullScreen; + refreshColorChoiceScreen(); + } + + function refreshColorChoiceScreen () { + activeLayout = ColorChoiceScreen; + activeLayout['#000000'].selected = (chosenColor === '#000000'); + activeLayout['#FF0000'].selected = (chosenColor === '#FF0000'); + activeLayout['#00FF00'].selected = (chosenColor === '#00FF00'); + activeLayout['#0000FF'].selected = (chosenColor === '#0000FF'); + activeLayout['#FFFF00'].selected = (chosenColor === '#FFFF00'); + activeLayout['#FF00FF'].selected = (chosenColor === '#FF00FF'); + activeLayout['#00FFFF'].selected = (chosenColor === '#00FFFF'); + activeLayout['#FFFFFF'].selected = (chosenColor === '#FFFFFF'); + activeLayout['Theme'].selected = (chosenColor === 'Theme'); + activeLayout.render(); + } + + function chooseColor (Control) { Bangle.buzz(); chosenColor = Control.id; refreshColorChoiceScreen(); } + function chooseThemeColor () { Bangle.buzz(); chosenColor = 'Theme'; refreshColorChoiceScreen(); } + + function applyColor () { + Changes[ColorToChange] = chosenColor; + openColorsScreen(); + } + + function withdrawColor () { + openColorsScreen(); + } + +/**** common logic for multiple screens ****/ + + function applyChanges () { + Settings = Object.assign(Settings,Changes); + Changes = {}; + openMainScreen(); + } + + function withdrawChanges () { + Changes = {}; + openMainScreen(); + } + + + g.setFont12x20(); // does not seem to be respected in layout! + + let OkCancelWidth = Math.max( + g.stringWidth('Ok'), g.stringWidth('Cancel') + ) + 2*10; + + let StdFont = { font:'12x20' }; + let legible = Object.assign({ col:'#000000', bgCol:'#FFFFFF' }, StdFont); + let leftAligned = Object.assign({ halign:-1, valign:0 }, legible); + let ColorView = Object.assign({ width:30, border:1, BorderColor:'#000000' }, StdFont); + let ColorChoice = Object.assign({ DrawableWidth:30, DrawableHeight:30, onTouch:chooseColor }, StdFont); + +/**** MainScreen ****/ + + let MainScreen = new Layout({ + type:'v', c:[ + Label('Settings', { common:legible, bold:true, filly:1 }), + { height:4 }, + { type:'h', c:[ + { width:4 }, + Label('Faces', { common:leftAligned, fillx:1 }), + Image(Caret, { common:leftAligned }), + { width:4 }, + ], filly:1, onTouch:openFacesScreen }, + { type:'h', c:[ + { width:4 }, + Label('Hands', { common:leftAligned, fillx:1 }), + Image(Caret, { common:leftAligned }), + { width:4 }, + ], filly:1, onTouch:openHandsScreen }, + { type:'h', c:[ + { width:4 }, + Label('Colors', { common:leftAligned, fillx:1 }), + Image(Caret, { common:leftAligned }), + { width:4 }, + ], filly:1, onTouch:openColorsScreen }, + { height:4 }, + { type:'h', c:[ + Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applySettings }), + { width:4 }, + Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawSettings }), + ], filly:1 }, + ], bgCol:'#FFFFFF' + }); + + +/**** FacesScreen ****/ + + let FacesScreen = new Layout({ + type:'v', c:[ + Label('Clock Faces', { common:legible, bold:true, filly:1 }), + { height:4 }, + { type:'h', c:[ + { width:4 }, + Radiobutton({ id:'none', GroupName:'Faces', common:legible, onChange:chooseFace }), + Label(' no Face', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:checkInnerRadiobutton }, + { type:'h', c:[ + { width:4 }, + Radiobutton({ id:'3,6,9,12', GroupName:'Faces', common:legible, onChange:chooseFace }), + Label(' 3, 6, 9 and 12', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:checkInnerRadiobutton }, + { type:'h', c:[ + { width:4 }, + Radiobutton({ id:'1-12', GroupName:'Faces', common:legible, onChange:chooseFace }), + Label(' numbers 1...12', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:checkInnerRadiobutton }, + { type:'h', c:[ + { width:30 }, + Checkbox({ id:'colored', common:legible, onChange:toggleColored }), + Label(' colorful', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:toggleInnerCheckbox }, + { height:4 }, + { type:'h', c:[ + Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyChanges }), + { width:4 }, + Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawChanges }), + ], filly:1 }, + ], bgCol:'#FFFFFF' + }); + + +/**** HandsScreen ****/ + + let HandsScreen = new Layout({ + type:'v', c:[ + Label('Clock Hands', { common:legible, bold:true, filly:1 }), + { height:4 }, + { type:'h', c:[ + { width:4 }, + Radiobutton({ id:'simple', GroupName:'Faces', common:legible, onChange:chooseHand }), + Label(' simple', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:checkInnerRadiobutton }, + { type:'h', c:[ + { width:4 }, + Radiobutton({ id:'rounded', GroupName:'Faces', common:legible, onChange:chooseHand }), + Label(' rounded + Bolt', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:checkInnerRadiobutton }, + { type:'h', c:[ + { width:4 }, + Radiobutton({ id:'hollow', GroupName:'Faces', common:legible, onChange:chooseHand }), + Label(' hollow + Bolt', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:checkInnerRadiobutton }, + { type:'h', c:[ + { width:4 }, + Checkbox({ id:'withSeconds', common:legible, onChange:toggleSeconds }), + Label(' show Seconds', { common:leftAligned, pad:4, fillx:1 }), + ], filly:1, onTouch:toggleInnerCheckbox }, + { height:4 }, + { type:'h', c:[ + Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyChanges }), + { width:4 }, + Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawChanges }), + ], filly:1 }, + ], bgCol:'#FFFFFF' + }); + + +/**** ColorsScreen ****/ + + let ColorsScreen = new Layout({ + type:'v', c:[ + Label('Clock Colors', { common:legible, bold:true, filly:1 }), + { height:4 }, + { type:'h', c:[ + { width:4 }, + Label('Foreground', { common:leftAligned, pad:4, fillx:1 }), + Label('', { id:'Foreground', common:ColorView, bgCol:Theme.fg }), + { width:4 }, + ], filly:1, onTouch:selectForegroundColor }, + { type:'h', c:[ + { width:4 }, + Label('Background', { common:leftAligned, pad:4, fillx:1 }), + Label('', { id:'Background', common:ColorView, bgCol:Theme.bg }), + { width:4 }, + ], filly:1, onTouch:selectBackgroundColor }, + { type:'h', c:[ + { width:4 }, + Label('Seconds', { common:leftAligned, pad:4, fillx:1 }), + Label('', { id:'Seconds', common:ColorView, bgCol:Theme.fgH }), + { width:4 }, + ], filly:1, onTouch:selectSecondsColor }, + { height:4 }, + { type:'h', c:[ + Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyChanges }), + { width:4 }, + Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawChanges }), + ], filly:1 }, + ], bgCol:'#FFFFFF' + }); + + +/**** ColorChoiceScreen ****/ + + function drawColorChoice (x,y, Width,Height, Details) { + let selected = Details.selected; + if (selected) { + g.setColor('#FF0000'); + g.fillPoly([ + x,y, x+Width-1,y, x+Width-1,y+Height-1, x,y+Height-1, x,y, + x+3,y+3, x+3,y+Height-4, x+Width-4,y+Height-4, x+Width-4,y+3, x+3,y+3 + ]); + } else { + g.setColor('#000000'); + g.drawRect(x+3,y+3, x+Width-4,y+Height-4); + } + + g.setColor(Details.col); + g.fillRect(x+4,y+4, x+Width-5,y+Height-5); + } + + let ColorChoiceScreen = new Layout({ + type:'v', c:[ + Label('Choose Color', { common:legible, bold:true, filly:1 }), + { height:4 }, + { type:'h', c:[ + Drawable(drawColorChoice, { id:'#000000', common:ColorChoice, col:'#000000' }), + { width:8 }, + Drawable(drawColorChoice, { id:'#FF0000', common:ColorChoice, col:'#FF0000' }), + { width:8 }, + Drawable(drawColorChoice, { id:'#00FF00', common:ColorChoice, col:'#00FF00' }), + { width:8 }, + Drawable(drawColorChoice, { id:'#0000FF', common:ColorChoice, col:'#0000FF' }), + ], filly:1 }, + { type:'h', c:[ + Drawable(drawColorChoice, { id:'#FFFFFF', common:ColorChoice, col:'#FFFFFF' }), + { width:8 }, + Drawable(drawColorChoice, { id:'#FFFF00', common:ColorChoice, col:'#FFFF00' }), + { width:8 }, + Drawable(drawColorChoice, { id:'#FF00FF', common:ColorChoice, col:'#FF00FF' }), + { width:8 }, + Drawable(drawColorChoice, { id:'#00FFFF', common:ColorChoice, col:'#00FFFF' }), + ], filly:1 }, + { type:'h', c:[ + Label('use Theme:', { id:'Theme', common:leftAligned, pad:4 }), + { width:10 }, + Drawable(drawColorChoice, { id:'Theme', common:ColorChoice, col:Theme.fg }), + ], filly:1, onTouch:chooseThemeColor }, + { height:4 }, + { type:'h', c:[ + Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyColor }), + { width:4 }, + Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawColor }), + ], filly:1 }, + ], bgCol:'#FFFFFF' + }); + + + + readSettings(); + + Bangle.on('swipe', (Direction) => { + if (Direction === 0) { openMainScreen(); } + }); + + setTimeout(refreshClock, 500); // enqueue first draw request + + Bangle.on('lcdPower', (on) => { + if (on) { + if (Timer != null) { clearTimeout(Timer); Timer = undefined; } + refreshClock(); + } + }); + + Bangle.setUI('clock'); diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog index ea6911f1a..ad6f0742d 100644 --- a/apps/coretemp/ChangeLog +++ b/apps/coretemp/ChangeLog @@ -1,2 +1,3 @@ 0.01: New app 0.02: Cleanup interface and add settings, widget, add skin temp reporting. +0.03: Move code for recording to this app diff --git a/apps/coretemp/recorder.js b/apps/coretemp/recorder.js new file mode 100644 index 000000000..1499605f3 --- /dev/null +++ b/apps/coretemp/recorder.js @@ -0,0 +1,31 @@ +(function(recorders) { + recorders.coretemp = function() { + var core = "", skin = ""; + var hasCore = false; + function onCore(c) { + core=c.core; + skin=c.skin; + hasCore = true; + } + return { + name : "Core", + fields : ["Core","Skin"], + getValues : () => { + var r = [core,skin]; + core = ""; + skin = ""; + return r; + }, + start : () => { + hasCore = false; + Bangle.on('CoreTemp', onCore); + }, + stop : () => { + hasCore = false; + Bangle.removeListener('CoreTemp', onCore); + }, + draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y) + }; + } +}) + diff --git a/apps/cprassist/ChangeLog b/apps/cprassist/ChangeLog index 5560f00bc..529010aa8 100644 --- a/apps/cprassist/ChangeLog +++ b/apps/cprassist/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Ported to Banglejs2 diff --git a/apps/cprassist/cprassist.js b/apps/cprassist/cprassist.js index 862ae54d6..128ae7407 100644 --- a/apps/cprassist/cprassist.js +++ b/apps/cprassist/cprassist.js @@ -35,23 +35,24 @@ function provideFeedback() { } function drawHeart() { - g.fillCircle(40, 92, 12); - g.fillCircle(60, 92, 12); - g.fillPoly([29, 98, 50, 120, 71, 98]); + var lowestPoint = g.getHeight()*3/5; + g.fillCircle(40, lowestPoint-29, 12); + g.fillCircle(60, lowestPoint-29, 12); + g.fillPoly([29, lowestPoint-22, 50, lowestPoint, 71, lowestPoint-22]); } function updateScreen() { - const colors = [0xFFFF, 0x9492]; - g.reset().clearRect(0, 50, 250, 150); + const colors = [0xFFFF-g.getBgColor(), 0x9492]; + g.reset().clearRect(0, 24, g.getWidth(), g.getHeight()*5/6); if (counter > 0) { g.setFont("Vector", 40).setFontAlign(0, 0); g.setColor(colors[counter%2]); drawHeart(); - g.drawString(counter + "", g.getWidth()/2, 100); + g.drawString(counter, 120, g.getHeight()*3/5-20); } else { g.setFont("Vector", 20).setFontAlign(0, 0); - g.drawString("RESCUE", g.getWidth()/2, 70); - g.drawString("BREATHS", g.getWidth()/2, 120); + g.drawString("RESCUE", g.getWidth()/2, g.getHeight()/3); + g.drawString("BREATHS", g.getWidth()/2, g.getHeight()*3/5); } } @@ -73,7 +74,7 @@ function tick() { interval = setInterval(tick, 60000/setting('compression_rpm')); g.clear(1).setFont("6x8"); -g.drawString(setting('compression_count') + ' / ' + setting('breath_count'), 30, 200); +g.drawString(setting('compression_count') + ' / ' + setting('breath_count'), 30, g.getHeight()*5/6); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/devstopwatch/ChangeLog b/apps/devstopwatch/ChangeLog index e2b392fe9..7e90e061e 100644 --- a/apps/devstopwatch/ChangeLog +++ b/apps/devstopwatch/ChangeLog @@ -1,3 +1,8 @@ 0.01: App created 0.02: Persist state to storage to enable stopwatch to continue in the background 0.03: Modified to use setUI, theme and different screens +0.04: *bugfix* stopwatch broken with v0.03 setUI + realigned quick n dirty screen positions + help adjusted to fit bangle1 & bangle2 screen-size with widgets + fixed bangle2 colors for chrono and last lap highlight + added screen for bangle2 and a small README \ No newline at end of file diff --git a/apps/devstopwatch/README.md b/apps/devstopwatch/README.md new file mode 100644 index 000000000..02a13151f --- /dev/null +++ b/apps/devstopwatch/README.md @@ -0,0 +1,18 @@ +# dev stop watch + +stores state at kill + +## Bangle 1 +![](bangle1-dev-stopwatch-screenshot.png) + +* BTN1: start/lap +* BTN2: launcher +* BTN3: reset + +## Bangle 2 +![](bangle2-dev-stopwatch-screenshot.png) + +* TAP top right: start/lap +* TAP bottom right: reset +* Use BTN to get to launcher + diff --git a/apps/devstopwatch/app.js b/apps/devstopwatch/app.js index 83bb693a9..d2a4b1117 100644 --- a/apps/devstopwatch/app.js +++ b/apps/devstopwatch/app.js @@ -3,11 +3,11 @@ const EMPTY_H = '00:00:000'; const MAX_LAPS = 6; const XY_CENTER = g.getWidth() / 2; const big = g.getWidth()>200; -const Y_CHRONO = 40; -const Y_HEADER = big?80:60; -const Y_LAPS = big?125:90; +const Y_CHRONO = big?40:30; +const Y_HEADER = big?95:65; +const Y_LAPS = big?125:80; const H_LAPS = big?15:8; -const Y_BTN3 = big?225:165; +const Y_HELP = big?225:135; const FONT = '6x8'; const CHRONO = '/* C H R O N O */'; @@ -27,18 +27,17 @@ var state = require("Storage").readJSON("devstopwatch.state.json",1) || { // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ - if (btn==0) { - reset = false; - - if (state.started) { - changeLap(); - } else { - if (!reset) { - chronoInterval = setInterval(chronometer, 10); - } + switch (btn) { + case -1: + if (state.started) { + changeLap(); + } else { + chronoInterval = setInterval(chronometer, 10); + } + break; + case 1: resetChrono(); break; + default: Bangle.showLauncher(); break; //launcher handeled by ROM } -} - if (btn==1) resetChrono(); }); function resetChrono() { @@ -105,6 +104,7 @@ function printChrono() { var print = ''; + g.setColor(g.theme.fg); g.setFont(FONT, big?2:1); print = CHRONO; g.drawString(print, XY_CENTER, Y_CHRONO, true); @@ -124,7 +124,8 @@ function printChrono() { let suffix = ' '; if (state.currentLapIndex === i) { let suffix = '*'; - g.setColor("#f70"); + if (process.env.HWVERSION==2) g.setColor("#0ee"); + else g.setColor("#f70"); } const lapLine = `L${i - 1} ${state.laps[i]} ${suffix}\n`; @@ -133,8 +134,17 @@ function printChrono() { g.setColor(g.theme.fg); g.setFont(FONT, 1); - print = 'Press 3 to reset'; - g.drawString(print, XY_CENTER, Y_BTN3, true); + //help for model 2 or 1 + if (process.env.HWVERSION==2) { + print = /*LANG*/'TAP right top/bottom'; + g.drawString(print, XY_CENTER, Y_HELP, true); + print = /*LANG*/'start&lap/reset, BTN1: EXIT'; + g.drawString(print, XY_CENTER, Y_HELP+10, true); + } + else { + print = /*LANG*/'BTNs 1:startlap 2:exit 3:reset'; + g.drawString(print, XY_CENTER, Y_HELP, true); + } g.flip(); } diff --git a/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png b/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png index b668794b1..8a9c9b46e 100644 Binary files a/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png and b/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png differ diff --git a/apps/devstopwatch/bangle2-dev-stopwatch-screenshot.png b/apps/devstopwatch/bangle2-dev-stopwatch-screenshot.png new file mode 100644 index 000000000..a01c0c261 Binary files /dev/null and b/apps/devstopwatch/bangle2-dev-stopwatch-screenshot.png differ diff --git a/apps/doztime/README.md b/apps/doztime/README.md index 075b2f66a..2f5b04780 100644 --- a/apps/doztime/README.md +++ b/apps/doztime/README.md @@ -11,4 +11,4 @@ The year itself begins on the December solstice. Because that always happens, th The epoch (year numbering) begins in the last year when the perihelion coincided with the June solstice, near the beginning of the Holocene era. That astronomical basis makes the calendar free from politics, religion, or geography. -While the year number remains cardinal, BTN5 toggles between cardinal and ordinal for the rest of the calendar segments. BTN4 adds or removes a quickly changing digit to or from the clock. +While the year number remains cardinal, tapping on the right side of the watch face toggles between cardinal and ordinal for the rest of the calendar segments. Tapping on the left adds or removes a quickly changing digit to or from the clock. diff --git a/apps/doztime/app.js b/apps/doztime/app-bangle1.js similarity index 100% rename from apps/doztime/app.js rename to apps/doztime/app-bangle1.js diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js new file mode 100644 index 000000000..b77e5201a --- /dev/null +++ b/apps/doztime/app-bangle2.js @@ -0,0 +1,244 @@ +// Positioning values for graphics buffers +const g_height = 80; // total graphics height +const g_x_off = 0; // position from left was 16, then 8 here +const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240 +const g_width = 240 - 2 * g_x_off; // total graphics width +const g_height_d = 28; // height of date region was 32 +const g_y_off_d = 0; // y position of date region within graphics region +const spacing = 0; // space between date and time in graphics region +const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region +const g_height_t = 44; // height of time region was 48 + +// Other vars +const A1 = [30,30,30,30,31,31,31,31,31,31,30,30]; +const B1 = [30,30,30,30,30,31,31,31,31,31,30,30]; +const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; +const timeColour = "#ffffff"; +const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff +const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20 +const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20 +const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30 +const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30 +const baseYear = 11584; +const baseDate = Date(2020,11,21); // month values run from 0 to 11 +let accum = new Date(baseDate.getTime()); +let sequence = []; +let timeActiveUntil; +let addTimeDigit = false; +let dateFormat = false; +let lastX = 999999999; +let res = {}; +//var last_time_log = 0; + +var drawtime_timeout; + +// Date and time graphics buffers +var dateColour = "#ffffff"; // override later +var timeColour2 = timeColour; +var g_d = Graphics.createArrayBuffer(g_width,g_height_d,1,{'msb':true}); +var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true}); +// Set screen mode and function to write graphics buffers +//Bangle.setLCDMode(); +g.clear(); // start with blank screen +g.flip = function() +{ + g.setBgColor(0,0,0); + g.setColor(dateColour); + g.drawImage( + { + width:g_width, + height:g_height_d, + buffer:g_d.buffer + }, g_x_off, g_y_off + g_y_off_d); + g.setColor(timeColour2); + g.drawImage( + { + width:g_width, + height:g_height_t, + buffer:g_t.buffer + }, g_x_off, g_y_off + g_y_off_t); +}; + +setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1 +setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2 +//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); +//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true}); +//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true}); + +Bangle.on('touch', function(button, xy) { //from Gordon Williams + if (button==1) toggleTimeDigits(); + if (button==2) toggleDateFormat(); +}); + +function buildSequence(targ){ + for(let i=0;i n > dt)-1; + let year = baseYear+parseInt(index/12); + let month = index % 12; + let day = parseInt((dt-sequence[index])/86400000); + let colour = dateColours[day % 6]; + if(day==30){ colour=dateColours[6]; } + return({"year":year,"month":month,"day":day,"colour":colour}); +} +function toggleTimeDigits(){ + addTimeDigit = !addTimeDigit; + modeTime(); +} +function toggleDateFormat(){ + dateFormat = !dateFormat; + modeTime(); +} +function formatDate(res,dateFormat){ + let yyyy = res.year.toString(12); + calenDef = calen10; + if(!dateFormat){ //ordinal format + let mm = ("0"+(res.month+1).toString(12)).substr(-2); + let dd = ("0"+(res.day+1).toString(12)).substr(-2); + if(res.day==30){ + calenDef = calen7; + let m = ((res.month+1).toString(12)).substr(-2); + return(yyyy+"-"+"S"+m); // ordinal format + } + return(yyyy+"-"+mm+"-"+dd); + } + let m = res.month.toString(12); // cardinal format + let w = parseInt(res.day/6); + let d = res.day%6; + //return(yyyy+"-"+res.month+"-"+w+"-"+d); + return(yyyy+"-"+m+"-"+w+"-"+d); +} + +function writeDozTime(text,def){ + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_t.clear(); + g_t.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new + else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new + else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} +function writeDozDate(text,def,colour){ + + dateColour = colour; + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_d.clear(); + g_d.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new + else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new + else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} + +// Functions for time mode +function drawTime() +{ + let dt = new Date(); + let date = ""; + let timeDef; + let x = 0; + dt.setDate(dt.getDate()); + if(addTimeDigit){ + x = + 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); + let msg = "00000"+Math.floor(x).toString(12); + let time = msg.substr(-5,3)+"."+msg.substr(-2); + let wait = 347*(1-(x%1)); + timeDef = time6; + } else { + x = + 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds(); + let msg = "0000"+Math.floor(x).toString(12); + let time = msg.substr(-4,3)+"."+msg.substr(-1); + let wait = 4167*(1-(x%1)); + timeDef = time5; + } + if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day + date = formatDate(res,dateFormat); + if(dt2200)) { + } else { + // We have a GPS time. Set time + setTime(g.time.getTime()/1000); + } + }); + Bangle.setGPSPower(1,"time"); + setTimeout(fixTime, 10*60*1000); // every 10 minutes +} +// Start time fixing with GPS on next 10 minute interval +setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000); diff --git a/apps/ffcniftya/ChangeLog b/apps/ffcniftya/ChangeLog index 18bc264a3..420c553f5 100644 --- a/apps/ffcniftya/ChangeLog +++ b/apps/ffcniftya/ChangeLog @@ -1 +1,2 @@ 0.01: New Clock Nifty A +0.02: Shows the current week number (ISO8601), can be disabled via settings "" diff --git a/apps/ffcniftya/README.md b/apps/ffcniftya/README.md index f1fee9b1f..86f1f5c2d 100644 --- a/apps/ffcniftya/README.md +++ b/apps/ffcniftya/README.md @@ -1,4 +1,14 @@ # Nifty-A Clock +Colors are black/white - photos have non correct camera color "blue" + +## This is the clock + ![](screenshot_nifty.png) +## The week number (ISO8601) can be turned of in settings +(default is **"On"**) + +![](screenshot_settings_nifty.png) + + diff --git a/apps/ffcniftya/app.js b/apps/ffcniftya/app.js index 31742f64a..5da1ec48e 100644 --- a/apps/ffcniftya/app.js +++ b/apps/ffcniftya/app.js @@ -1,5 +1,6 @@ const locale = require("locale"); const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; +const CFG = require('Storage').readJSON("ffcniftya.json", 1) || {showWeekNum: true}; /* Clock *********************************************/ const scale = g.getWidth() / 176; @@ -16,6 +17,18 @@ const center = { y: Math.round(((viewport.height - widget) / 2) + widget), } +function ISO8601_week_no(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 + var tdt = new Date(date.valueOf()); + var dayn = (date.getDay() + 6) % 7; + tdt.setDate(tdt.getDate() - dayn + 3); + var firstThursday = tdt.valueOf(); + tdt.setMonth(0, 1); + if (tdt.getDay() !== 4) { + tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); + } + return 1 + Math.ceil((firstThursday - tdt) / 604800000); +} + function d02(value) { return ('0' + value).substr(-2); } @@ -29,23 +42,26 @@ function draw() { const minutes = d02(now.getMinutes()); const day = d02(now.getDate()); const month = d02(now.getMonth() + 1); - const year = now.getFullYear(); - - const month2 = locale.month(now, 3); - const day2 = locale.dow(now, 3); + const year = now.getFullYear(now); + const weekNum = d02(ISO8601_week_no(now)); + const monthName = locale.month(now, 3); + const dayName = locale.dow(now, 3); + const centerTimeScaleX = center.x + 32 * scale; g.setFontAlign(1, 0).setFont("Vector", 90 * scale); - g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale); - g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale); + g.drawString(hour, centerTimeScaleX, center.y - 31 * scale); + g.drawString(minutes, centerTimeScaleX, center.y + 46 * scale); g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale); + const centerDatesScaleX = center.x + 40 * scale; g.setFontAlign(-1, 0).setFont("Vector", 16 * scale); - g.drawString(year, center.x + 40 * scale, center.y - 62 * scale); - g.drawString(month, center.x + 40 * scale, center.y - 44 * scale); - g.drawString(day, center.x + 40 * scale, center.y - 26 * scale); - g.drawString(month2, center.x + 40 * scale, center.y + 48 * scale); - g.drawString(day2, center.x + 40 * scale, center.y + 66 * scale); + g.drawString(year, centerDatesScaleX, center.y - 62 * scale); + g.drawString(month, centerDatesScaleX, center.y - 44 * scale); + g.drawString(day, centerDatesScaleX, center.y - 26 * scale); + if (CFG.showWeekNum) g.drawString(d02(ISO8601_week_no(now)), centerDatesScaleX, center.y + 15 * scale); + g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale); + g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale); } diff --git a/apps/ffcniftya/screenshot_nifty.png b/apps/ffcniftya/screenshot_nifty.png index 0df056223..de939f6ba 100644 Binary files a/apps/ffcniftya/screenshot_nifty.png and b/apps/ffcniftya/screenshot_nifty.png differ diff --git a/apps/ffcniftya/screenshot_settings_nifty.png b/apps/ffcniftya/screenshot_settings_nifty.png new file mode 100644 index 000000000..b81a4662c Binary files /dev/null and b/apps/ffcniftya/screenshot_settings_nifty.png differ diff --git a/apps/ffcniftya/settings.js b/apps/ffcniftya/settings.js new file mode 100644 index 000000000..46e4ef5aa --- /dev/null +++ b/apps/ffcniftya/settings.js @@ -0,0 +1,23 @@ +(function(back) { + var FILE = "ffcniftya.json"; + // Load settings + var cfg = require('Storage').readJSON(FILE, 1) || { showWeekNum: true }; + + function writeSettings() { + require('Storage').writeJSON(FILE, cfg); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Nifty-A Clock" }, + "< Back" : () => back(), + 'week number?': { + value: cfg.showWeekNum, + format: v => v?"On":"Off", + onchange: v => { + cfg.showWeekNum = v; + writeSettings(); + } + } + }); +}) \ No newline at end of file diff --git a/apps/flipper/ChangeLog b/apps/flipper/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/flipper/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/flipper/README.md b/apps/flipper/README.md new file mode 100644 index 000000000..88025b8b2 --- /dev/null +++ b/apps/flipper/README.md @@ -0,0 +1,20 @@ +# Flipper + +![](flipper.png) + + *A utility to switch from the dark to the light theme and vice versa* + +* If the current theme is dark it will switch to the light theme +* If the current theme is light it will switch to the dark theme +* Combine with the awesome pattern launcher and it saves loads of time + + +## Demo Video + +There's no screenshot but there is a [demo video.](https://espruino.microco.sm/api/v1/files/9caa1afef7e4cce1d9b518af2dd271f1a57c5ecc.mp4) + +## Future Enhancements + +* Nothing left to add + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/flipper/flipper.app.js b/apps/flipper/flipper.app.js new file mode 100644 index 000000000..7171306b1 --- /dev/null +++ b/apps/flipper/flipper.app.js @@ -0,0 +1,39 @@ +const storage = require('Storage'); +let settings = storage.readJSON('setting.json', 1); + +function cl(x) { return g.setColor(x).getColor(); } + +function upd(th) { + g.theme = th; + settings.theme = th; + storage.write('setting.json', settings); + delete g.reset; + g._reset = g.reset; + g.reset = function(n) { return g._reset().setColor(th.fg).setBgColor(th.bg); }; + g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); }; + g.clear(1); +} + +function flipTheme() { + if (!g.theme.dark) { + upd({ + fg:cl("#fff"), bg:cl("#000"), + fg2:cl("#0ff"), bg2:cl("#000"), + fgH:cl("#fff"), bgH:cl("#00f"), + dark:true + }); + } else { + upd({ + fg:cl("#000"), bg:cl("#fff"), + fg2:cl("#000"), bg2:cl("#cff"), + fgH:cl("#000"), bgH:cl("#0ff"), + dark:false + }); + } +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +flipTheme(); +setTimeout(load, 20); diff --git a/apps/flipper/flipper.icon.js b/apps/flipper/flipper.icon.js new file mode 100644 index 000000000..494072c3c --- /dev/null +++ b/apps/flipper/flipper.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAO/mMUzs975K+ggLKysUAYNVqoLFitUoAKBqtQBYkJBIQABqwLEgQLEqtABggJDqkVBaoNCBZQwEgILWgoJENYsVBIcVBYpDEgpSIBYMBKQg6CuogCBY1UgoLCXAQLDqAsDBYhSBqEJHAoLDoEBcQ4LBEwILIMooLdIg4LaVoyaGERLcFao4LIdRAACYYUQBY5RKAH4Ar")) diff --git a/apps/flipper/flipper.png b/apps/flipper/flipper.png new file mode 100644 index 000000000..b91543070 Binary files /dev/null and b/apps/flipper/flipper.png differ diff --git a/apps/ftclock/.gitignore b/apps/ftclock/.gitignore new file mode 100644 index 000000000..b384cf1f2 --- /dev/null +++ b/apps/ftclock/.gitignore @@ -0,0 +1,4 @@ +timezonedb.csv.zip +country.csv +zone.csv +timezone.csv diff --git a/apps/ftclock/ChangeLog b/apps/ftclock/ChangeLog new file mode 100644 index 000000000..c944dd9ac --- /dev/null +++ b/apps/ftclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: first release +0.02: RAM efficient version of `fourTwentyTz.js` (as suggested by @gfwilliams). diff --git a/apps/ftclock/README.md b/apps/ftclock/README.md new file mode 100644 index 000000000..f30151552 --- /dev/null +++ b/apps/ftclock/README.md @@ -0,0 +1,24 @@ +# Four Twenty Clock + +A clock that tells when and where it's going to be [4:20](https://en.wikipedia.org/wiki/420_%28cannabis_culture%29) next + +![screensot](screenshot.png) ![screenshot at 4:20](screenshot1.png) + +## Generating `fourTwentyTz.js` + +Once in a while we need to regenerate it for 2 reasons: + +* One or more places got in or out of daylight saving time (DST) mode. +* The database saying _when_ places enter/exit DST mode got updated. + +I'll do my best to release a new version every time this happens, +but if you ever need to do this yourself, here's how: + +* `cd` to the `ftclock` folder +* If you haven't done so yet, run `npm install` there (this would create the `node_modules` folder). +* Get and unzip the latest `timezone.csv.zip` from https://timezonedb.com/download +* Run `npm run make` + +## Creator + +[Nimrod Kerrett](zzzen.com) diff --git a/apps/ftclock/app-icon.js b/apps/ftclock/app-icon.js new file mode 100644 index 000000000..297847e95 --- /dev/null +++ b/apps/ftclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AH4A/AH4A/AAMHu4ACuwHBs4HDsEGBIQLCsADBgwPDCAQGEuwXFBwI0GEAMHuAGCCoMHC4pMHEAIXEAgIGEBwI9BC4wSCC8IVCMAwIBs4XKUQJfITQgXCDwp8EHAqaECoLFEu4cDBIggBs6uFZozuGBAVmC4g+FMgZQEZQ5vGC4iRIC5IrDN4h5EC5J3BCoIKGgyaEC44VBC46yEDgoeDgxqLC5SCMAgoTFY47GFC4xFBdwwPBD4oWFAH4A/AH4A/AH4AjA==")) diff --git a/apps/ftclock/app.js b/apps/ftclock/app.js new file mode 100644 index 000000000..b12db10f1 --- /dev/null +++ b/apps/ftclock/app.js @@ -0,0 +1,51 @@ +let getNextFourTwenty = require("fourTwenty").getNextFourTwenty; +require("FontTeletext10x18Ascii").add(Graphics); +let leaf_img = "\x17\x18\x81\x00\x00\x10\x00\x00 \x00\x00@\x00\x01\xc0\x00\x03\x80\x00\x0f\x80\x00\x1f\x00\x00>\x00\x00|\x00\xc0\xf8\x19\xe1\xf0\xf1\xe3\xe3\xc3\xf7\xdf\x83\xff\xfe\x03\xff\xf8\x03\xff\xe0\x03\xff\x80\x03\xfe\x00\x7f\xff\xc0\xff\xff\xc0\x06\xe0\x00\x18\xc0\x00 \x80\x00\x00\x00"; + +// timeout used to update every minute +let drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function draw() { + g.reset(); + let date = new Date(); + let timeStr = require("locale").time(date,1); + let next420 = getNextFourTwenty(); + g.clearRect(0,26,g.getWidth(),g.getHeight()); + g.setColor("#00ff00").setFontAlign(0,-1).setFont("Teletext10x18Ascii",2); + g.drawString(next420.minutes? timeStr: `\0${leaf_img}${timeStr}\0${leaf_img}`, g.getWidth()/2, 28); + g.setColor(g.theme.fg); + g.setFontAlign(-1,-1).setFont("Teletext10x18Ascii"); + g.drawString(g.wrapString(next420.text, g.getWidth()-8).join("\n"),4,60); + + // queue draw in one minute + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// draw immediately at first, queue update +draw(); +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); +// Show launcher when middle button pressed +Bangle.setUI("clock"); diff --git a/apps/ftclock/app.png b/apps/ftclock/app.png new file mode 100644 index 000000000..0553837ca Binary files /dev/null and b/apps/ftclock/app.png differ diff --git a/apps/ftclock/fourTwenty.js b/apps/ftclock/fourTwenty.js new file mode 100644 index 000000000..b2a2aa8fb --- /dev/null +++ b/apps/ftclock/fourTwenty.js @@ -0,0 +1,47 @@ +let ftz = require("fourTwentyTz"), + offsets = ftz.offsets, + timezones = ftz.timezones; + +function get420offset() { + let current_time = Math.floor((Date.now()%(24*3600*1000))/60000); + let current_min = current_time%60; + if (current_min>20 && current_min<25) { + current_time -= current_min-20; // 5 minutes grace period + } + let offset = 16*60+20-current_time; + if (offset<0) { + offset += 24*60; + } + return offset; +} + +function makeFourTwentyText(minutes, places) { + //let plural = minutes==1? "": "s"; + //let msgprefix = minutes? `${minutes} minute${plural} to`: "It is now"; + let msgprefix = minutes? `${minutes}m to`: "It is now"; + let msgsuffix = places.length>1? ", and other fine places": ""; + let msgplace = places[Math.floor(Math.random()*places.length)]; + return `${msgprefix} 4:20 at ${msgplace}${msgsuffix}.`; +} + +function getNextFourTwenty() { + let offs = get420offset(); + for (let i=0; i { + countries[r[0]] = r[1]; + }) + .on('end', () => { + fs.createReadStream(__dirname+'/zone.csv') + .pipe(csv.parse()) + .on('data', (r) => { + let parts = r[2].replace('_',' ').split('/'); + let city = parts[parts.length-1]; + let country =''; + if (parts.length>2) { // e.g. America/North_Dakota/New_Salem + country = parts[1]; // e.g. North Dakota + } else { + country = countries[r[1]]; // e.g. United States + } + zones[parseInt(r[0])] = {"name": `${city}, ${country}`}; + }) + .on('end', () => { + fs.createReadStream(__dirname+'/timezone.csv') + .pipe(csv.parse()) + .on('data', (r) => { + code = parseInt(r[0]); + if (!(code in zones)) return; + starttime = parseInt(r[2] || "0"); // Bugger. They're feeding us blanks for UTC now + offs = parseInt(r[3]); + if (offs<0) { + offs += 60*60*24; + } + zone = zones[code]; + if (starttime { + for (z in zones) { + zone = zones[z]; + if (zone.offs%60) continue; // One a dem funky timezones. Ignore. + zonelist = offsdict[zone.offs] || []; + zonelist.push(zone.name); + offsdict[zone.offs] = zonelist; + } + offsets = []; + for (o in offsdict) { + offsets.unshift(parseInt(o)); + } + fs.open("fourTwentyTz.js","w", (err, fd) => { + if (err) { + console.log("Can't open output file"); + return; + } + fs.write(fd, "// Generated by mkFourTwentyTz.js\n", handleWrite); + fs.write(fd, `// ${Date()}\n`, handleWrite); + fs.write(fd, "// Data source: https://timezonedb.com/files/timezonedb.csv.zip\n", handleWrite); + fs.write(fd, "exports.offsets = ", handleWrite); + fs.write(fd, JSON.stringify(offsets), handleWrite); + fs.write(fd, ";\n", handleWrite); + fs.write(fd, "exports.timezones = function(offs) {\n", handleWrite); + fs.write(fd, " switch (offs) {\n", handleWrite); + for (i=0; i Bangle.on('GPS-raw', onGPSraw), 10); + listenerGPSraw = 1; + } + + lastFix = fix; + lastFix.SATinView = SATinView; } function onGPSraw(nmea) { - if (nmea.slice(3,6) == "GSV") { - // console.log(nmea); - if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13)); - if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13)); - SATinView = nofBD + nofGP; - } + if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13)); + if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13)); + SATinView = nofBD + nofGP; } + Bangle.loadWidgets(); Bangle.drawWidgets(); Bangle.on('GPS', onGPS); -Bangle.on('GPS-raw', onGPSraw); +//Bangle.on('GPS-raw', onGPSraw); +Bangle.setGPSPower(1, "app"); + +function exitApp() { + load(); +} + +setWatch(_=>exitApp(), BTN1); +if (global.BTN2) { + setWatch(_=>exitApp(), BTN2); + setWatch(_=>exitApp(), BTN3); +} diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/Changelog index 7f837e50e..e4a0bdfe8 100644 --- a/apps/gpstouch/Changelog +++ b/apps/gpstouch/Changelog @@ -1 +1,2 @@ 0.01: First version +0.02: Enchanced contrast of icon image diff --git a/apps/gpstouch/gpstouch.icon.js b/apps/gpstouch/gpstouch.icon.js index c4cf85676..3e05da0ff 100644 --- a/apps/gpstouch/gpstouch.icon.js +++ b/apps/gpstouch/gpstouch.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA=")) +require("heatshrink").decompress(atob("mEw4UA///iADCn+EqoAWqAuJgoLcn/8BZENGwNwBY/VBYNXBY0DJ4fABYoiCEggLDmtX1Wq6tcBYvVrQLB0owCBYdVtQLB1NVBYg6BBQIABHgQLCgIuCGAVABYcNqwtBGIOVJAILFyoCCBY5eBBdo7IgIIB1t6BYJfENZaDB9QKB1aDFBYKbEBYizBrwLB2qnFdwSmCX401cYdUBZTjGfYgHCBZB2BBYhUBAARSBBYhICAAIGCBYkVBQJSCBYpICIwQLFHgQ6CBYo8CHQQLFHgQFDBYsVQIQLHgo6DBY0BHQYLGgY6DBYwAFBbCjDACY")) diff --git a/apps/gpstouch/gpstouch.png b/apps/gpstouch/gpstouch.png index c411356ae..a40419a3f 100644 Binary files a/apps/gpstouch/gpstouch.png and b/apps/gpstouch/gpstouch.png differ diff --git a/apps/hralarm/ChangeLog b/apps/hralarm/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/hralarm/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/hralarm/README.md b/apps/hralarm/README.md new file mode 100644 index 000000000..37b14ad9d --- /dev/null +++ b/apps/hralarm/README.md @@ -0,0 +1,15 @@ +# Heart rate alarm + +This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits. + +## Usage + +Configure the heart rate limits in the apps settings. This widget uses both 'HRM' and 'BTHRM' events. + +## Features + +Long vibration every 10 seconds on reaching upper limit, short vibrations between upper limit and warning threshold and an single vibration when reaching the lower limit again. + +## Requests/Creator + +https://github.com/halemmerich diff --git a/apps/hralarm/settings.js b/apps/hralarm/settings.js new file mode 100644 index 000000000..3158ab8b7 --- /dev/null +++ b/apps/hralarm/settings.js @@ -0,0 +1,57 @@ +(function(back) { + var FILE = "hralarm.json"; + + var settings = Object.assign({ + enabled: false, + upper: 180, + warning: 170, + lower: 150, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + '': { 'title': 'HR Alarm' }, + '< Back': back, + 'Enabled': { + value: !!settings.enabled, + format: v => settings.enabled ? "On" : "Off", + onchange: v => { + settings.enabled = v; + writeSettings(); + } + }, + 'Upper limit': { + value: settings.upper, + min: 0, + step:5, + max: 300, + onchange: v => { + settings.upper = v; + writeSettings(); + } + }, + 'Lower limit': { + value: settings.lower, + min: 0, + step:5, + max: 300, + onchange: v => { + settings.lower = v; + writeSettings(); + } + }, + 'Warning at': { + value: settings.warning, + min: 0, + step:5, + max: 300, + onchange: v => { + settings.warning = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/hralarm/widget.js b/apps/hralarm/widget.js new file mode 100644 index 000000000..30a94fdf2 --- /dev/null +++ b/apps/hralarm/widget.js @@ -0,0 +1,27 @@ +(() => { + var settings = require('Storage').readJSON("hralarm.json", true) || {}; + if (!settings.enabled){ Bangle.setHRMPower(0, 'hralarm'); return; } + Bangle.setHRMPower(1, 'hralarm'); + var hitLimit = 0; + var checkHr = function(hr){ + if (hr.bpm > settings.warning && hr.bpm <= settings.upper){ + Bangle.buzz(100, 1); + } + if (hitLimit < getTime() && hr.bpm > settings.upper){ + hitLimit = getTime() + 10; + Bangle.buzz(2000, 1); + } + if (hitLimit > 0 && hr.bpm < settings.lower){ + hitLimit = 0; + Bangle.buzz(500, 1); + } + }; + Bangle.on("HRM", checkHr); + Bangle.on("BTHRM", checkHr); + + WIDGETS["hralarm"]={ + area:"tl", + width: 0, + draw: function(){} + }; +})() diff --git a/apps/hralarm/widget.png b/apps/hralarm/widget.png new file mode 100644 index 000000000..726cf3f9b Binary files /dev/null and b/apps/hralarm/widget.png differ diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index 5e60068aa..1873649f9 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -2,9 +2,7 @@ 0.02: Remove messages on disconnect 0.03: Handling of message actions (ok/clear) 0.04: Added common bundleId's -0.05: Added more bundleId's (app-id's which can be used to - determine a friendly app name in the notifications) +0.05: Added more bundleId's (app-id's which can be used to determine a friendly app name in the notifications) 0.06: Fix (not) popupping up old messages -0.07: Added more details from music (instead of Undefined) - Added more app identifiers - +0.07: Added more details from music (instead of Undefined), added more app identifiers +0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements diff --git a/apps/ios/boot.js b/apps/ios/boot.js index 8ccfb617d..d317c23b0 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -66,6 +66,7 @@ E.on('notify',msg=>{ "com.apple.mobilecal": "Calendar", "com.apple.mobilemail": "Mail", "com.apple.mobilephone": "Phone", + "com.apple.mobileslideshow": "Pictures", "com.apple.MobileSMS": "SMS Message", "com.apple.Passbook": "iOS Wallet", "com.apple.podcasts": "Podcasts", @@ -83,6 +84,7 @@ E.on('notify',msg=>{ "com.ifttt.ifttt" : "IFTTT", "com.jumbo.app" : "Jumbo", "com.linkedin.LinkedIn" : "LinkedIn", + "com.marktplaats.iphone": "Marktplaats", "com.microsoft.Office.Outlook" : "Outlook Mail", "com.nestlabs.jasper.release" : "Nest", "com.netflix.Netflix" : "Netflix", @@ -90,6 +92,7 @@ E.on('notify',msg=>{ "com.skype.skype": "Skype", "com.skype.SkypeForiPad": "Skype", "com.spotify.client": "Spotify", + "com.storytel.iphone": "Storytel", "com.strava.stravaride": "Strava", "com.tinyspeck.chatlyio": "Slack", "com.toyopagroup.picaboo": "Snapchat", @@ -98,6 +101,8 @@ E.on('notify',msg=>{ "com.vilcsak.bitcoin2": "Coinbase", "com.wordfeud.free": "WordFeud", "com.zhiliaoapp.musically": "TikTok", + "io.robbie.HomeAssistant": "Home Assistant", + "net.weks.prowl": "Prowl", "net.whatsapp.WhatsApp": "WhatsApp", "nl.ah.Appie": "Albert Heijn", "nl.postnl.TrackNTrace": "PostNL", @@ -118,7 +123,7 @@ E.on('notify',msg=>{ new : msg.new, title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer), subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer), - body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) + body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) || "Cannot display" }); // TODO: posaction/negaction? }); diff --git a/apps/lapcounter/ChangeLog b/apps/lapcounter/ChangeLog index 9db0e26c5..146ff1b05 100644 --- a/apps/lapcounter/ChangeLog +++ b/apps/lapcounter/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: Themeable app icon diff --git a/apps/lapcounter/app-icon.js b/apps/lapcounter/app-icon.js index a443b3a41..354c07124 100644 --- a/apps/lapcounter/app-icon.js +++ b/apps/lapcounter/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap")) +require("heatshrink").decompress(atob("mEwwI0xg+evPsAon+ApX8Aon4AonwAod78AFDv4FWvoFE/IFDz4FXvIFD3wFE/wFW7wFDh5xBAoUfAok/Aol/BZUXAogA6A=")) diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index c171ec3d4..dba05915b 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -5,4 +5,9 @@ 0.05: Additional icons for (1) charging and (2) bat < 30%. 0.06: Fix - Alarm disabled, if clock was closed. 0.07: Added settings to adjust data that is shown for each row. -0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. \ No newline at end of file +0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. +0.09: Tab anywhere to open the launcher. +0.10: Removed swipes to be compatible with the Pattern Launcher. Stability improvements. +0.11: Show the gadgetbridge weather temperature (settings). +0.12: Added humidity to data. +0.13: Improved battery visualization. \ No newline at end of file diff --git a/apps/lcars/README.md b/apps/lcars/README.md index 97695a408..46e134f78 100644 --- a/apps/lcars/README.md +++ b/apps/lcars/README.md @@ -4,18 +4,28 @@ A simple LCARS inspired clock. Note: To display the steps, the health app is required. If this app is not installed, the data will not be shown. To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerdavid/BangleApps) +## Control + * Tap left / right to change between screens. + * Tap top / bottom to control the current screen. + ## Features * LCARS Style watch face. - * Full screen mode - widgets are still loaded. - * Supports multiple screens with different data. - * [Screen 1] Date + Time + Lock status. - * [Screen 1] Shows randomly images of real planets. - * [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.) - * [Screen 1] Swipe up/down to activate an alarm. - * [Screen 1] Shows 3 customizable datapoints on the first screen. - * [Screen 1] The lower orange line indicates the battery level. - * [Screen 2] Display month graphs for steps + hrm on the second screen. + * Full screen mode - widgets are still loaded but not shown. + * Tab on left/right to switch between different screens. + * Cusomizable data that is shown on screen 1 (steps, weather etc.) + * Shows random images of real planets. + * Tap on top/bottom of screen 1 to activate an alarm. + * The lower orange line indicates the battery level. + * Display graphs for steps + hrm on the second screen. +## Data that can be configured + * Steps - Steps loaded via the health module + * Battery - Current battery level in % + * VREF - Voltage of battery + * HRM - Last measured HRM + * Temp - Weather temperature loaded via the weather module + gadgetbridge + * Humidity - Humidity loaded via the weather module + gadgetbridge + * CoreT - Temperature of device ## Multiple screens support Access different screens via swipe left/ right @@ -24,10 +34,7 @@ Access different screens via swipe left/ right ![](screenshot_2.png) -## Icons -
Icons made by Smashicons, Freepik from www.flaticon.com
- - ## Contributors -- Creator: [David Peer](https://github.com/peerdavid). +- Initial creation and improvements: [David Peer](https://github.com/peerdavid). - Improvements: [Adam Schmalhofer](https://github.com/adamschmalhofer). +- Improvements: [Jon Warrington](https://github.com/BartokW). diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index 74d0450c0..3b9745a7a 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -1,15 +1,11 @@ const SETTINGS_FILE = "lcars.setting.json"; -const Storage = require("Storage"); - - -// ...and overwrite them with any saved values -// This way saved values are preserved if a new version adds more settings +const locale = require('locale'); const storage = require('Storage') let settings = { alarm: -1, - dataRow1: "Battery", - dataRow2: "Steps", - dataRow3: "Temp." + dataRow1: "Steps", + dataRow2: "Temp", + dataRow3: "Battery" }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -24,6 +20,7 @@ let cOrange = "#FF9900"; let cPurple = "#FF00DC"; let cWhite = "#FFFFFF"; let cBlack = "#000000"; +let cGrey = "#424242"; /* * Global lcars variables @@ -31,13 +28,13 @@ let cBlack = "#000000"; let lcarsViewPos = 0; let drag; let hrmValue = 0; -var connected = NRF.getSecurityStatus().connected; -var plotWeek = false; +var plotMonth = false; +var disableInfoUpdate = true; // When gadgetbridge connects, step infos cannot be loaded /* * Requirements and globals */ -const locale = require('locale'); + var bgLeft = { width : 27, height : 176, bpp : 3, @@ -121,40 +118,49 @@ function queueDraw() { function printData(key, y, c){ g.setFontAlign(-1,-1,0); - var text = "ERR"; - var value = "NOT FOUND"; + key = key.toUpperCase() + var text = key; + var value = "ERR"; - if(key == "Battery"){ - text = "BAT"; - value = E.getBattery() + "%"; - - } else if(key == "Steps"){ + if(key == "STEPS"){ text = "STEP"; value = getSteps(); - } else if(key == "Temp."){ - text = "TEMP"; - value = Math.floor(E.getTemperature()) + "C"; - - } else if(key == "HRM"){ - text = "HRM"; - value = hrmValue; + } else if(key == "BATTERY"){ + text = "BAT"; + value = E.getBattery() + "%"; } else if (key == "VREF"){ - text = "VREF"; value = E.getAnalogVRef().toFixed(2) + "V"; + } else if(key == "HRM"){ + value = hrmValue; + + } else if (key == "TEMP"){ + var weather = getWeather(); + value = weather.temp; + + } else if (key == "HUMIDITY"){ + text = "HUM"; + var weather = getWeather(); + value = weather.hum + "%"; + + } else if(key == "CORET"){ + value = locale.temp(parseInt(E.getTemperature())); } g.setColor(c); + g.fillRect(79, y-2, 85 ,y+18); + + g.setFontAlign(0,-1,0); + g.drawString(value, 110, y); + + g.setColor(c); + g.setFontAlign(-1,-1,0); g.fillRect(133, y-2, 165 ,y+18); g.fillCircle(161, y+8, 10); g.setColor(cBlack); g.drawString(text, 135, y); - - g.setColor(c); - g.setFontAlign(1,-1,0); - g.drawString(value, 130, y); } function drawHorizontalBgLine(color, x1, x2, y, h){ @@ -166,7 +172,7 @@ function drawHorizontalBgLine(color, x1, x2, y, h){ } -function drawLock(){ +function drawInfo(){ if(lcarsViewPos != 0){ return; } @@ -175,7 +181,8 @@ function drawLock(){ g.setColor(cOrange); g.clearRect(120, 10, g.getWidth(), 75); g.drawString("LCARS", 128, 13); - if(connected){ + + if(NRF.getSecurityStatus().connected){ g.drawString("CONN", 128, 33); } else { g.drawString("NOCON", 128, 33); @@ -191,13 +198,14 @@ function drawState(){ return; } - g.clearRect(20, 93, 77, 170); - g.setColor(cWhite); - var bat = E.getBattery(); - var current = new Date(); - var hours = current.getHours(); + g.clearRect(20, 93, 75, 170); + g.setFontAlign(0, 0, 0); + g.setFontAntonioMedium(); if(!isAlarmEnabled()){ + var bat = E.getBattery(); + var current = new Date(); + var hours = current.getHours(); var iconImg = Bangle.isCharging() ? iconCharging : bat < 30 ? iconNoBattery : @@ -206,16 +214,16 @@ function drawState(){ hours % 4 == 1 ? iconMars : hours % 4 == 2 ? iconMoon : iconEarth; - g.drawImage(iconImg, 29, 104); + g.drawImage(iconImg, 24, 118); + g.setColor(cWhite); + g.drawString("STATUS", 24+25, 108); } else { // Alarm within symbol - g.setFontAntonioMedium(); - g.setFontAlign(0, 0, 0); g.setColor(cOrange); - g.drawString("ALARM", 29+25, 107); + g.drawString("ALARM", 24+25, 108); g.setColor(cWhite); g.setFontAntonioLarge(); - g.drawString(getAlarmMinutes(), 29+25, 107+35); + g.drawString(getAlarmMinutes(), 24+25, 108+35); } g.setFontAlign(-1, -1, 0); @@ -234,12 +242,17 @@ function drawPosition0(){ // The last line is a battery indicator too var bat = E.getBattery() / 100.0; - var batX2 = parseInt((172 - 35) * bat + 35); - drawHorizontalBgLine(cOrange, 35, batX2, 171, 5); - drawHorizontalBgLine(cPurple, batX2+10, 172, 171, 5); + var batStart = 19; + var batWidth = 172 - batStart; + var batX2 = parseInt(batWidth * bat + batStart); + drawHorizontalBgLine(cOrange, batStart, batX2, 171, 5); + drawHorizontalBgLine(cGrey, batX2, 172, 171, 5); + for(var i=0; i+batStart<=172; i+=parseInt(batWidth/4)){ + drawHorizontalBgLine(cBlack, batStart+i, batStart+i+3, 168, 8) + } - // Draw logo - drawLock(); + // Draw Infos + drawInfo(); // Write time g.setFontAlign(-1, -1, 0); @@ -247,15 +260,15 @@ function drawPosition0(){ var currentDate = new Date(); var timeStr = locale.time(currentDate,1); g.setFontAntonioLarge(); - g.drawString(timeStr, 28, 10); + g.drawString(timeStr, 27, 10); // Write date g.setColor(cWhite); g.setFontAntonioMedium(); var dayStr = locale.dow(currentDate, true).toUpperCase(); dayStr += " " + currentDate.getDate(); - dayStr += " " + currentDate.getFullYear(); - g.drawString(dayStr, 29, 56); + dayStr += " " + locale.month(currentDate, 1).toUpperCase(); + g.drawString(dayStr, 30, 56); // Draw data g.setFontAlign(-1, -1, 0); @@ -294,7 +307,7 @@ function drawPosition1(){ } // Plot HRM graph - if(plotWeek){ + if(plotMonth){ var data = new Uint16Array(32); var cnt = new Uint8Array(32); health.readDailySummaries(new Date(), h=>{ @@ -331,8 +344,8 @@ function drawPosition1(){ g.setFontAlign(1, 1, 0); g.setFontAntonioMedium(); g.setColor(cWhite); - g.drawString("WEEK HRM", 154, 27); - g.drawString("WEEK STEPS [K]", 154, 115); + g.drawString("M-HRM", 154, 27); + g.drawString("M-STEPS [K]", 154, 115); // Plot day } else { @@ -372,8 +385,8 @@ function drawPosition1(){ g.setFontAlign(1, 1, 0); g.setFontAntonioMedium(); g.setColor(cWhite); - g.drawString("DAY HRM", 154, 27); - g.drawString("DAY STEPS", 154, 115); + g.drawString("D-HRM", 154, 27); + g.drawString("D-STEPS", 154, 115); } } @@ -401,7 +414,8 @@ function draw(){ * Step counter via widget */ function getSteps() { - var steps = 0 + var steps = 0; + let health; try { health = require("health"); } catch(ex) { @@ -413,6 +427,32 @@ function getSteps() { } +function getWeather(){ + var weather; + + try { + weather = require('weather').get(); + } catch(ex) { + // Return default + } + + if (weather === undefined){ + weather = { + temp: "-", + hum: "-", + txt: "-", + wind: "-", + wdir: "-", + wrose: "-" + }; + } else { + weather.temp = locale.temp(parseInt(weather.temp-273.15)) + } + + return weather; +} + + /* * Handle alarm */ @@ -451,7 +491,7 @@ function handleAlarm(){ .then(() => { // Update alarm state to disabled settings.alarm = -1; - Storage.writeJSON(SETTINGS_FILE, settings); + storage.writeJSON(SETTINGS_FILE, settings); }); } @@ -462,24 +502,17 @@ function handleAlarm(){ Bangle.on('lcdPower',on=>{ if (on) { // Whenever we connect to Gadgetbridge, reading data from - // health failed. Therefore, we update and read data from - // health iff the connection state did not change. - if(connected == NRF.getSecurityStatus().connected) { - draw(); - } else { - connected = NRF.getSecurityStatus().connected - drawLock(); - } + // health failed. Therefore, we update only partially... + drawInfo(); + drawState(); } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } - - connected = NRF.getSecurityStatus().connected }); Bangle.on('lock', function(isLocked) { - drawLock(); + drawInfo(); }); Bangle.on('charging',function(charging) { @@ -498,7 +531,7 @@ function increaseAlarm(){ settings.alarm = getCurrentTimeInMinutes() + 5; } - Storage.writeJSON(SETTINGS_FILE, settings); + storage.writeJSON(SETTINGS_FILE, settings); } @@ -509,47 +542,55 @@ function decreaseAlarm(){ settings.alarm = -1; } - Storage.writeJSON(SETTINGS_FILE, settings); + storage.writeJSON(SETTINGS_FILE, settings); } +function feedback(){ + Bangle.buzz(40, 0.3); +} -// Thanks to the app "gbmusic" for this code to detect swipes in all 4 directions. -Bangle.on("drag", e => { - if (!drag) { // start dragging - drag = {x: e.x, y: e.y}; - } else if (!e.b) { // released - const dx = e.x-drag.x, dy = e.y-drag.y; - drag = null; +// Touch gestures to control clock. We don't use swipe to be compatible with the bangle ecosystem +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.2); + var right = g.getWidth() - left; + var upper = parseInt(g.getHeight() * 0.2); + var lower = g.getHeight() - upper; - // Horizontal swipe - if (Math.abs(dx)>Math.abs(dy)+10) { - if(dx > 0){ - lcarsViewPos = 0; - } else { - lcarsViewPos = 1; - } - - // Vertical swipe - } else if (Math.abs(dy)>Math.abs(dx)+10) { - if(lcarsViewPos == 0){ - if(dy > 0){ - decreaseAlarm(); - } else { - increaseAlarm(); - } - - // Only update the state and return to - // avoid a full draw as this is much faster. - drawState(); - return; - } - - if(lcarsViewPos == 1){ - plotWeek = dy < 0 ? true : false; - } - } + var is_left = e.x < left; + var is_right = e.x > right; + var is_upper = e.y < upper; + var is_lower = e.y > lower; + if(is_left && lcarsViewPos == 1){ + feedback(); + lcarsViewPos = 0; draw(); + return; + + } else if(is_right && lcarsViewPos == 0){ + feedback(); + lcarsViewPos = 1; + draw(); + return; + } + + if(lcarsViewPos == 0){ + if(is_upper){ + feedback(); + increaseAlarm(); + drawState(); + return; + } if(is_lower){ + feedback(); + decreaseAlarm(); + drawState(); + return; + } + } else if (lcarsViewPos == 1 && (is_upper || is_lower) && plotMonth != is_lower){ + feedback(); + plotMonth = is_lower; + draw(); + return; } }); diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js index 0d004b002..ba630799a 100644 --- a/apps/lcars/lcars.settings.js +++ b/apps/lcars/lcars.settings.js @@ -7,7 +7,7 @@ alarm: -1, dataRow1: "Battery", dataRow2: "Steps", - dataRow3: "Temp." + dataRow3: "Temp" }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -18,14 +18,14 @@ storage.write(SETTINGS_FILE, settings) } - var data_options = ["Battery", "Steps", "Temp.", "HRM", "VREF"]; + var data_options = ["Steps", "Battery", "VREF", "HRM", "Temp", "Humidity", "CoreT"]; E.showMenu({ '': { 'title': 'LCARS Clock' }, '< Back': back, 'Row 1': { value: 0 | data_options.indexOf(settings.dataRow1), - min: 0, max: 4, + min: 0, max: 6, format: v => data_options[v], onchange: v => { settings.dataRow1 = data_options[v]; @@ -34,7 +34,7 @@ }, 'Row 2': { value: 0 | data_options.indexOf(settings.dataRow2), - min: 0, max: 4, + min: 0, max: 6, format: v => data_options[v], onchange: v => { settings.dataRow2 = data_options[v]; @@ -43,7 +43,7 @@ }, 'Row 3': { value: 0 | data_options.indexOf(settings.dataRow3), - min: 0, max: 4, + min: 0, max: 6, format: v => data_options[v], onchange: v => { settings.dataRow3 = data_options[v]; diff --git a/apps/lcars/screenshot.png b/apps/lcars/screenshot.png index b3dfd4200..120229fba 100644 Binary files a/apps/lcars/screenshot.png and b/apps/lcars/screenshot.png differ diff --git a/apps/limelight/ChangeLog b/apps/limelight/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/limelight/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/limelight/README.md b/apps/limelight/README.md new file mode 100644 index 000000000..49b858127 --- /dev/null +++ b/apps/limelight/README.md @@ -0,0 +1,19 @@ +# Limelight + *Simple configurable analogue clock based on the work of @Andreas_Rozek [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock)* + +![](screenshot_limelight.png) + +* Selection of different fonts +* Settings menu where you can select font, or switch to Vector font and try a range of sizes +* Reduction by 100 lines of code, demonstrating that there is no need for a custom widget draw method +* Full screen option (widgets are loaded but not displayed) + +![](screenshot_gochihand.png) +![](screenshot_monoton.png) +![](screenshot_grenadier.png) + +Many thanks for @Andreas_Rozek for his pioneering work on building an analogue clock toolkit for the Bangle 2. + +Limelight Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS +Forum](http://forum.espruino.com/microcosms/1424/) + diff --git a/apps/limelight/limelight.app.js b/apps/limelight/limelight.app.js new file mode 100644 index 000000000..20d79deeb --- /dev/null +++ b/apps/limelight/limelight.app.js @@ -0,0 +1,263 @@ +/* + * Limelight analoguce clock with bolted hands + * Based on the work of @Andreas_Rozek + * [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock) + * + * . Demonstrates simpler approach to establishing the available size of the appRect in relation + * to widgets, avoids having to take on the responsibility for managing the widget draw. + * . Demonstrates a settings menu and various configuration options + * . Demonstrates fullscreen verses, widgets and app area. + * + */ + +g.clear(); + +const SETTINGS_FILE = "limelight.json"; +var UPDATE_PERIOD; +var drawTimeout; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; + settings.secondhand = settings.secondhand||false; + settings.font = settings.font||"Limelight"; + settings.vector = settings.vector||false; + settings.fullscreen = settings.fullscreen||false; + settings.vector_size = settings.vector_size||42; + UPDATE_PERIOD = (settings.secondhand ? 1000 : 60000); +} + +loadSettings(); + +// if we are not full screen then load and draw the widgets so that Bangle.appRect gets set +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +// fonts.google.com +Graphics.prototype.setFontLimelight = function(scale) { + // Actual height 28 (28 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAeAAAAAD8AAAAAf4AAAAB/gAAAAH+AAAAAf4AAAAB/gAAAAD8AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAPwAAAAH8AAAAD+AAAAD+AAAAB/AAAAA/gAAAAfwAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAA/gAAAA//wAAAP//wAAB///wAAP///gAA///+AAH///8AAf///4AD////gAP///+AA////4AD////gAMAAAGAAwAAAYADAAABgAMAAAGAAwAAAwABgAADAAHAAAYAAOAADgAAeAA8AAAfh/AAAAf/wAAAAHgAAAAAAAAAAGAAAAAAYAAAAABAAAAAAMAAAAAAwAAAAAD///+AAf///4AB////gAH///+AAf///4AD////gAP///+AA////4AH////gAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgABAAAeAAOAAH4AAwAA/gAGAAP+AAYAB/4ADAAf/gAMAD/+AAwAf/4ADAH//gAMA//+AAwH//4ADB//9gAOP//GAA///wYAD//+BgAH//gGAAf/8AYAA//ABgAB/4AGAAD+AAYAADAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAHAAAwAAGAAHAAAMAAYAAAwADAEABgAMAwAGAAwDAAYADAMABgAMAwAGAAwDAAYAD////gAP///+AA////4AD////gAP///8AAf///wAB/7//AAD/H/4AAP4f/AAAPA/4AAAAA+AAAAAAAAAAAAAAAAAAAGAAAAAB8AAAAAPwAAAADzAAAAAcMAAAAHgwAAAA8DAAAAHAMAAAB4AwAAAOADAAADwAMAAAcAAwAAD///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD////gAP///+AAAAAMAAAAAAwAAAAADAAAAAAcAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAHAAAH4AOAAP/gAcAA+GAAwADAwABgAMDAAGAAwMAAYADAwABgAMDAAGAAwMAAYADA///gAMD//+AAwP//4ADA///AAMB//8AAwH//wADAP/+AAIAf/wAAAA/+AAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAD/8AAAA//8AAAH//4AAB///wAAH///gAA////AAH///8AAf///4AD////gAP///+AAwAAAYADAYABgAMBgAGAAwGAAYADAYABgAMBgAGAAwGAAwABgYADAAHAwAYAAMDgHAAAAHh4AAAAP/AAAAAHgAAAAAAAAAAAAAAAA+AAAAAD4AAAAAMAAAAAAwAAAAADAAAAAAMAA/+AAwB//4ADB///gAM///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD//+AAAP/wAAAA/wAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAA/g/wAAH/GDgAA/+wGAAD//AMAAf/4AwAB//gBgAP//AGAAx/+AYADD/4BgAMH/wGAAwP/AYACA/+BgAMB/8GAAwD/wYADAP/jgAMBf/+AAYH//wABgz//AADHH/4AAH4f/gAAOA/8AAAAB/AAAAAAwAAAAAAAAAAAAAAAAAeAAAAAH/AAAAB8eAAAAGAcBgAAwAwHAAGABgMAAYAGAYADAAIBgAMAAwGAAwADAYADAAYBgAMAAgGAAwAAAYAD////gAP///+AA////wAB////AAH///4AAP///AAA///8AAA///AAAB//4AAAB/+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAeAAAD8D8AAAf4f4AAB/h/gAAH+H+AAAf4f4AAB/h/gAAD8D8AAAHgHgAAAAAAAAAAAAAAA="), 46, atob("DQ0aExgZHRkbGBsbDQ=="), 40+(scale<<8)+(1<<16)); +} + +// fonts.google.com +Graphics.prototype.setFontGochiHand = function(scale) { + // Actual height 29 (31 - 3) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAB4AAAAAD4AAAAAB4AAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAH/gAAAD//gAAD///gAD///+AAH///AAAH//gAAAH/wAAAAHwAAAAAAAAwAAAAAP+AAAAA//gAAAB//wAAAD//4AAAD8P4AAAHwD8AAAHgB8AAAPgA8AAAPAA8AAAPAA8AAAPAA8AAAPgA8AAAPgA8AAAPgB8AAAHwB4AAAH4D4AAAD+PwAAAD//gAAAB//gAAAA/+AAAAAP8AAAAAAAAAAAAAAAAAAAcAAAAAA8AAAAAB8AAAAAD4AAAAAD4AAAAAHwAAAAAHgAAAAAPgAAAAAPgAAAAAf/AAAAAf//wAAAP//wAAAH//wAAAAf/wAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAA8AAAeAB8AAA+AD8AAB+AH8AAB4AP8AAD4AP8AADwAf8AADwB+8AAD4D88AAD4H48AAD//w+AAB//g+AAB//A+AAA/8A+AAAPwA+AAAAAA+AAAAAAcAAAAAAIAAAAAAAAAA8AAAAAB8AAAAAB8AAAAAB4AHgAAD4AHwAAD4AH4AADwPH8AADwfB8AADwfA8AAD4fA+AAD4fA+AAB//A+AAB//A+AAA//A8AAA//x8AAAPP/8AAAAH/4AAAAD/wAAAAB/gAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAH8AAAAAP+AAAAA/+AAAAB/+AAAAD8+AAAAP4+AAAB/weAAAD/weAAAD/8eAAAB///AAAA///AAAAH//8AAAAf//AAAAD//AAAAAf/AAAAAf+AAAAAfAAAAAAMAAAAAAAAAAAAAAAAAAPA/gAAAfh/wAAA/x/4AAA/x/4AAB/4/8AAB74B8AAB58A8AAB58A+AAB5+A+AAB4+A+AAB4+A+AAB4fA+AAB4fg+AAB4Pg8AAB4P58AAB4H/4AAB4D/4AAB4D/wAAAQA/gAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAB/+AAAAD//gAAAH//gAAAPwfwAAAPgf4AAAfAf4AAAeA/8AAAeA98AAAeA88AAAfB48AAAfB48AAAPB48AAAOB48AAAAB98AAAAB/8AAAAA/4AAAAA/wAAAAAfgAAA8AAAAAA8BAAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA+HgAAAA+HgAAAA+HgAAAAfHgAAAAf//+AAAf//+AAAP//+AAAH//8AAAAPwAAAAAHgAAAAAHwAAAAAHwAAAAAHwAAAAADwAAAAADgAAAAAAAAAAAAAAAAAAAB/AAAAP3/wAAAf//wAAA///4AAA//D8AAB9+B8AAB4+A8AAB4+A+AAB4+A+AAB4+A+AAB8+A+AAB8+A+AAA/+A8AAA//A8AAAf/x8AAAP//4AAAH//wAAAAD/gAAAAB/AAAAAAAAAAAAAAAAAAD/AAAAAD/gAAAAH/gAAAAP/wAAAAPHwAAAAPDwAAAAeDwAAAAeDwAAAAeDwAAAAeHwAAAAeHgAAAAePgAAAAefAAAAAf/AAAAAf///gAAf///wAAP///wAAP///gAAH8AAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BwAAAA8B4AAAA+D4AAAA8B4AAAAcB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), 46, atob("DQoYERUWFBYVFhcVDQ=="), 42+(scale<<8)+(1<<16)); +} + +// free for commercial use +// https://www.1001fonts.com/search.html?search=Grenadier+NF +Graphics.prototype.setFontGrenadierNF = function(scale) { + // Actual height 39 (39 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAB4AAAAAAPAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAEAAAAAAPgAAAAA/8AAAAB//gAAAD//4AAAP//wAAAf//AAAB//+AAAD//8AAAB//wAAAAP/gAAAAB+AAAAAAMAAAAAAAAAAAAAAAAB4AAAAAD/8AAAAB//4AAAA///wAAAP8D/AAAD8AD8AAA/AAPwAAPgAAfAAD4AAB8AAeAAAHgAHwAAA+AA8AAADwAHgAAAeAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APgAAAfAA8AAADwAHwAAA+AAeAAAHgAD4AAB8AAPgAAfAAA+AAHwAAD8AD8AAAP8D/AAAA///wAAAD//8AAAAH/+AAAAAD8AAAAAAAAAAAAAAAAAAABAAAAAAAcAAAAAAHwAAAAAB8AAAAAAfgAAAAAH/////AB/////4AP/////AB/////4AAAAAAAAAAAAAAAAAAAAADAAAAAAA4APAAAAPAB4AAAD4APAAAA/AB4AAAP4APAAAD/AB8AAA/4AHgAAPvAA8AAD54ADwAB+PAAfAAfh4AB8AH4PAAPwD+B4AA///gPAAD//wB4AAH/4APAAAP8AAAAAAAAAAAAAAAAAAAPAAAAHAB4AAAB4APAAAAPAB4PAAB4APB4AAPAB+/gAB4AH/8AAfAAf/wADwAB/fAA+AABD8APgAAAPwH4AAAA//+AAAAD//gAAAAH/4AAAAAP8AAAAAAAAAAAAAAAAAAAAAAYAAAAAAPAAAAAAH4AAAAAD/AAAAAB/4AAAAA//AAAAAf94AAAAH+PAAAAD/B4AAAB/gPAAAA/wB4AAAf8APAAAP////4AH/////AD/////4AAAAAPAAAAAAA4AAAAAAAAAAAAAAAAAAAIAAPAAAPAAB4AAf4AAPAA//gAB4AP/8AAPAB/3gAB4APg8AAfAB4DwADwAPAfAA+AB4B8APgAPAP4H4AB4A//+AAPAB//gAB4AH/4AAAAAP8AAAAAAAAAAAAAB4AAAAAD/4AAAAA//wAAAAf//AAAAP+H8AAAH+AHwAAB/gAfAAA/4AB4AAf+AAPAAP/wAA8AH+eAAHgB/ngAA8APw8AAHgB4HgAA8AMA8AAHgAADwAA8AAAeAAPgAAD4AB4AAAPgAfAAAA+AHwAAAH4D8AAAAf//AAAAB//wAAAAD/8AAAAAH8AAAAAAAAAAAAAAAAAAAAAAAYAAAAAAfAB4AAAP4APAAAH/AB4AAH/gAPAAD/wAB4AD/wAAPAB/4AAB4A/8AAAPA/8AAAB4f+AAAAPP+AAAAB//AAAAAP/gAAAAB/gAAAAAPwAAAAAB4AAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAB//AAAAAf/+AAAAH//4AAAB+AfgAA+fAB8AAP/wAHwAD/+AAeAA//gAB4APj8AAPAB4PAAB4APB4AAPAB4PAAB4APB4AAPAB8fgAB4AH/8AAPAAf/wADwAB/eAA+AADj4APgAAAPwD8AAAA///AAAAD//wAAAAP/4AAAAAf8AAAAAAAAAAAB4AAAAAB/4AAAAA//wAAAAP//AAAAD+H8AAAA/AHwAAAHgAfAAAB8AB4AAAPAAHgAADwAA8AAAeAAHgBADwAAeA4AeAADwfADwAAeP4AeAAD3+ADwAA9/gAfAAH/wAB4AB/4AAPgAf8AAA+AH+AAAD8B/gAAAP//wAAAA//4AAAAD/8AAAAAD+AAAAAAAAAAAAAAAAAAAAAAAeB4AAAADwPAAAAAeB4AAAAAAAAAAAAAAAA=="), 46, atob("Bg4kChURExEaFBoaBg=="), 45+(scale<<8)+(1<<16)); +} + +// fonts.google.com +Graphics.prototype.setFontMonoton = function(scale) { + // Actual height 38 (37 - 0) + g.setFontCustom(atob("AAAAAAAAAAEkAAAAAbYAAAABtgAAAAG2AAAAAbYAAAABtgAAAAG2AAAAASQAAAAAAAAAAAAAAAAAAAB4AAAAB/gAAAB/gAAAB/h4AAB/h/AAB/h/CAB/h/D4B/h/D/B/j/D/APj/D/AAD/D/AAB/D/AAAPD/AAAAD/AAAAB/AAAAAPAAAAAAAAAAAAAAAAAAAAAD/wAAAB//4AAAfAD4AAHj/x4AA5//5wAHfAB5gAzj/5zAHc//5mAbvDBzcDdz/zmwNu+HzZhs3ADu2G2YAHbYbbAANthtsAA2yG2wABtsbbAAG2xtsAA2yG2wADbYbZgAdthm3ADs2DZv/92wM3P/O7AbvAD3YB3P/87gDvP/HMAHPgD7gAOP/+cAAfH+HgAAfgH4AAAf/+AAAAD+AAAAAAAAAAAAAAAAbYAAAABtgAAAAG2AAAAAbYAAAABt////wG3////AbYAAAABt////wG3////AbYAAAABt////wG3////AbYAAAABt////wEn///+AAAAAAAAAAAAAADQAAAUgdoAAF7BtsAA3sG2wAOewbbAB17BtsAc3sG2wDnewbbAdx7BtsHO3sG2w7newbbPd57Btv3OXsGzc73ewZuPc57A2f3nHsDMc5wewG8POB7Ac/zgHsA4Y8AewB+fABSAB/wAAAAAAAAAAAAAAAAA0AAAAsHbAAAbYbbAANthtsAA22G2wADbYbbJJNthts22W2G2zbZpIbbNtm2xts22bbG2zbZJIbbNttthtv2322Gzfbu7YNuO3HZg3f7P5sDuf2OMwGeHPHmAO/2f8wAccOOOAA/PfvwAA/4f8AAAAAAAAAAAAAAAAABkgAAAA/bAAAAPtsAAAD52wAAA8fbAAAfH9sAAHz42wAB8+fbAAePn9sADjx82wAJ8ePbAAfPj9sADz482wAI+fDbAAfHwNsADx8A2wAM+D/b+APgP9v4D4AA2wAMAD/b+AAAP9v4AAAA2wAAAADSAAAAAAAAAAAAAAAAAAAAMAaf/8AwBt//wJgG2AAA3Abf/8JsBt//w2wG2AABtgbf/822BtgADbYG2NvNtgbY28SSBtjbxtsG2NvG2gbY28SSBtjbzbYG2Nv9tgbY23m2BtjNg2YG2Gz+bAbYZnzcBtgzg5gG2Dn/MASQHHzgAAAPg8AAAAP/gAAAAHwAAAAAAAAAAAAAAAAA//4AAAf//8AAHgAB4AA4//44AGf//5wAzgAB7AGY//52AbP//7MDZwABmwNu//zZhs3//m2G25LTbYbbN7Nthts3sSSG2zexpIbbN7G2xts3sSaG2zezbYbbN7Nthts3v22GbDbezYNsG+HbA3Qbf7sBsB3edgGQDPHuAMAGf9wAQAOOOAAAAfvwAAAAf8AAAAAAAAAAAAAABtgAAAAG2AAAAAbYAAAABtgAAAAG2AAAAAbYAAAMBtgAAPwG2AAP8AbYAf4cBtgf4fwG0f4f4Aaf4f4cAf4f4fwH4f4f4AYf4f4cA/4/w/wHw/w/wAQ/w/wAA/w/wAAHw/wAAAQ/wAAAA/wAAAAHwAAAAAAAAAAAAAAAAAAAA+AfAAAf/P/gADwPwHgA5/OfnAHP+f/OAZwO4HYDM+d/MwNn+3/bBuwZsM2G2e2+bYbb7b9thtskk2yG2zbZtobbNtm2xts22bbG2zbZtsbbNtm2xts22bbG2zbZNsbbNttshtv2322GzfZu7YNuO3HZg3f7v5sBud3OcwHfPvHmAOf3P8wAcAeAOAAf///wAA/4f8AAAAAAAAAAAAAAAAfwAAAAH/wAAAA4DwAIAGfzgAwAz/3AJgGYDsA2AzP2YDsDZz9g2wNszbDNhs3ns22G2zezbYbbN5NthtsTkSSG2xORtMbbE5G2xts3sySG2zezbYbbN7Nths2ABm2Cbf/+3YNmf/nbAzeAB7MBuf/+dgHcP/DuAO///9wAc///OAA8AADwAA///8AAA///AAAAAAAAAAAAAAAAAAAAAAA2xtgAADbG2AAANsbYAAA2xtgAADbG2AAANsbYAAAkhJAAAAAAAAAAAAAAAA"), 46, atob("ChIiERcYGRwfGSAfCw=="), 40+(scale<<8)+(1<<16)); +} + +/* + * If only 1 widget is loaded at the top, then Bangle.appRect changes + * to report as if widgets were loaded at the bottom as well. The + * other option would be for Bangle.appRect to adjust for different + * combinations EG: no widgets, wigets on top, widgets on bottom and + * widgets on top and bottom areas, but it does not at present. + * + * Example of Bangle.appRect with 3 widges on the top, note h = 152, not 176 + * ={ x: 0, y: 24, w: 176, h: 152, x2: 175, y2: 175 } + * + * With the example below we are going assume that the bottom widget + * space is not used. + * + */ +const CenterX = g.getWidth()/2; +const CenterY = (g.getHeight()/2) + (Bangle.appRect.y/2); +const outerRadius = (g.getHeight() - Bangle.appRect.y)/2; + +if (settings.fullscreen) { + Bangle.loadWidgets(); + /* + * We load the widgets as some like widpedom accumualte the step count. + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * widgets area to the top bar doesn't get cleared. + */ + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +function debug(o) { + //console.log(o); +} + +debug("limelight.app.js"); +debug("CenterX=" + CenterX); +debug("CenterY=" + CenterY); +debug("outerRadius=" + outerRadius); +debug("y12=" + (CenterY - outerRadius)); +debug("y6=" + (CenterY + outerRadius)); + +let HourHandLength = outerRadius * 0.5; +let HourHandWidth = 2*5, halfHourHandWidth = HourHandWidth/2; + +let MinuteHandLength = outerRadius * 0.7; +let MinuteHandWidth = 2*3, halfMinuteHandWidth = MinuteHandWidth/2; + +let SecondHandLength = outerRadius * 0.9; +let SecondHandOffset = halfHourHandWidth + 10; + +let outerBoltRadius = halfHourHandWidth + 2, innerBoltRadius = outerBoltRadius - 4; +let HandOffset = outerBoltRadius + 4; + +let twoPi = 2*Math.PI, deg2rad = Math.PI/180; +let Pi = Math.PI; +let halfPi = Math.PI/2; + +let sin = Math.sin, cos = Math.cos; + +let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1]; + +let HandPolygon = [ + -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3], + sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0], + sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3], + -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0], +]; + +let HourHandPolygon = new Array(HandPolygon.length); +for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + HourHandPolygon[i] = halfHourHandWidth*HandPolygon[i]; + HourHandPolygon[i+1] = halfHourHandWidth*HandPolygon[i+1]; + if (i < l/2) { HourHandPolygon[i+1] -= HourHandLength; } + if (i > l/2) { HourHandPolygon[i+1] += HandOffset; } +} +let MinuteHandPolygon = new Array(HandPolygon.length); +for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + MinuteHandPolygon[i] = halfMinuteHandWidth*HandPolygon[i]; + MinuteHandPolygon[i+1] = halfMinuteHandWidth*HandPolygon[i+1]; + if (i < l/2) { MinuteHandPolygon[i+1] -= MinuteHandLength; } + if (i > l/2) { MinuteHandPolygon[i+1] += HandOffset; } +} + +/**** transforme polygon ****/ + +let transformedPolygon = new Array(HandPolygon.length); + +function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } +} + +/**** draw clock hands ****/ + +function drawClockHands () { + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + let Seconds = now.getSeconds(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + g.setColor(g.theme.fg); + + transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + if (settings.secondhand) { + g.setColor(g.theme.fg2); + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + } + + g.setColor(g.theme.fg); + g.fillCircle(CenterX,CenterY, outerBoltRadius); + + g.setColor(g.theme.bg); + g.drawCircle(CenterX,CenterY, outerBoltRadius); + g.fillCircle(CenterX,CenterY, innerBoltRadius); +} + +function setNumbersFont() { + if (settings.vector) { + g.setFont('Vector', settings.vector_size); + return; + } + + if (settings.font == "GochiHand") + g.setFontGochiHand(); + else if (settings.font == "Grenadier") + g.setFontGrenadierNF(); + else if (settings.font == "Monoton") + g.setFontMonoton(); + else + g.setFontLimelight(); +} + +function drawNumbers() { + g.setColor(g.theme.fg); + setNumbersFont(); + + g.setFontAlign(0,-1); + g.drawString('12', CenterX, CenterY - outerRadius); + + g.setFontAlign(1,0); + g.drawString('3', CenterX + outerRadius, CenterY); + + g.setFontAlign(0,1); + g.drawString('6', CenterX, CenterY + outerRadius); + + g.setFontAlign(-1,0); + g.drawString('9', CenterX - outerRadius,CenterY); +} + +function draw() { + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + drawClockHands(); + drawNumbers(); + queueDraw(); +} + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, UPDATE_PERIOD - (Date.now() % UPDATE_PERIOD)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI('clock'); +draw(); diff --git a/apps/limelight/limelight.icon.js b/apps/limelight/limelight.icon.js new file mode 100644 index 000000000..f7e74db90 --- /dev/null +++ b/apps/limelight/limelight.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lksgIqngf/wAFC//+AgUch/4AgMBwAQEh/8Dgf/4AKOEAQKCAYUB//gAoU/DQkPBQYVBGx5SDBQIbDBR0GEAlgFYcHGwh4B+CDHRwL04")) \ No newline at end of file diff --git a/apps/limelight/limelight.png b/apps/limelight/limelight.png new file mode 100644 index 000000000..b1744b28e Binary files /dev/null and b/apps/limelight/limelight.png differ diff --git a/apps/limelight/limelight.settings.js b/apps/limelight/limelight.settings.js new file mode 100644 index 000000000..aacea2f86 --- /dev/null +++ b/apps/limelight/limelight.settings.js @@ -0,0 +1,78 @@ +(function(back) { + const SETTINGS_FILE = "limelight.json"; + + // initialize with default settings... + let s = { + 'vector_size': 42, + 'vector': false, + 'font': "Limelight", + 'secondhand': false, + 'fullscreen': false + } + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = storage.readJSON(SETTINGS_FILE, 1) || {} + const saved = settings || {} + + // copy settings into variable + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var font_options = ["Limelight","GochiHand","Grenadier","Monoton"]; + + E.showMenu({ + '': { 'title': 'Limelight Clock' }, + '< Back': back, + 'Full Screen': { + value: s.fullscreen, + format: () => (s.fullscreen ? 'Yes' : 'No'), + onchange: () => { + s.fullscreen = !s.fullscreen; + save(); + }, + }, + 'Font': { + value: 0 | font_options.indexOf(s.font), + min: 0, max: 3, + format: v => font_options[v], + onchange: v => { + s.font = font_options[v]; + save(); + }, + }, + 'Vector Font': { + value: s.vector, + format: () => (s.vector ? 'Yes' : 'No'), + onchange: () => { + s.vector = !s.vector; + save(); + }, + }, + 'Vector Size': { + value: s.vector_size, + min: 24, + max: 56, + step: 6, + onchange: v => { + s.vector_size = v; + save(); + } + }, + 'Second Hand': { + value: s.secondhand, + format: () => (s.secondhand ? 'Yes' : 'No'), + onchange: () => { + s.secondhand = !s.secondhand; + save(); + }, + } + }); +}) diff --git a/apps/limelight/screenshot_gochihand.png b/apps/limelight/screenshot_gochihand.png new file mode 100644 index 000000000..244b008dc Binary files /dev/null and b/apps/limelight/screenshot_gochihand.png differ diff --git a/apps/limelight/screenshot_grenadier.png b/apps/limelight/screenshot_grenadier.png new file mode 100644 index 000000000..a55896297 Binary files /dev/null and b/apps/limelight/screenshot_grenadier.png differ diff --git a/apps/limelight/screenshot_limelight.png b/apps/limelight/screenshot_limelight.png new file mode 100644 index 000000000..7b12e4cc2 Binary files /dev/null and b/apps/limelight/screenshot_limelight.png differ diff --git a/apps/limelight/screenshot_monoton.png b/apps/limelight/screenshot_monoton.png new file mode 100644 index 000000000..e75b11f5d Binary files /dev/null and b/apps/limelight/screenshot_monoton.png differ diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 448f8119a..39b825e02 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -14,3 +14,5 @@ 0.12: Fixed nl_NL formatting, because the full months won't fit on the Bangle.js2's screen 0.13: Now use shorter de_DE date format to more closely match other languages for size 0.14: Added some first translations for Messages in nl_NL +0.15: Fixed sv_SE formatting, long date does not work well for Bangle.js2 + Added Swedish localisation with English text \ No newline at end of file diff --git a/apps/locale/locales.js b/apps/locale/locales.js index cf511c54f..428e0c773 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -276,13 +276,31 @@ var locales = { temperature: "°C", ampm: { 0: "fm", 1: "em" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, - datePattern: { 0: "%A %B %d %Y", "1": "%Y-%m-%d" }, // söndag 1 mars 2020 // 2020-03-01 + datePattern: { 0: "%b %d %Y", "1": "%Y-%m-%d" }, // feb 1 2020 // 2020-03-01 abmonth: "jan,feb,mars,apr,maj,juni,juli,aug,sep,okt,nov,dec", month: "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december", abday: "sön,mån,tis,ons,tors,fre,lör", day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag", trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "on", off: "off" } }, + "en_SE": { // Swedish localisation with English text + lang: "en_SE", + decimal_point: ",", + thousands_sep: ".", + currency_symbol: "kr", + int_curr_symbol: "SKR", + speed: 'kmh', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%B %d %Y", "1": "%Y-%m-%d" }, // March 1 2020 // 2020-03-01 + abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + month: "January,February,March,April,May,June,July,August,September,October,November,December", + abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", + day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", + // No translation for english... + }, "en_NZ": { lang: "en_NZ", decimal_point: ".", diff --git a/apps/mclockplus/ChangeLog b/apps/mclockplus/ChangeLog index a1cecc698..097545ba8 100644 --- a/apps/mclockplus/ChangeLog +++ b/apps/mclockplus/ChangeLog @@ -1,2 +1,3 @@ 0.01: Created app 0.02: Use Bangle.setUI for button/launcher handling +0.03: Allow widgets to detect this is a clock diff --git a/apps/mclockplus/mclockplus.app.js b/apps/mclockplus/mclockplus.app.js index 4c74ce1be..970397478 100644 --- a/apps/mclockplus/mclockplus.app.js +++ b/apps/mclockplus/mclockplus.app.js @@ -304,15 +304,14 @@ Bangle.on('lcdPower',function(on) { }); g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); // Update time once a second timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); - // Start stopwatch when BTN3 is pressed setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"}); B3 = 1; // BTN3 is bound to start the stopwatch diff --git a/apps/menuwheel/ChangeLog b/apps/menuwheel/ChangeLog index defdb5049..050cf2049 100644 --- a/apps/menuwheel/ChangeLog +++ b/apps/menuwheel/ChangeLog @@ -1 +1,2 @@ 0.01: New menu! +0.02: Clean up touch handler in setUI diff --git a/apps/menuwheel/boot.js b/apps/menuwheel/boot.js index 3e708e9a8..deb15264d 100644 --- a/apps/menuwheel/boot.js +++ b/apps/menuwheel/boot.js @@ -1,8 +1,5 @@ E.showMenu = function(items) { g.clearRect(Bangle.appRect); // clear screen if no menu supplied - // clean up back button listener - if (Bangle.backHandler) Bangle.removeListener('touch', Bangle.backHandler) - delete Bangle.backHandler; if (!items) { Bangle.setUI(); return; @@ -206,8 +203,13 @@ E.showMenu = function(items) { if (b===1) back(); } } - // note: backHandler is cleaned up at the top of this file Bangle.on('touch', Bangle.backHandler); } return l; }; +// setUI now also needs to clear up our back button touch handler +Bangle.setUI = (old => function() { + if (Bangle.backHandler) Bangle.removeListener("touch", Bangle.backHandler); + delete Bangle.backHandler; + return old.apply(this, arguments); +})(Bangle.setUI); \ No newline at end of file diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index fe46ba97a..522534af0 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -23,3 +23,8 @@ 0.14: Hide widget when all unread notifications are dismissed from phone 0.15: Don't buzz when Quiet Mode is active 0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147) +0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font +0.18: Use app-specific icon colors + Spread message action buttons out + Back button now goes back to list of messages + If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267) diff --git a/apps/messages/app.js b/apps/messages/app.js index e36bb699e..80e4a3244 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -83,7 +83,7 @@ function getMessageImage(msg) { if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); if (s=="facebook") return getFBIcon(); if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); - if (s=="instagram") return atob("GBiBAf////////////////wAP/n/n/P/z/f/b/eB7/c87/d+7/d+7/d+7/d+7/c87/eB7/f/7/P/z/n/n/wAP////////////////w=="); + if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); if (s=="gmail") return getNotificationImage(); if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); if (s=="mail") return getNotificationImage(); @@ -101,6 +101,31 @@ function getMessageImage(msg) { if (msg.id=="back") return getBackImage(); return getNotificationImage(); } +function getMessageImageCol(msg,def) { + return { + // generic colors, using B2-safe colors + "calendar": "#f00", + "mail": "#ff0", + "music": "#f0f", + "phone": "#0f0", + "sms message": "#0ff", + // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) + // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) + "facebook": "#4267b2", + "gmail": "#ea4335", + "google home": "#fbbc05", + "hangouts": "#1ba261", + "instagram": "#dd2a7b", + "messenger": "#0078ff", + "outlook mail": "#0072c6", + "skype": "#00aff0", + "slack": "#e51670", + "telegram": "#0088cc", + "twitter": "#1da1f2", + "whatsapp": "#4fce5d", + "wordfeud": "#dcc8bd", + }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); +} function showMapMessage(msg) { var m; @@ -200,7 +225,7 @@ function showMessageSettings(msg) { function showMessage(msgid) { var msg = MESSAGES.find(m=>m.id==msgid); - if (!msg) return checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found + if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found if (msg.src=="Maps") { cancelReloadTimeout(); // don't auto-reload to clock now return showMapMessage(msg); @@ -224,10 +249,11 @@ function showMessage(msgid) { {type:"btn", src:getBackImage(), cb:()=>{ msg.new = false; saveMessages(); // read mail cancelReloadTimeout(); // don't auto-reload to clock now - checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); }} // back ]; if (msg.positive) { + buttons.push({fillx:1}); buttons.push({type:"btn", src:getPosImage(), cb:()=>{ msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now @@ -236,6 +262,7 @@ function showMessage(msgid) { }}); } if (msg.negative) { + buttons.push({fillx:1}); buttons.push({type:"btn", src:getNegImage(), cb:()=>{ msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now @@ -248,7 +275,7 @@ function showMessage(msgid) { var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n"); layout = new Layout({ type:"v", c: [ {type:"h", fillx:1, bgCol:colBg, c: [ - { type:"btn", src:getMessageImage(msg), pad: 3, cb:()=>{ + { type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{ cancelReloadTimeout(); // don't auto-reload to clock now showMessageSettings(msg); }}, @@ -310,7 +337,9 @@ function checkMessages(options) { body = msg.track; } if (img) { - g.drawImage(img, x+24, r.y+24, {rotate:0}); // force centering + var fg = g.getColor(); + g.setColor(getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering + .setColor(fg); // only color the icon x += 50; } var m = msg.title+"\n"+msg.body; diff --git a/apps/messages/widget.js b/apps/messages/widget.js index e831e5b68..1239ef262 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,8 +1,9 @@ -WIDGETS["messages"]={area:"tl",width:0,draw:function() { +WIDGETS["messages"]={area:"tl", width:0, iconwidth:23, +draw:function() { Bangle.removeListener('touch', this.touch); if (!this.width) return; var c = (Date.now()-this.t)/1000; - g.reset().clearRect(this.x,this.y,this.x+this.width,this.y+23); + g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+this.iconwidth); g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y); //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute let settings = require('Storage').readJSON("messages.settings.json", true) || {}; @@ -17,7 +18,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { WIDGETS["messages"].t=Date.now(); // first time WIDGETS["messages"].l=Date.now()-10000; // last buzz if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing - WIDGETS["messages"].width=64; + WIDGETS["messages"].width=this.iconwidth; Bangle.drawWidgets(); Bangle.setLCDPower(1);// turns screen on },hide:function() { @@ -37,7 +38,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { b(); },touch:function(b,c) { var w=WIDGETS["messages"]; - if (!w||!w.width||c.xw.x+w.width||c.yw.y+23) return; + if (!w||!w.width||c.xw.x+w.width||c.yw.y+w.iconwidth) return; load("messages.app.js"); }}; /* We might have returned here if we were in the Messages app for a @@ -46,4 +47,4 @@ want to buzz but should still show that there are unread messages. */ if (global.MESSAGES===undefined) (function() { var messages = require("Storage").readJSON("messages.json",1)||[]; if (messages.some(m=>m.new)) WIDGETS["messages"].show(true); -})(); +})(); \ No newline at end of file diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog new file mode 100644 index 000000000..939ac3b5d --- /dev/null +++ b/apps/mmind/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/mmind/README.md b/apps/mmind/README.md new file mode 100644 index 000000000..8060b95f6 --- /dev/null +++ b/apps/mmind/README.md @@ -0,0 +1,31 @@ +# Mastermind + +Play the classic mind game mastermind on your Bangle 2. + +![](screenshot_mmind.png) + + +## Game +The game will start when run. +Four colors pins are randomly chosen and kept secret. +You need to find the secret by scoring your choice within 6 turns. +The game makes use of touch features. + + +## Play +Select one of the dots, the color menu will show, select a colour for the pin. +If all pins are chosen with a color the red button will turn green. +Hit the green button and your play will be scored and listed from the top. +The first digit shows the number of pins with the correct color and in the right place. +The second digit gives the number of pins with the correct color but in the wrong place. +There are six turns to get the correct secret. +The blue button will start a new game. + + +## Requests +This is the first version, things to add are: +Add a menu to change game options like the number of colors, allow double colors, 5 pins per row. Add feature to drag screen up and down to see more scores. Timer and high score. +Any other fearures or remarks, let me know @psbest. + +## Creator +This game is created by Peter Slendebroek. diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js new file mode 100644 index 000000000..e7def025d --- /dev/null +++ b/apps/mmind/mmind.app.js @@ -0,0 +1,198 @@ +//MMind + +//set vars +const H = g.getWidth(); +const W = g.getHeight(); +var touch_actions = []; +var cols = ["#FF0000","#00FF00","#0000FF", "#FF00FF", "#FFFF00", "#00FFFF", "#000000","#FFFFFF"]; +var turn = 0; +var col_menu = false; +//pinsRow = 6; +//pinsThick = 10; +//pinsRow = 5; +//pinsThick = 10; +var pinsRow = 4; +var pinsThick = 10; +var play = [-1, -1, -1, -1]; + +var pinsCol = 5; +var playx = -1; +var sx = (W - 30 )/pinsRow; +var sy = (H - 20 )/7; +var touch_actions = []; +var secret = []; +var secret_no_dub = true; +var endgame = false; + +g.clear(); +g.setColor("#FFFFFF"); +g.fillRect(0, 0, H, W); +g.setFont("Vector12",45); + +function draw() { + touch_actions = []; + g.clear(); + g.setColor("#FFFFFF"); + g.fillRect(0, 0, H, W); + g.setColor("#000000"); + //draw scores + for (y=0;y= 0) s = Math.round(Math.random()*pinsCol); + secret[i]= s; + } + } + +function score() { + bScore = 0; + wScore = 0; + for (i=0; i touch_actions[i][0][0] && e.x < touch_actions[i][0][2] && + e.y > touch_actions[i][0][1] && e.y < touch_actions[i][0][3]) { + // a action is hit, add acctions here, todo: start, stop, new, etc. + switch (touch_actions[i][1][0]) { + case 1: + //get pins col menu + col_menu = 1; + playx = touch_actions[i][1][1]; + break; + case 2: + //copy choice col to play + play[playx] = touch_actions[i][1][1]; + col_menu = 0; + break; + case 3: + //score play + var sc; + sc = score(); + game.push([play, sc]); + play = [-1,-1,-1,-1]; + turn+=1; + if (turn==6 || sc[0]==pinsRow) { + play = secret; + col_menu = 0; + endgame = true; + } + break; + case 4: + //new game + play = [-1,-1,-1,-1]; + game = []; + endgame=false; + break; + } + } + } + //console.log(touch_actions[i][1][0], touch_actions[i][1][1]); + + draw(); + } +); + + +game = []; +get_secret(); +draw(); +//Bangle.loadWidgets(); +//Bangle.drawWidgets(); + + + + + diff --git a/apps/mmind/mmind.icon.js b/apps/mmind/mmind.icon.js new file mode 100644 index 000000000..17c28ba0f --- /dev/null +++ b/apps/mmind/mmind.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+64A/AEOBq2sBAusqwJHCaQFDAYlP2m0yGBCIkSj0eiWHBIkDgsFgYTE01v3O5t4mC1krgAEBq0ACYQuCAANsHIcxFwIwCEocsFwIwCBIYuCAANQF4QwBOgQABAgNIF4ZgELwQvCHIcCF4cEKwYvEt45DF4QwCL5YvFL5ITDF6OstheCvTjEjAuBjDJFX4UEq4TEyguBygTEF4dWBIeskkkqwQDDgUGgwaEBIUBgITHkslCYeBd4MrqwDBAgIuBcwRVGNIVs0oJEv3S6V+CYmIisjkcVZAYpBgDyBAAJFBFwTlGZIolDqouBGAQJDFwQABmRfCFAICCGwXXhgvDMAheCfI1UF4eoKwYvEiovHSoJfLF4pfJCYYvN1gwBAYMSLwVcbQmQFwOQZIq/C1GACYkcFwMcCYQoCLYNWF4KPBDgNWmIkEBIVPp5TDBIdWqoTHmUyCYlWRQTwCD4wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHmy2QJH6PRBI/Q6AkOCAIAFBINDjwABGInR3+53O/GIu72gABGJnQCAQAE69oFwQABCYfFFwIwCBIfCDIe7FIus1gvXLwQACLw4aCAAkAgAvcL4gvLq1WF5uyFwdoCYfLF4fLDpHCX6owBtFoxoUF6PF4ruFDwPC4XJFxbSCAAwVNAH4ARA")) diff --git a/apps/mmind/mmind.info b/apps/mmind/mmind.info new file mode 100644 index 000000000..2e79822b1 --- /dev/null +++ b/apps/mmind/mmind.info @@ -0,0 +1,17 @@ + { + "id": "mmind", + "name": "Classic Mind Game", + "shortName":"Master Mind", + "icon": "mmind.png", + "version":"0.01", + "description": "This is the classic game for masterminds", + "type": "game", + "tags": "mastermind, game, classic", + "readme":"README.md", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"mmind.app.js","url":"mmind.app.js"}, + {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} + ] + } diff --git a/apps/mmind/mmind.png b/apps/mmind/mmind.png new file mode 100644 index 000000000..14a3ef7c6 Binary files /dev/null and b/apps/mmind/mmind.png differ diff --git a/apps/mmind/screenshot_mmind.png b/apps/mmind/screenshot_mmind.png new file mode 100644 index 000000000..5c886e7e8 Binary files /dev/null and b/apps/mmind/screenshot_mmind.png differ diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog index 7b83706bf..653f859ae 100644 --- a/apps/mylocation/ChangeLog +++ b/apps/mylocation/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Enhanced icon, make it bolder diff --git a/apps/mylocation/mylocation.icon.js b/apps/mylocation/mylocation.icon.js index bfb38d5ac..b79f5875f 100644 --- a/apps/mylocation/mylocation.icon.js +++ b/apps/mylocation/mylocation.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4UA///t/7j/P3/vB4cBqtVoAbHBQIABBQ0FBYdQBYsVBYdUERIkGHIQADHoguEGAwuEGAwKFBZg8DHQw8EBYNf/1Vq3/8oLDIwNf/Wpv//0oLG9Wq3/qBYJUCBYuqBaBqBBYW+BepHEBbybCBYP+BYSnErYLDyoLFAANq/r8Ga5T7MBZZUBAAhSCfhA6DBZhIGBQg8FHQg8GHQgwGFwowFBQwwDFwwLMlS7Bqta1AKEn2q1K1C1WgBYf/1WqBYIDB1QKCgYLC0taBYoXB/QICBY0//7vBAAQ8EEgIABCwwME9QVEA")) +require("heatshrink").decompress(atob("mEw4UA///gH4AYPO/QPDgNVqtADY/1BYNfBQ0PBQIAB+ALFmoLDrgLF6oLDq4KEgYKDBYPABYcNBYlVuAuIGAwuEAANUBYYKFHgg6Bq4ZCr4DBHgQLBvWq2te1WlBYZGBBYOr1Wq1qSDBYNqBIILDKgQLLgoLHqBqDBfJHLBZBrOgKPCBYiPCU4NaBYe1WYrABBQLCCfgYGCrwVBa4kAirvKNgIAErgLDKgIAEKQQ8EAAY6DBZhIDIww8GHQg8GHQgwGFwowEFwx5EOog8GHQ0AlWpBYNq1AKFWIILBAYOgBYbICytWAgQKCgTgDcwYXGAAgvGAAY8EEgYWGBgoVEA==")) diff --git a/apps/mylocation/mylocation.png b/apps/mylocation/mylocation.png index 7148990a4..038ee177e 100644 Binary files a/apps/mylocation/mylocation.png and b/apps/mylocation/mylocation.png differ diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 56dea1188..80ab29c56 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -2,6 +2,7 @@ + + + +
+

Presentor

+
+
+
+
+
+
#
+
Subject
+
Time
+
Notes
+
+
Loading...
+
+
+
+
+ + +
+ + + + + + diff --git a/apps/presentor/settings.json b/apps/presentor/settings.json new file mode 100644 index 000000000..398bf1332 --- /dev/null +++ b/apps/presentor/settings.json @@ -0,0 +1 @@ +{"pparts":[{"subject":"#1","minutes":10,"seconds":0,"notes":"This is a note."},{"subject":"#2","minutes":2,"seconds":50,"notes":"Change in the app!"}],"sversion":2.2} \ No newline at end of file diff --git a/apps/promenu/ChangeLog b/apps/promenu/ChangeLog index 5560f00bc..b7287cc80 100644 --- a/apps/promenu/ChangeLog +++ b/apps/promenu/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Add Bangle.js 2 Support diff --git a/apps/promenu/boot.js b/apps/promenu/boot.js index 002734113..bd813a812 100644 --- a/apps/promenu/boot.js +++ b/apps/promenu/boot.js @@ -70,7 +70,7 @@ E.showMenu = function(items) { if(g.theme.dark){ fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+20); }else{ - fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-10); + fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); } g.setColor(hl ? g.theme.fgH : g.theme.fg); g.setFontAlign(-1,-1); diff --git a/apps/promenu/bootb2.js b/apps/promenu/bootb2.js new file mode 100644 index 000000000..c84e0d894 --- /dev/null +++ b/apps/promenu/bootb2.js @@ -0,0 +1,142 @@ +E.showMenu = function(items) { + function RectRnd(x1,y1,x2,y2,r) { + pp = []; + pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r])); + pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2])); + pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r])); + pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1])); + return pp; + } + function fillRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.fillPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + function drawRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.drawPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + g.reset().clearRect(Bangle.appRect); // clear if no menu supplied + Bangle.setLCDPower(1); // ensure screen is on + if (!items) { + Bangle.setUI(); + return; + } + var menuItems = Object.keys(items); + var options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + options.fontHeight = options.fontHeight||25; + if (options.selected === undefined) + options.selected = 0; + var ar = Bangle.appRect; + var x = ar.x; + var x2 = ar.x2; + var y = ar.y; + var y2 = ar.y2 - 12; // padding at end for arrow + if (options.title) + y += 22; + var loc = require("locale"); + var l = { + lastIdx : 0, + draw : function(rowmin,rowmax) { + var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length); + var idx = E.clip(options.selected-( rows>>1),0,menuItems.length-rows); + if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled + l.lastIdx = idx; + var iy = y; + g.reset().setFontAlign(0,-1,0).setFont('12x20'); + if (options.predraw) options.predraw(g); + if (rowmin===undefined && options.title) + g.drawString(options.title,(x+x2)/2,y-21).drawLine(x,y-2,x2,y-2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 4; + if (rowmin!==undefined) { + if (idxrowmax) { + rows = 1+rowmax-rowmin; + } + } + while (rows--) { + var name = menuItems[idx]; + var item = items[name]; + var hl = (idx==options.selected && !l.selectEdit); + if(g.theme.dark){ + fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+40); + }else{ + fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); + } + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign(-1,-1); + var v = item.value; + v = loc.translate(""+v); + if(loc.translate(name).length >= 17-v.length && "object" == typeof item){ + if (item.format) v=item.format(v); + g.drawString(loc.translate(name).substring(0, 12-v.length)+"...",x+3.7,iy+2.7); + }else{ + if(loc.translate(name).length >= 15){ + g.drawString(loc.translate(name).substring(0, 15)+"...",x+3.7,iy+2.7); + }else{ + g.drawString(loc.translate(name),x+3.7,iy+2.7); + } + } + if ("object" == typeof item) { + var xo = x2; + var v = item.value; + if (item.format) v=item.format(v); + v = loc.translate(""+v); + if (l.selectEdit && idx==options.selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2}); + } + g.setFontAlign(1,-1); + g.drawString(v,xo-2,iy+1); + } + g.setColor(g.theme.fg); + iy += options.fontHeight; + idx++; + } + g.setFontAlign(-1,-1); + g.setColor((idxitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) item.onchange(item.value); + l.draw(options.selected,options.selected); + } else { + var lastSelected=options.selected; + options.selected = (dir+options.selected+menuItems.length)%menuItems.length; + l.draw(Math.min(lastSelected,options.selected), Math.max(lastSelected,options.selected)); + } + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + return l; +}; diff --git a/apps/ptlaunch/README.md b/apps/ptlaunch/README.md index cf75315a9..12c205980 100644 --- a/apps/ptlaunch/README.md +++ b/apps/ptlaunch/README.md @@ -2,11 +2,19 @@ Directly launch apps from the clock screen with custom patterns. -## Usage +## Installation and Usage + +Install Pattern Launcher alongside your main laucher app. +_Do not delete that launcher!_ +Pattern Launcher is designed as an additional app launching utility, not as a replacement for the main launcher. + +In the main launcher, start Pattern Launcher in the app menu to assign the pattern configuration (see below). +Note that this actually among the applications, _not_ in the application settings! Create patterns and link them to apps in the Pattern Launcher app. Then launch the linked apps directly from the clock screen by simply drawing the desired pattern. +Note that this does only work in the clock screen, not if other applications run. ## Add Pattern Screenshots @@ -28,7 +36,8 @@ Then launch the linked apps directly from the clock screen by simply drawing the ## Detailed Steps -From the main menu you can: +The main menu of Pattern Launcher is accessible from the _application_ starter of the main launcher. +From there you can: - Add a new pattern and link it to an app (first entry) - To create a new pattern first select "Add Pattern" @@ -60,6 +69,16 @@ Make sure the watch is unlocked before you start drawing. If this bothers you, y Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry! +4. Where can I configure the patterns? + +You have to start the "Pattern Launcher" app from the main app launcher's app selection. + +5. Do I have to delete my former app launcher so that Pattern Launcher is the only installed launcher? + +No! Pattern Launcher works alongside another "main" launcher. +If you have deleted that one, you do not have a general purpose app launcher any more and cannot access Pattern Launcher's configuration. +If you already have deleted your main launcher accidentially, just reinstall it from the app loader. + ## Authors Initial creation: [crazysaem](https://github.com/crazysaem) @@ -67,3 +86,5 @@ Initial creation: [crazysaem](https://github.com/crazysaem) Improve pattern detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/) Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/) + +Doc additions: [dirkhillbrecht](http://forum.espruino.com/profiles/182498/) diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog new file mode 100644 index 000000000..0950b7ae0 --- /dev/null +++ b/apps/puzzle15/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version, UI mechanics ready, no real game play so far +0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... +0.03: Menu logic now generally functioning, splash screen added. The first really playable version! +0.04: Settings dialog, about screen +0.05: Central game end function diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md new file mode 100644 index 000000000..16c0c4593 --- /dev/null +++ b/apps/puzzle15/README.md @@ -0,0 +1,57 @@ +# Puzzle15 - A 15-puzzle for the Bangle.js 2 + +This is a Bangle.js 2 adoption of the famous 15 puzzle. + +## The game + +A board of _n_ by _n_ fields is filled with _n^2-1_ numbered stones. So, one field, the "gap", is left free. + +Bring them in the correct order so that the gap is finally at the bottom right of the playing field. +The less moves you need, the better you are. + +If _n_ is 4, the number of stones is _16-1=15_. Hence the name of the game. + +## How to play + +If you start the game, it shows a splash screen and then generates a shuffled 4x4 board with a 15 puzzle. +Move the stones with drag gestures on the screen. +If you want to move the stone below the gap upward, drag from the bottom of the screen upward. +The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved. + +If you managed to order the stones correctly, a success message appears. +You can continue with another game, go to the game's main menu, or quit the game entirely. + +There is a grey menu button right of the board containing the well-known three-bar menu symbol ("Hamburger menu"). +It opens the game's main menu directly from within the game. + +## The main menu + +Puzzle15 has a main menu which can be reached from the in-game menu button or the end-of-game message window. +It features the following options: + +* **Continue** - Continue the currently running game. _This option is only shown if the main menu is opened during an open game._ +* **Start 3x3**, **Start 4x4**, **Start 5x5** - Start a new game on a board with the respective dimension. Any currently open game is dropped. +* **About** Show a small "About" info box. +* **Exit** Exit Puzzle15 and return to the default watch face. + +## Game settings + +The game has some global settings which can be accessed on the usual way through the Bangle.js' app settings user interface. +Currently it has the following options: + +* **Splash** - Define whether the game should open with a splash screen. **long** shows the splash screen for five seconds, **short** shows it for two seconds. **off** starts the app _without_ a splash screen, it directly comes up with whatever the "Start with" option says. +* **Start with** - What should happen after the splash screen (or, if it is disabled, directly at app start): **3x3**, **4x4** and **5x5** start the game with a board of the respective dimension, **menu** shows the main menu which allows to select the board size. + +## Implementation notes + +The game engine always generates puzzles which can be solved. + +Solvability is detected by counting inversions, +i.e. pairs of stones where the stone at the earlier field (row-wise, left to right, top to bottom) has a number _greater than_ the stone on the later field, with all pairs of stones compared. +The algorithm is described at https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ . + +## The splash screen + +The Splash screen shows a part of the illustration "The 14-15-puzzle in puzzleland" from Sam Loyd. Other than Puzzle15, it depicts a 15 puzzle with the stones "14" and "15" swapped. This puzzle is indeed *not* solvable. + +Have fun! diff --git a/apps/puzzle15/introscreen.png b/apps/puzzle15/introscreen.png new file mode 100644 index 000000000..766cd61fa Binary files /dev/null and b/apps/puzzle15/introscreen.png differ diff --git a/apps/puzzle15/puzzle15.app-icon.js b/apps/puzzle15/puzzle15.app-icon.js new file mode 100644 index 000000000..04fb4a665 --- /dev/null +++ b/apps/puzzle15/puzzle15.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AC3+7oAD7e7AAW8BQndBQe79/9DomgHocH74KD/RJE34Xax4XDtvoC4fJ54XDluAC4f2z4XDzm/C4ett4XD34OBF4e/I4m+C4f8r4XChHuC5U98oXEF4cP7/AC5O9mYXC/2/F4cGtwvE/SsBC4Ws7gvD7YCBL4ULO4i/u1QAD7QED1e6AoetCAnf/YeE1wpD/lgBQcKIAgXG14LD/twC5kL3Z+BC4P+LgIXBg272wXD7wXEh7eCC4PWzIXChHtOoIXB/WX54XDh3KmAXC1oLBI4UD+AXC+/rdIIvD5wvD3O4C4cJ4AXC/dUI4kJhgMBC4Ov+AXDh9QC4X2/gvEhvvoAXC81dC4duR4f8wSncC6v8u4AD3ndAAXcy4KDtYKD7vf/oGE2wRDvPNBQfLFAnP/o2EVIIACg7yBAATZBAAe/C7P9g4XCx+wn/6C4Op//AC4MK+cI/+QC4X2/fPC4PM2HKh8H7vpewIXBhvThV5+AXC+/5C4UL2HHC4Pf/P/AIJHB6cAj2wC4X+3AXPhADBF4fX94XB1va1vOC4PXAIX6hfrxvb0CPD7p3C1e6hW2C4LOBAIIXB3eJ3YXEX78GM4IAC9QXG1QAD7QEDJYIFD14oE//7DwgME/twBQcPC70G6EG5dQ1/8VYPtC4ObgfM5IXHr/whvO4Gvy6LBtX9vfugnr3AXHkXggGOC4P97/43X9ukOgnv6BfIC4Oe2AXC6+nI4MOgfI9QXJhssF4f91AXCgnA9IXHr3u1HusGv3Ob//s/t693l3xHJX9v+3YAD7oAE5YKD34XFAC4=")) diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js new file mode 100644 index 000000000..eec21933c --- /dev/null +++ b/apps/puzzle15/puzzle15.app.js @@ -0,0 +1,771 @@ +// A 15-puzzle game for the Bangle.js 2 clock +// (C) Dirk Hillbrecht 2022 +// Released unter the terms of the MIT license + +// The intro screen as large base-64-encoded binary data +const introscreen = E.toArrayBuffer(atob("sLABwAAAAA5QAAACAAAAHAAMEDgA/F/nvoAAAAA+3AAAAAAAAB4AQBIkAPwv//4AAAAAP/wQAAAAAAAAAA3wBAD8P//+AAAAAD7tGQAAAAAAAIADAAMF2C///wAAACA4oRmAHx/wQAAAAAABOfgP//8AAAAAAGEYgA+/AMAAQAAAAAP8B///AAAGAABxMKgD/AAABGAAAAAD/Af//wAAYQAr6TOoAfwAIAWTgAAAA/4D//8SAeCA9W0/mAH8ABAEBAAAAAP+A///YAMRh/5of/ADwMAYOAAAAAAD/gH/f4AMYd/3dv+AAwUABSAAAAAAA/8A/XIAMDrPn///gAMQAADAABgAAAH3APBAAMAdX3/fvjACEAQAAA/mAAAB/wcAAAcAz1/+3/34AgAAgADuF+AAAP8YAA8MEJ+Bt29/GAIAACAADAgwAAD/4AACMAyv/+f/6HwGAABAAE2IoAAAfgAAB8ADf/////mOADh5gABNCCAAAHwB/x4AAX//5/L9D4AAAAYBCQjgAA3QAgBgAAE//7P//jzAAADPAGkAoADggABwAwAC3//F9/5n4AAB/wFpACAHAADAAAEAA///c/v/4jABwf+A8Xwh+AD0BAAAAIfvPm/G/+IYA8P3wPh+P4AHAAAB+ACH//+Hy/9iCAfD/8CYX/QAL9AAAAcAme//z/PrR6gB4P/guPgAAODAGBwAAI8+7gf0P6/oAeD/4bjh//wxA4AAAw2f//8f+f+PiAHwf/+53AA/+AAAAA4Gv/P/9/A/nwgB8D6AuWD8EAAAYAD4A+7/8X/ADr8IAfAwgZlO/AAAHwAH4L2/////mDz9CAD6MB65YAAAAf+A/4D23/n/v4j4fcgA/wDguWAAAB8AD7ngg1////+EAD/Pg8AGDZlmgAP8e4zCX4aP///9AZA/D/4AAAGYf//////4ZwG2j/8H/gwLPggAAAAfWH/////8AQRCpr/+E/9IBT4IAA/hj1h///wFgIqBBO9//33/m6i8CAB8A//YH/gDMfGO/WH/0/5//4gEfAgP4Af+AB/gAHFEBoaTH+/+w/8fQDgI+aP//iifYABRCJ9a9w///+P+H/M4Gw8//34o3yBKkMUW0e3Pt//i6AHxeDg///gouP/poNPAnjzp/3v+2PgDfngz/v3rvjhO6bTTqDWsY//v/nz4LD/4N6bff3YYHu7UU6yZcm////5+3Kgf/CtMP/b+GJ7hr9O41tNB///+XNhAb/hv3kv/7hg7MQn3o+4N5Y9//gx8ME+59P47//gYK6Pnbvc5bu6///48MFl+d93/yv++GAVi+X51IyKqA//+eC4AzP0P3y4//hgb4/83QXYfjAP//1MYbwn9D/77/14YCnf/gIHLX96D//5WGA8Z7Bnf7b/8GgKG94GXuU9y4P/+DwwCeeafvfm93hubX8dvF3E6Ar/N/54OATj2Du/Pv/qY10/zTBB1h+CAff/8AwH4dmb6t7/2mN+IuRyI4CBngD///AMx6Xsnve+09pkNvgsQwAAWAHwo/+hBAeh5Czt5/HcZxSQ6AceCAAiAK/9ggQHBNBuZ5eQvGOducgFgA1BxADx5MIEX/Jo2p8YR/xjt4wINYAfYAf/K4TDAV8Ye48DLBDXY46ADQmBIe+HoSOMgwj5hFqH+CAB78O6AAX4gACAAgMPjYEK4f//7p/2f/NjOuAH8MAACACb5YmCCsDSPnbADz7w49+lI8BAAAf8aICRgAjACB/O4Ag1VcMuD/eAQD/oHISSsYEMzhg/qtABft//7LACAG/gwAMg2MGBCJ4cPD25gBN95Ovn5UA4DgHRncFAxQijj//5KA/jvcL6gAJBEAAf7fBB4MUI+fv/szeEAaqjO/gObYAAAAz57GDpAO5x19BIA/9uYw7gEyMQHwAD/mAg8KBjwv9Giv/vrvKQHBNAEsAUAP9kuP+gZAff+dcEX/ZljjebYDAAAAnf8Pj/YKsH/2E1HEAUQGTB21gwAAAGS5NYv0C6jP1mJY5MEqV0f9fcAfPoHM7qWL/EDg39PGcex7iu+4Bz7B/AD/63qRg/kwML/+DJR9PvOUTAK64RwYAcjek5f/4Yv/aT20L7v0m6gCNvNiYgDI15iE//jH/+2zij+9HsMY8qrA98f+CL9SiP/8//8/gkJ/Ht99M/OogD+MAOifQJj//nH//Y5KPw7//FLBZACAH+Arn8GV//////zMVT36V5sdAecAgCkHqP5H3///4v/+cbUdvIc7wADDAId4AegXuX///////iaVfkvNu/+wYA5Aj/AJl6j////v3/uyOW91wcvqImAZIcQeCBeg/////f/v8MqV/7gZ2cDwICMYAcgXpf////v+/JmJv5/7+B9A+HAiKYA5Bcb//////nxMOI8f92d+APDgZF34SbeU7///72/naafn22WAD/DwwCigR+kHlO///9/9fmWFt/+GMiT8uMARV/F5B/XP///5BTmsGTV/3EADfxlAMsfqjZ/XT///X/l/9FJs//CADDuQgEUnw08vnW/3/38tvx7G3v/BHeOfm4DNT4zpJ7uj///5NI//Cef/gvBlf5uBig8Og/aN////YVe9iWuvnwAD7g/JoxYeF0k/tf///s0U7c8ozbz4BwEHztJoPyrIdrZ//////qz5DpZvcDgBw/7MVHxc+mc+/8P/jL//+PV358GAACPtyKD4SYt/PZ+B////8/+dk/gDgB+G5tNA8fvir/HfNfAG4f/+9q8AAAAABgayg+E0Ot3/flDwADAP/4W/AHAAAAUfJoHizE7/xzww5AMPAAAXoQGADAAH9M/wZF2Sb/b4B3KAQPiLSOMAAAOAA4if/wmz3m+/eAOBwAA/aljmEAAAwAALNz/7Z8v/7/BIgbLwA3jBjNAIAHwAGnD39kTKe//3gBxeAAH0oreAP/+Pok7YH9HgCv7/+gABPAAADcxoADff4h3olwHy/Cvet+YAeAACgO/b+AR//g7geWjwP+/Kf/+IDgAeQXeEt3A+53AH4dKMDH//wnu/BIwDAYIbi/VQa/2bol+knwPg99r7/+MIAAAgDQ33L2P+/rLeyTfiaA/Cf/9kgAADYAIN+7f/ef+dj5pzj54Bynf8kiAABM8Dh+/r//+/anc0/A+7wH/9+On8AAHhgsf/1+CAD31mbA4bcDgI//DyQAAwAxrP/f4AgAKfzNlMNlkGib7gABdAYYBrD/3wLwAD//m3DGSPwYn/wA8BZgBg5S//i5AAAD/xR5x5uOHpP4AAQewAAPVn/nwAAAAP4o89mvDjSXsYAfH5AIB9///4Pwfx7+0debX4R09+pgeQHA8Bn/78f/nwAH/5DfLvzgfp3gAAkI0AJxL//+fjAAAP8k/ly4YOgF4no4Z3AAYH/xAL/gAAB7/DyJcGPz9cSA2XgaAPB+2AH/4AAAZ/+BonFj8B3AYAfAtiLg//A5QAQAAOQ/5WZg5qSVwYAAMAxK4P//jcAZyAGbD/sIQctMlYsAD9wclSf//gEAAB/B84H9UEPdm5cgAAAwOKMP//8eAGs/9i/gfaBnmziXYIABwXsRN7f//Cb+D/7fmB1wfzoqvktAAATGQD/3/vwdgAG918YP3h5+Q/0fmAAJMRZb///ZEQAAcT5xV/+A7MCdgIMAABBqt///+WEAADp+MNo/4XkCj4AMAAbILUf///vEhAgkvDH6D/vzA/mAgQIAPBrDpf/7D7+eSThxt4P/pgbnhkYCAQYDs//v/3/7P5Nw8+3g/+gPvgDhDABhgZP3//v3/j828efJsB/YDP8BjhAPM8Hf/9f/9/7eaDPPkwYH/g9BxIAQBAhA9Pa3/v/+3Fi/yz8Fgv+ASUAQYAJuOKH/9///+7yrj7p/MH//4/5CM8AwEw4S+7df/+uxx59ejvh2D//0YA+sAADhDq+d9v+b++M+zR3IfYf9ibgASEcAgCX1br+8i+fz+Nk7memA/J5IADiYYC2XwbbGK0/f/OHydxpz4DxiJgAAICAAY8e/3vd/mP8C5G4y93gP8hOAHhOeAA86rOvEfzQ/hMjudE4GAwlLwAgUcI4wtr77vf9uH+rR3OZI5YCMRvgAQAABDhR73nv8jwf7o5nnmfBx/yV+AIAAP+IY3/lX/ZvB90sxwzTkHiMZn4AAGQAxhXtX33E+8HoPI+ZhzwSAgXfwARwQPlaSv4dyfhg9w2cMw4+PjCYr+AEewAkhdlfvRP5qH/B+/YeODnIiHe4OAYAHCeld7/m88f/4ONkOHh2hjM//gAAAA4AK/n8TOLFp/gIzHjwsyYBXf8AAAACBA153p3ByYH8GZRw4ZMYwpM3wAwAAwA7E3k7j5Ng/zEg4cO0xOKyz3AAgABALQf6M44++D/sYcOD7iMRZHP8AAAAAGOFpkcObPwf7kOHhO3SkGQYbwANgBCDfUyOjun/B+yPjwzPzHJYlf2AOMAAQEjLHd1T+cHxDw4dj+YvjKX/wOBgBwAr/4udN/Zg/4McJxv9xMMLD/GAcACk+o/hOur3Cv/ifD4ZfsMizgO/BHgAEQfC8/bZz8zruHh+KR//FIYA/n2fAAif4ZjrmcnLR/w4jEMH8kQkAAwAJmJEB/j8GSOcFKH/AZjCAz6pCAAAAAHwIho8bjZnNjegf4GxgwD/GUgAGAAB+BIqDh/k3gZKeD/jYRMAB4ZwABwAAIg55/8P3ZwMrM4P/KISAAHimAAOAACIHD//59s8nJDzB/zFMAAAMLwAAi4B7Aen/8P3+7lJpcH9gHAAABzgAACNs+gBs/fx5Pd3ExxgfxngAAAOcCAAMBf4AEB/+P42diY8MD8w4AAAB/wfAOAB4AD4H3w/DcFMOQwP/DAAAAH+B+4AAIAAD8v/X4femHBzl/4AAAAAHwAQAACAAAJ39x/Ct5Dh8Lf/gAAAAAP4AAEh4AADe+/D8HUgxvGf/8AAAAAAOwABf4AAAMT/8fxOQd3i9//8AAAAAAzAAAYAAAD5d/P+5IMxxH//8wAAAAAHYBgCCMAAOPP4fxkHw4Vv/3DAAAAAA5AGAhRwAA/D/D/bD4cMx//wOAAAAADAAf8BAAABwH4f9xzvE/f/4A4AAAAAYAB/gpgAAaf/v+QcPim3//AB4AAAABwAP8AAAABg/8fYeDxmT//wABwAAAAHwD6AADgAHP/h/Dg+1Zf/8=")); + +// *** Global constants from which several other settings are derived + +// Minimum number of pixels to interpret it as drag gesture +const dragThreshold = 10; + +// Maximum number of pixels to interpret a click from a drag event series +const clickThreshold = 3; + +// Number of steps in stone move animation +const animationSteps = 6; + +// Milliseconds to wait between move animation steps +const animationWaitMillis = 30; + +// Total width of the playing field (full screen width) +const fieldw = g.getWidth(); + +// Total height of the playing field (screen height minus widget zones) +const fieldh = g.getHeight() - 48; + + +// *** Global game characteristics + +// Size of the playing field +var stonesPerLine; + +// Size of one field +var stonesize; + +// Actual left start of the playing field (so that it is centered) +var leftstart; + +// Actual top start of the playing field (so that it is centered) +var topstart; + +// Number of stones on the board (needed at several occasions) +var stonesPerBoard; + +// Set the stones per line globally and all derived values, too +function setStonesPreLine(bPL) { + stonesPerLine = bPL; + stonesize = Math.floor(Math.min(fieldw / (stonesPerLine + 1), fieldh / stonesPerLine)) - 2; + leftstart = (fieldw - ((stonesPerLine + 1) * stonesize + 8)) / 2; + topstart = 24 + ((fieldh - (stonesPerLine * stonesize + 6)) / 2); + stonesPerBoard = (stonesPerLine * stonesPerLine); +} + + +// *** Global app settings + +var SETTINGSFILE = "puzzle15.json"; + +// variables defined from settings +var splashMode; +var startWith; + +/* For development purposes +require('Storage').writeJSON(SETTINGSFILE, { + splashMode: "off", + startWith: "5x5", +}); +/* */ + +/* OR (also for development purposes) +require('Storage').erase(SETTINGSFILE); +/* */ + +// Helper method for loading the settings +function def(value, def) { + return (value !== undefined ? value : def); +} + +// Load settings +function loadSettings() { + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + splashMode = def(settings.splashMode, "long"); + startWith = def(settings.startWith, "4x4"); +} + + +// *** Low level helper classes + +// One node of a first-in-first-out storage +class FifoNode { + constructor(payload) { + this.payload = payload; + this.next = null; + } +} + +// Simple first-in-first-out (fifo) storage +// Needed to keep the stone movements in order +class Fifo { + + // Initialize an empty Fifo + constructor() { + this.first = null; + this.last = null; + } + + // Add an element to the end of the internal fifo queue + add(payload) { + if (this.last === null) { // queue is empty + this.first = new FifoNode(payload); + this.last = this.first; + } else { + let newlast = new FifoNode(payload); + this.last.next = newlast; + this.last = newlast; + } + } + + // Returns the first element in the queue, null if it is empty + remove() { + if (this.first === null) + return null; + let oldfirst = this.first; + this.first = this.first.next; + if (this.first === null) + this.last = null; + return oldfirst.payload; + } + + // Returns if the fifo is empty, i.e. it does not hold any elements + isEmpty() { + return (this.first === null); + } + +} + +// Helper class to keep track of tasks +// Executes tasks given by addTask. +// Tasks must call Worker.endTask() when they are finished, for this they get the worker passed as parameter. +// If a task is given with addTask() while another task is still running, +// it is queued and executed once the currently running task and all +// previously scheduled tasks have finished. +// Tasks must be functions with the Worker as first and only parameter. +class Worker { + + // Create an empty worker + constructor() { + this.tasks = new Fifo(); + this.busy = false; + } + + // Add a task to the worker + addTask(task) { + if (this.busy) // other task is running: Queue this task + this.tasks.add(task); + else { // No other task is running: Execute directly + this.busy = true; + task(this); + } + } + + // Called by the task once it finished + endTask() { + if (this.tasks.isEmpty()) // No more tasks queued: Become idle + this.busy = false; + else // Call the next task immediately + this.tasks.remove()(this); + } + +} + +// Evaluate "drag" events from the UI and call handlers for drags or clicks +// The UI sends a drag as a series of events indicating partial movements +// of the finger. +// This class combines such parts to a long drag from start to end +// If the drag is short, it is interpreted as click, +// otherwise as drag. +// The approprate method is called with the data of the drag. +class Dragger { + + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { + this.clickHandler = clickHandler; + this.dragHandler = dragHandler; + this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold); + this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold); + this.dx = 0; + this.dy = 0; + this.enabled = true; + } + + // Enable or disable the Dragger + setEnabled(b) { + this.enabled = b; + } + + // Handle a raw drag event from the UI + handleRawDrag(e) { + if (!this.enabled) + return; + this.dx += e.dx; // Always accumulate + this.dy += e.dy; + if (e.b === 0) { // Drag event ended: Evaluate full drag + if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold) + this.clickHandler({ + x: e.x - this.dx, + y: e.y - this.dy + }); // take x and y from the drag start + else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold) + this.dragHandler({ + x: e.x - this.dx, + y: e.y - this.dy, + dx: this.dx, + dy: this.dy + }); + this.dx = 0; // Clear the drag accumulator + this.dy = 0; + } + } + + // Attach the drag evaluator to the UI + attach() { + Bangle.on("drag", e => this.handleRawDrag(e)); + } +} + +// *** Mid-level game mechanics + +// Representation of a position where a stone is set. +// Stones can be moved from field to field. +// The playing field consists of a fixed set of fields forming a square. +// During an animation, a series of interim field instances is generated +// which represents the locations of a stone during the animation. +class Field { + + // Generate a field with a left and a top coordinate. + // Note that these coordinates are "cooked", i.e. they contain all offsets + // needed place the elements globally correct on the screen + constructor(left, top) { + this.left = left; + this.top = top; + this.centerx = (left + stonesize / 2) + 1; + this.centery = (top + stonesize / 2) + 2; + } + + // Returns whether this field contains the given coordinate + contains(x, y) { + return (this.left < x && this.left + stonesize > x && + this.top < y && this.top + stonesize > y); + } + + // Generate a field for the given playing field index. + // Playing field indexes start at top left with "0" + // and go from left to right line by line from top to bottom. + static forIndex(index) { + return new Field(leftstart + (index % stonesPerLine) * (stonesize + 2), + topstart + (Math.floor(index / stonesPerLine)) * (stonesize + 2)); + + } + // Special field for the result "stone" + static forResult() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart + ((stonesPerLine - 1) * (stonesize + 2))); + } + + // Special field for the menu + static forMenu() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart); + } + +} + +// Representation of a moveable stone of the game. +// Stones are moved from field to field to solve the puzzle +// Stones are numbered from 0 to the maximum number ot stones. +// Stone "0" represents the gap on the playing field. +// The main knowledge of a Stone instance is how to draw itself. +class Stone { + + // Create stone with the given number + // The constructor creates the "draw()" function which is used to draw the stone + constructor(number, targetindex) { + this.number = number; + this.targetindex = targetindex; + // gap: Does not draw anything + if (number === 0) + this.draw = function(field) {}; + else if ((number + (stonesPerLine % 2 == 0 ? (Math.floor((number - 1) / stonesPerLine)) : 0)) % 2 == 0) { + // Black stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(1, 1, 1).drawString(number, field.centerx, field.centery); + }; + } else { + // White stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.drawString(number, field.centerx, field.centery); + }; + } + } + + // Returns whether this stone is on its target index + isOnTarget(index) { + return index === this.targetindex; + } +} + +// Helper class which knows how to clear the rectangle opened up by the two given fields +class Clearer { + + // Create a clearer for the area between the two given fields + constructor(startfield, endfield) { + this.minleft = Math.min(startfield.left, endfield.left); + this.mintop = Math.min(startfield.top, endfield.top); + this.maxleft = Math.max(startfield.left, endfield.left); + this.maxtop = Math.max(startfield.top, endfield.top); + } + + // Clear the area defined by this clearer + clearArea() { + g.setColor(1, 1, 1); + g.fillRect(this.minleft, this.mintop, + this.maxleft + stonesize, this.maxtop + stonesize); + } + +} + +// Helper class which moves a stone between two fields +class Mover extends Clearer { + + // Create a mover which moves the given stone from startfield to endfield + // and animate the move in the given number of steps + constructor(stone, startfield, endfield, steps) { + super(startfield, endfield); + this.stone = stone; + this.startfield = startfield; + this.endfield = endfield; + this.steps = steps; + } + + // Create the coordinate between start and end for the given step + // Computation uses sinus for a smooth movement + stepCoo(start, end, step) { + return start + ((end - start) * ((1 + Math.sin((step / this.steps) * Math.PI - (Math.PI / 2))) / 2)); + } + + // Compute the interim field for the stone to place during the animation + stepField(step) { + return new Field( + (this.minleft === this.maxleft ? this.minleft : + this.stepCoo(this.startfield.left, this.endfield.left, step)), + (this.mintop === this.maxtop ? this.mintop : + this.stepCoo(this.startfield.top, this.endfield.top, step))); + } + + // Perform one animation step + animateStep(step, worker) { + this.clearArea(); + this.stone.draw(this.stepField(step)); + if (step < this.steps) // still steps left: Issue next step + setTimeout(function(t) { + t.animateStep(step + 1, worker); + }, animationWaitMillis, this); + else // all steps done: Inform the worker + worker.endTask(); + } + + // Start the animation, this method is called by the worker + animate(worker) { + this.animateStep(1, worker); + } + +} + +// Representation of the playing field +// Knows to draw the field and to move a stone into a gap +class Board { + + // Generates the actual playing field with all fields and stones + constructor() { + this.fields = []; + this.resultField = Field.forResult(); + this.menuField = Field.forMenu(); + for (i = 0; i < stonesPerBoard; i++) + this.fields[i] = Field.forIndex(i); + this.setShuffled(); + //this.setAlmostSolved(); // to test the game end + } + + /* Set the board into the "solved" position. Useful for showcasing and development + setSolved() { + this.stones = []; + for (i = 0; i < stonesPerBoard; i++) + this.stones[i] = new Stone((i + 1) % stonesPerBoard, i); + this.moveCount = 0; + } + /* */ + + /* Initialize an almost solved playing field. Useful for tests and development + setAlmostSolved() { + this.setSolved(); + b = this.stones[this.stones.length - 1]; + this.stones[this.stones.length - 1] = this.stones[this.stones.length - 2]; + this.stones[this.stones.length - 2] = b; + } + /* */ + + // Initialize a shuffled field. The fields are always solvable. + setShuffled() { + let nrs = []; // numbers of the stones + for (i = 0; i < stonesPerBoard; i++) + nrs[i] = i; + this.stones = []; + let count = stonesPerBoard; + for (i = 0; i < stonesPerBoard; i++) { + // Take a random number of the (remaining) numbers + let curridx = Math.floor(Math.random() * count); + let currnr = nrs[curridx]; + // Initialize the next stone with that random number + this.stones[i] = new Stone(currnr, (currnr + (stonesPerBoard - 1)) % stonesPerBoard); + // Remove the number just taken from the list of numbers + for (j = curridx + 1; j < count; j++) + nrs[j - 1] = nrs[j]; + count -= 1; + } + // not solvable: Swap the first and second stone which are not the gap. + // This will always result in a solvable board. + if (!this.isSolvable()) { + let a = (this.stones[0].number === 0 ? 2 : 0); + let b = (this.stones[1].number === 0 ? 2 : 1); + let bx = this.stones[a]; + this.stones[a] = this.stones[b]; + this.stones[b] = bx; + } + this.moveCount = 0; + } + + // Draws the complete playing field + draw() { + new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); + for (i = 0; i < this.fields.length; i++) + this.stones[i].draw(this.fields[i]); + this.drawResult(null); + this.drawMenu(); + } + + // returns the index of the field left of the field with the given index, + // -1 if there is none (index indicates already a leftmost field on the board) + leftOf(index) { + return (index % stonesPerLine === 0 ? -1 : index - 1); + } + + // returns the index of the field right of the field with the given index, + // -1 if there is none (index indicates already a rightmost field on the board) + rightOf(index) { + return (index % stonesPerLine === (stonesPerLine - 1) ? -1 : index + 1); + } + + // returns the index of the field top of the field with the given index, + // -1 if there is none (index indicates already a topmost field on the board) + topOf(index) { + return (index >= stonesPerLine ? index - stonesPerLine : -1); + } + + // returns the index of the field bottom of the field with the given index, + // -1 if there is none (index indicates already a bottommost field on the board) + bottomOf(index) { + return (index < (stonesPerLine - 1) * stonesPerLine ? index + stonesPerLine : -1); + } + + // Return the index of the gap in the field, -1 if there is none (should never happel) + indexOf0() { + for (i = 0; i < this.stones.length; i++) + if (this.stones[i].number === 0) + return i; + return -1; + } + + // Returns the row in which the gap is, 0 is upmost + rowOf0() { + let idx = this.indexOf0(); + if (idx < 0) + return -1; + return Math.floor(idx / stonesPerLine); + } + + // Searches the gap on the field and then moves one of the adjacent stones into it. + // The stone is selected by the given startfunc which returns the index + // of the selected adjacent field. + // Startfunc is one of (left|right|top|bottom)Of. + moveTo0(startfunc, worker) { + let endidx = this.indexOf0(); // Target field (the gap) + if (endidx === -1) { + worker.endTask(); + return; + } + let startidx = startfunc(endidx); // Start field (relative to the gap) + if (startidx === -1) { + worker.endTask(); + return; + } + // Replace in the internal representation + let moved = this.stones[startidx]; + this.stones[startidx] = this.stones[endidx]; + this.stones[endidx] = moved; + this.moveCount += 1; + // Move on screen using an animation effect. + new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(worker); + } + + // Move the stone right from the gap into the gap + moveRight(worker) { + this.moveTo0(this.leftOf, worker); + } + + // Move the stone left from the gap into the gap + moveLeft(worker) { + this.moveTo0(this.rightOf, worker); + } + + // Move the stone above the gap into the gap + moveUp(worker) { + this.moveTo0(this.bottomOf, worker); + } + + // Move the stone below the gap into the gap + moveDown(worker) { + this.moveTo0(this.topOf, worker); + } + + // Check if the board is solved (all stones at the right position) + isSolved() { + for (i = 0; i < this.stones.length; i++) + if (!this.stones[i].isOnTarget(i)) + return false; + return true; + } + + // counts the inversions on the board + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + getInversionCount() { + let inversions = 0; + for (outer = 0; outer < stonesPerBoard - 1; outer++) { + let outernr = this.stones[outer].number; + if (outernr === 0) + continue; + for (inner = outer + 1; inner < stonesPerBoard; inner++) { + let innernr = this.stones[inner].number; + if (innernr > 0 && outernr > innernr) + inversions++; + } + } + return inversions; + } + + // return whether the puzzle is solvable + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + isSolvable() { + let invs = this.getInversionCount(); + if (stonesPerLine % 2 !== 0) // odd number of rows/columns + return (invs % 2 === 0); + else { + return ((invs + this.rowOf0()) % 2 !== 0); + } + } + + // draw the result field, pass null as argument if not called from worker + drawResult(worker) { + let field = this.resultField; + let solved = this.isSolved(); + if (solved) + g.setColor(0, 1, 0); + else + g.setColor(1, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery); + if (worker !== null) + worker.endTask(); + if (solved) + setTimeout(() => { + gameEnd(this.moveCount); + }, 500); + } + + // draws the menu button + drawMenu() { + let field = this.menuField; + g.setColor(0.5, 0.5, 0.5); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + let l = field.left + 8; + let r = field.left + stonesize - 8; + let t = field.top + 5; + for (i = 0; i < 3; i++) + g.fillRect(l, t + (i * 6), r, t + (i * 6) + 2); + } + +} + + +// *** Global helper methods + +// draw some text with some surrounding to increase contrast +// text is drawn at given (x,y) position with textcol. +// frame is drawn 2 pixels around (x,y) in each direction in framecol. +function framedText(text, x, y, textcol, framecol) { + g.setColor(framecol); + for (i = -2; i < 3; i++) + for (j = -2; j < 3; j++) { + if (i === 0 && j === 0) + continue; + g.drawString(text, x + i, y + j); + } + g.setColor(textcol).drawString(text, x, y); +} + +// Show the splash screen at program start, call afterSplash afterwards. +// If spash mode is "off", call afterSplash directly. +function showSplash(afterSplash) { + if (splashMode === "off") + afterSplash(); + else { + g.reset(); + g.drawImage(introscreen, 0, 0); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(afterSplash, (splashMode === "long" ? 2000 : 1000)); + }, (splashMode === "long" ? 1000 : 1)); + }, (splashMode === "long" ? 2000 : 1000)); + } +} + + +// *** Global flow control + +// Initialize the game with an explicit number of stones per line +function initGame(bpl) { + setStonesPreLine(bpl); + newGame(); +} + +// Start a new game with the same number of stones per line as before +function newGame() { + board = new Board(); + continueGame(); +} + +// Continue the currently running game +function continueGame() { + E.showMenu(); + board.draw(); + dragger.setEnabled(true); +} + +// Show message on game end, allows to restart new game +function gameEnd(moveCount) { + dragger.setEnabled(false); + E.showPrompt("You solved the\n" + stonesPerLine + "x" + stonesPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { + title: "Puzzle solved", + buttons: { + "Again": newGame, + "Menu": () => showMenu(false), + "Exit": exitGame + } + }).then(v => { + E.showPrompt(); + setTimeout(v, 10); + }); +} + +// A tiny about screen +function showAbout(doContinue) { + E.showAlert("Author: Dirk Hillbrecht\nLicense: MIT", "Puzzle15").then(() => { + if (doContinue) + continueGame(); + else + showMenu(false); + }); +} + +// Show the in-game menu allowing to start a new game +function showMenu(withContinue) { + var mainmenu = { + "": { + "title": "15 Puzzle" + } + }; + if (withContinue) + mainmenu.Continue = continueGame; + mainmenu["Start 3x3"] = () => initGame(3); + mainmenu["Start 4x4"] = () => initGame(4); + mainmenu["Start 5x5"] = () => initGame(5); + mainmenu.About = () => showAbout(withContinue); + mainmenu.Exit = exitGame; + dragger.setEnabled(false); + g.clear(true); + E.showMenu(mainmenu); +} + +// Handle a "click" event (only needed for menu button) +function handleclick(e) { + if (board.menuField.contains(e.x, e.y)) + setTimeout(() => showMenu(true), 10); +} + +// Handle a drag event (moving the stones around) +function handledrag(e) { + worker.addTask(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => board.moveRight(e) : e => board.moveLeft(e)) : + (e.dy > 0 ? e => board.moveDown(e) : e => board.moveUp(e))); + worker.addTask(e => board.drawResult(e)); +} + +// exit the game, clear screen first to prevent ghost images +function exitGame() { + g.clear(true); + setTimeout(load, 300); +} + + +// *** Main program + +g.clear(true); + +// Load global app settings +loadSettings(); + +// We need a worker... +var worker = new Worker(); + +// Board will be initialized after the splash screen has been shown +var board; + +// Dragger is needed for interaction during the game +var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +// Disable dragger as board is not yet initialized +dragger.setEnabled(false); + +// Nevertheless attach it so that it is ready once the game starts +dragger.attach(); + +// Start the game by handling the splash screen sequence +showSplash(() => { + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); + if (startWith === "3x3") + initGame(3); + else if (startWith === "4x4") + initGame(4); + else if (startWith === "5x5") + initGame(5); + else + showMenu(false); +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/puzzle15.app.png b/apps/puzzle15/puzzle15.app.png new file mode 100644 index 000000000..f95366a46 Binary files /dev/null and b/apps/puzzle15/puzzle15.app.png differ diff --git a/apps/puzzle15/puzzle15.settings.js b/apps/puzzle15/puzzle15.settings.js new file mode 100644 index 000000000..352ec4315 --- /dev/null +++ b/apps/puzzle15/puzzle15.settings.js @@ -0,0 +1,50 @@ +// Settings menu for the Puzzle15 app + +(function(back) { + var FILE = "puzzle15.json"; + // Load settings + var settings = Object.assign({ + splashMode: "long", + startWith: "4x4" + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "15 Puzzle" + }, + "< Back": () => back(), + "Splash": stringInSettings("splashMode", ["long", "short", "off"]), + "Start with": stringInSettings("startWith", ["3x3", "4x4", "5x5", "menu"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/screenshot.png b/apps/puzzle15/screenshot.png new file mode 100644 index 000000000..3a79a3ae6 Binary files /dev/null and b/apps/puzzle15/screenshot.png differ diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog index fb6c751bb..b9be6039d 100644 --- a/apps/qalarm/ChangeLog +++ b/apps/qalarm/ChangeLog @@ -3,3 +3,4 @@ 0.03: Fix unfreed memory, and clearInterval that disabled all clocks at midnight Fix app icon Change menu order so 'back' is at the top +0.04: Fix alarm not activating sometimes. diff --git a/apps/qalarm/qalarm.js b/apps/qalarm/qalarm.js index 6b31ba645..8e82be186 100644 --- a/apps/qalarm/qalarm.js +++ b/apps/qalarm/qalarm.js @@ -143,7 +143,7 @@ let alarms = require("Storage").readJSON("qalarm.json", 1) || []; let active = alarms.filter( (alarm) => alarm.on && - alarm.t < t && + alarm.t <= t && alarm.last != time.getDate() && (alarm.timer || alarm.daysOfWeek[time.getDay()]) ); diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog index 35832a300..c868b6668 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -3,4 +3,6 @@ 0.03: Bangle.js 2 support 0.04: Move Quiet Mode LCD options from global settings to this app 0.05: Avoid immediately redrawing widgets on load -0.06: Fix: don't try to redraw widget when widgets not loaded \ No newline at end of file +0.06: Fix: don't try to redraw widget when widgets not loaded +0.07: Option to switch theme + Changed time selection to 5-minute intervals \ No newline at end of file diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md index 535ae56e4..660bda787 100644 --- a/apps/qmsched/README.md +++ b/apps/qmsched/README.md @@ -9,6 +9,11 @@ Automatically turn Quiet Mode on or off at set times, and display a widget when | ![Edit Schedule menu](screenshot_b1_edit.png) | ![Edit Schedule menu](screenshot_b2_edit.png) | | ![LCD Options menu](screenshot_b1_lcd.png) | ![LCD Options menu](screenshot_b2_lcd.png) | +### Switch Theme: + +Switch to dark theme during Quiet Mode. + * **NOTE**: This switches between the default "Dark BW" and "Light BW" themes, so custom theme settings will be lost. + ### LCD Settings: If set, these override the default LCD settings while Quiet Mode is active. \ No newline at end of file diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js index 7be3339fb..e05eff6a2 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -3,7 +3,7 @@ Bangle.drawWidgets(); const modeNames = ["Off", "Alarms", "Silent"]; -// load global brightness setting +// load global settings let bSettings = require('Storage').readJSON('setting.json',true)||{}; let current = 0|bSettings.quiet; delete bSettings; // we don't need any other global settings @@ -18,6 +18,7 @@ delete bSettings; // we don't need any other global settings */ function save() { require('Storage').writeJSON('qmsched.json', settings); + eval(require('Storage').read('qmsched.boot.js')); // apply new schedules right away } function get(key, def) { return (key in settings) ? settings[key] : def; @@ -77,37 +78,66 @@ function formatTime(t) { const mins = Math.round((t-hrs)*60); return (" "+hrs).substr(-2)+":"+("0"+mins).substr(-2); } +/** + * Apply theme + */ +function applyTheme() { + const theme = (require("Storage").readJSON("setting.json", 1) || {}).theme; + if (theme && theme.dark===g.theme.dark) return; // already correct + g.theme = theme; + delete g.reset; + g._reset = g.reset; + g.reset = function(n) { return g._reset().setColor(g.theme.fg).setBgColor(g.theme.bg); }; + g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); }; + g.clear(1); + Bangle.drawWidgets(); + delete m.lastIdx; // force redraw + m.draw(); +} +/** + * Library uses this to make the app update itself + * @param {int} mode New Quite Mode + */ +function setAppQuietMode(mode) { + if (mode === current) return; + current = mode; + delete m.lastIdx; // force redraw + applyTheme(); + if (m.lastIdx===undefined) m.draw(); // applyTheme didn't redraw menu, but we need to show updated mode +} + +let m; function showMainMenu() { - let _m, menu = { + let menu = { "": {"title": "Quiet Mode"}, "< Exit": () => load() }; // "Current Mode""Silent" won't fit on Bangle.js 2 menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = { value: current, - format: v => modeNames[v], - onchange: function(v) { - if (v<0) {v = 2;} - if (v>2) {v = 0;} - require("qmsched").setMode(v); - current = v; - this.value = v; - }, + min:0, max:2, wrap: true, + format: () => modeNames[current], + onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current` }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { menu[formatTime(sched.hr)] = { format: () => modeNames[sched.mode], // abuse format to right-align text - onchange: function() { - _m.draw = ()=> {}; // prevent redraw of main menu over edit menu + onchange: () => { + m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange) showEditMenu(idx); } }; }); menu["Add Schedule"] = () => showEditMenu(-1); + menu["Switch Theme"] = { + value: !!get("switchTheme"), + format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", + onchange: v => v ? set("switchTheme", v) : unset("switchTheme"), + }; menu["LCD Settings"] = () => showOptionsMenu(); - _m = E.showMenu(menu); + m = E.showMenu(menu); } function showEditMenu(index) { @@ -125,31 +155,19 @@ function showEditMenu(index) { "< Cancel": () => showMainMenu(), "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' + min:0, max:23, wrap:true, + onchange: v => {hrs = v;}, }, "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' + min:0, max:55, step:5, wrap:true, + onchange: v => {mins = v;}, }, "Switch to": { value: mode, + min:0, max:2, wrap:true, 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' + onchange: v => {mode = v;}, }, }; function getSched() { @@ -174,7 +192,7 @@ function showEditMenu(index) { showMainMenu(); }; } - return E.showMenu(menu); + m = E.showMenu(menu); } function showOptionsMenu() { @@ -244,7 +262,7 @@ function showOptionsMenu() { onchange: () => {toggle("wakeOnTwist");}, }, }; - return E.showMenu(oMenu); + m = E.showMenu(oMenu); } loadSettings(); diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js index c3bc49b58..c4610ce3e 100644 --- a/apps/qmsched/boot.js +++ b/apps/qmsched/boot.js @@ -1,5 +1,7 @@ // apply Quiet Mode schedules (function qm() { + if (Bangle.qmTimeout) clearTimeout(Bangle.qmTimeout); // so the app can eval() this file to apply changes right away + delete Bangle.qmTimeout; let bSettings = require('Storage').readJSON('setting.json',true)||{}; const curr = 0|bSettings.quiet; delete bSettings; @@ -18,7 +20,7 @@ let t = 3600000*(next.hr-hr); // timeout in milliseconds if (t<0) {t += 86400000;} // scheduled for tomorrow: add a day /* update quiet mode at the correct time. */ - setTimeout(() => { + Bangle.qmTimeout=setTimeout(() => { require("qmsched").setMode(mode); qm(); // schedule next update }, t); diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js index e9ed3ec90..9696657cc 100644 --- a/apps/qmsched/lib.js +++ b/apps/qmsched/lib.js @@ -1,5 +1,31 @@ /** - * Apply LCD options for given mode + * Apply appropriate theme for given mode + * @param {int} mode Quiet Mode + */ +function switchTheme(mode) { + if (!!mode === g.theme.dark) return; // nothing to do + let s = require("Storage").readJSON("setting.json", 1) || {}; + // default themes, copied from settings.js:showThemeMenu() + function cl(x) { return g.setColor(x).getColor(); } + s.theme = mode ? { + // 'Dark BW' + fg: cl("#fff"), bg: cl("#000"), + fg2: cl("#0ff"), bg2: cl("#000"), + fgH: cl("#fff"), bgH: cl("#00f"), + dark: true + } : { + // 'Light BW' + fg: cl("#000"), bg: cl("#fff"), + fg2: cl("#000"), bg2: cl("#cff"), + fgH: cl("#000"), bgH: cl("#0ff"), + dark: false + }; + require("Storage").writeJSON("setting.json", s); + // reload clocks with new theme, otherwise just wait for user to switch apps + if (Bangle.CLOCK) load(global.__FILE__); +} +/** + * Apply LCD options and theme for given mode * @param {int} mode Quiet Mode */ exports.applyOptions = function(mode) { @@ -8,6 +34,7 @@ exports.applyOptions = function(mode) { Bangle.setOptions(get("options", {})); Bangle.setLCDBrightness(get("brightness", 1)); Bangle.setLCDTimeout(get("timeout", 10)); + if ((require("Storage").readJSON("qmsched.json", 1) || {}).switchTheme) switchTheme(mode); }; /** * Set new Quiet Mode and apply Bangle options @@ -20,4 +47,5 @@ exports.setMode = function(mode) { )); exports.applyOptions(mode); if (typeof WIDGETS === "object" && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); + if (global.setAppQuietMode) setAppQuietMode(mode); // current app knows how to update itself }; diff --git a/apps/qmsched/widget.js b/apps/qmsched/widget.js index b25192b06..daa11ac71 100644 --- a/apps/qmsched/widget.js +++ b/apps/qmsched/widget.js @@ -18,7 +18,7 @@ return; // drawWidgets will call draw again } let x = this.x, y = this.y; - g.clearRect(x, y, x+23, y+23); + g.reset().clearRect(x, y, x+23, y+23); // quiet mode: draw red one-way-street sign (dim red on Bangle.js 1) x = this.x+11;y = this.y+11; // center of widget g.setColor(process.env.HWVERSION===2 ? 1 : 0.8, 0, 0).fillCircle(x, y, 8); diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog index 16e65d4f9..b9c26b4e3 100644 --- a/apps/rebble/ChangeLog +++ b/apps/rebble/ChangeLog @@ -1,3 +1,4 @@ 0.01: First release 0.02: Fix typo to Purple 0.03: Added dependancy on Pedometer Widget +0.04: Fixed icon and png to 48x48 pixels diff --git a/apps/rebble/README.md b/apps/rebble/README.md index 712fa4e9b..0ecb51d7a 100644 --- a/apps/rebble/README.md +++ b/apps/rebble/README.md @@ -11,6 +11,7 @@ * Uses pedometer widget to get latest step count * Dependant apps are installed when Rebble installs * Uses the whole screen, widgets are made invisible but still run in the background +* The icon is James Dean - 'Rebel Without a Cause' ![](screenshot_rebble.png) ![](screenshot_rebble2.png) diff --git a/apps/rebble/rebble.icon.js b/apps/rebble/rebble.icon.js index 4c898974e..3fc45b820 100644 --- a/apps/rebble/rebble.icon.js +++ b/apps/rebble/rebble.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("oFA4X/AAIHBw3Aiv3HmE/HQQAF/gPEnWqAAOpy2VqoFB3gPIBoIABtQPJ1PVqv1q3qB5OlrNVEIQPK2tlBwOptQPIyvdH4VtrQPI3tbqtdB4OaB5FVH4NV0pgBB5F13//MIIPJ1O2TgWV/o/I1fbB4WpqoPI1NvB4REBJ5APD/wPBD5JOBB4WVqwPH0oPE0oPJ/NX//6AoNVF5HZq3pq2qSYIPI6tX+pNBB5Ol6v6B4IABH5P7//b1oPBN5GlLwPr9IPK1IPC/SvK1QPCOAIPL6te//5B5lW/5ABL5APB/wPB3IPJ1Y/C/yuBF5APC9X+yo/K34LB3QPBtQPJ//23SPB1QPI3eVs2qJwIPJ1flqyeBtQPJtZPBLwIPKzf/1ROCB5OWAQJOBB5QsBAAQGBf5FlB5tVvoPMNQO9B4daB5O+B4aPIqtX35tBB5M1qtbB4i/HB4WvOAjvGB4IpBIQIADB46aBB4t8B49VB54AFB6zrB1Wm1RTBywPI0oPCeQOaB4+ltOlq2V02VqwPOrQPIF5w/PFQIvPB71pH4uqX8g")) +require("heatshrink").decompress(atob("mEw4X/AoOG4EV+/I+dVAAVUCgcFBIYABpIJBgcFoIKEqkQgEH6EH0ILEqAhCgkBqEVBYdAhUBBoU9GAlAlw5CgERgILDIocEgEGoALDlEHwEAlkUg8EBYfAFwVA+BgEqmQjWrBgMQhgvDqmA9Wq1WsNoMALweDBQIAB4E8BYdTpwLD/kA4AXDjwKC1f/IAILDnQLC1//4ALEHQQLCKgILDFwYLB6EATgVABYe///MNgdA3kQEoILGqCNBlfQh//4NAPAVQ+YLBQYM/ocABYfAiEqgE0g6DBF4eAlFrYQZHDoOu1Xo8lgBYtCKIOo9aOBAAJrCBYWv9X/+gXEqSZC/f//4LHz/6DQIjEBYOhgG6BY1a1WggDCB3ojErYTBoEOa4QLF1X9jWrXwILGKYOvBYtfKYX+17iBHYdX1WQgf/34LBUwQLB1cLWIJqCBYdV9W+1+//oLBWQVVqnuD4M/KQoAB/+kBYJGBCwYLCI4P/DQILFnwLCEQ1Vp+q/46CBYtDXgJ1FAAVwfI4ABqAUCBY8A9gLIqEA9ALEKYYLB9YLERwQ=")) diff --git a/apps/rebble/rebble.png b/apps/rebble/rebble.png index 69653015c..acfd37400 100644 Binary files a/apps/rebble/rebble.png and b/apps/rebble/rebble.png differ diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index bedc63141..e2ae0111b 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -4,3 +4,10 @@ 0.03: Fix theme and maps/graphing if no GPS 0.04: Multiple bugfixes 0.05: Add recording for coresensor +0.06: Add recording for battery stats + Fix execution of other recorders (*.recorder.js) + Modified icons and colors for better visibility + Only show plotting speed if Latitude is available +0.07: Add recording for Barometer + Record all HRM events + Move recording for CoreTemp to its own app diff --git a/apps/recorder/README.md b/apps/recorder/README.md index 4a4561f1c..87be34424 100644 --- a/apps/recorder/README.md +++ b/apps/recorder/README.md @@ -16,7 +16,8 @@ You can record * **Time** The current time * **GPS** GPS Latitude, Longitude and Altitude * **Steps** Steps counted by the step counter -* **HR** Heart rate +* **HR** Heart rate and confidence +* **BAT** Battery percentage and voltage * **Core** CoreTemp body temperature **Note:** It is possible for other apps to record information using this app @@ -25,4 +26,4 @@ function in `widget.js` for more information. ## Tips -When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. +When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a red satellite symbol, which you will see turn green when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. diff --git a/apps/recorder/app.js b/apps/recorder/app.js index fcd8d6031..5b1c63aef 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -199,9 +199,10 @@ function viewTrack(filename, info) { menu['Plot Alt.'] = function() { plotGraph(info, "Altitude"); }; - menu['Plot Speed'] = function() { - plotGraph(info, "Speed"); - }; + if (info.fields.includes("Latitude")) + menu['Plot Speed'] = function() { + plotGraph(info, "Speed"); + }; // TODO: steps, heart rate? menu['Erase'] = function() { E.showPrompt("Delete Track?").then(function(v) { diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 4d8cdddb1..de465b7c1 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -48,67 +48,48 @@ Bangle.removeListener('GPS', onGPS); Bangle.setGPSPower(0,"recorder"); }, - draw : (x,y) => g.setColor(hasFix?"#0ff":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y) + draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y) }; }, hrm:function() { - var bpm = 0, bpmConfidence = 0; - var hasBPM = false; + var bpm = "", bpmConfidence = ""; function onHRM(h) { - if (h.confidence >= bpmConfidence) { - bpmConfidence = h.confidence; - bpm = h.bpm; - if (bpmConfidence) hasBPM = true; - } + bpmConfidence = h.confidence; + bpm = h.bpm; } return { name : "HR", - fields : ["Heartrate"], + fields : ["Heartrate", "Confidence"], getValues : () => { - var r = [bpmConfidence?bpm:""]; - bpm = 0; bpmConfidence = 0; + var r = [bpm,bpmConfidence]; + bpm = ""; bpmConfidence = ""; return r; }, start : () => { - hasBPM = false; Bangle.on('HRM', onHRM); Bangle.setHRMPower(1,"recorder"); }, stop : () => { - hasBPM = false; Bangle.removeListener('HRM', onHRM); Bangle.setHRMPower(0,"recorder"); }, - draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y) + draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) }; }, - - temp:function() { - var core = 0, skin = 0; - var hasCore = false; - function onCore(c) { - core=c.core; - skin=c.skin; - hasCore = true; - } + bat:function() { return { - name : "Core", - fields : ["Core","Skin"], + name : "BAT", + fields : ["Battery Percentage", "Battery Voltage", "Charging"], getValues : () => { - var r = [core,skin]; - return r; + return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()]; }, start : () => { - hasCore = false; - Bangle.on('CoreTemp', onCore); }, stop : () => { - hasCore = false; - Bangle.removeListener('CoreTemp', onCore); }, - draw : (x,y) => g.setColor(hasCore?"#0f0":"#888").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y) + draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y) }; - }, + }, steps:function() { var lastSteps = 0; return { @@ -121,11 +102,41 @@ }, start : () => { lastSteps = Bangle.getStepCount(); }, stop : () => {}, - draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y) + draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y) }; } - // TODO: recAltitude from pressure sensor }; + if (Bangle.getPressure){ + recorders['baro'] = function() { + var temp="",press="",alt=""; + function onPress(c) { + temp=c.temperature; + press=c.pressure; + alt=c.altitude; + } + return { + name : "Baro", + fields : ["Barometer Temperature", "Barometer Pressure", "Barometer Altitude"], + getValues : () => { + var r = [temp,press,alt]; + temp=""; + press=""; + alt=""; + return r; + }, + start : () => { + Bangle.setBarometerPower(1,"recorder"); + Bangle.on('pressure', onPress); + }, + stop : () => { + Bangle.setBarometerPower(0,"recorder"); + Bangle.removeListener('pressure', onPress); + }, + draw : (x,y) => g.setColor("#0f0").drawImage(atob("DAwBAAH4EIHIEIHIEIHIEIEIH4AA"),x,y) + }; + } + } + /* eg. foobar.recorder.js (function(recorders) { recorders.foobar = { @@ -138,7 +149,7 @@ } }) */ - require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders)); + require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders)); return recorders; } diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/run/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/run/README.md b/apps/run/README.md new file mode 100644 index 000000000..c094d4873 --- /dev/null +++ b/apps/run/README.md @@ -0,0 +1,34 @@ +# Run App + +This app allows you to display the status of your run, it +shows distance, time, steps, cadence, pace and more. + +To use it, start the app and press the middle button so that +the red `STOP` in the bottom right turns to a green `RUN`. + +## Display + +* `DIST` - the distance travelled based on the GPS (if you have a GPS lock). + * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As + the GPS updates your position as it gets more satellites your position changes and the distance + shown will increase, even if you are standing still. +* `TIME` - the elapsed time for your run +* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far** +* `HEART` - Your heart rate +* `STEPS` - Steps since you started exercising +* `CADENCE` - Steps per second based on your step rate *over the last minute* +* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically +so if you have no GPS lock you just need to wait. +* The current time is displayed right at the bottom of the screen +* `RUN/STOP` - whether the distance for your run is being displayed or not + +## Recording Tracks + +`Run` doesn't directly allow you to record your tracks at the moment. +However you can just install the `Recorder` app, turn recording on in +that, and then start the `Run` app. + +## TODO + +* Allow this app to trigger the `Recorder` app on and off directly. +* Keep a log of each run's stats (distance/steps/etc) diff --git a/apps/run/app-icon.js b/apps/run/app-icon.js new file mode 100644 index 000000000..a97d1b8ce --- /dev/null +++ b/apps/run/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/run/app.js b/apps/run/app.js new file mode 100644 index 000000000..44c42711c --- /dev/null +++ b/apps/run/app.js @@ -0,0 +1,158 @@ +var B2 = process.env.HWVERSION==2; +var Layout = require("Layout"); +var locale = require("locale") +var fontHeading = "6x8:2"; +var fontValue = B2 ? "6x15:2" : "6x8:3"; +var headingCol = "#888"; +var running = false; +var startTime; +var startSteps; +// This & previous GPS readings +var lastGPS, thisGPS; +var distance = 0; ///< distance in meters +var startSteps = Bangle.getStepCount(); ///< number of steps when we started +var lastStepCount = startSteps; // last time 'step' was called +var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute) + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// --------------------------- + +function formatTime(ms) { + var s = Math.round(ms/1000); + var min = Math.floor(s/60).toString(); + s = (s%60).toString(); + return min.padStart(2,0)+":"+s.padStart(2,0); +} + +// Format speed in meters/second +function formatPace(speed) { + if (speed < 0.1667) { + return `__'__"`; + } + const pace = Math.round(1000 / speed); // seconds for 1km + const min = Math.floor(pace / 60); // minutes for 1km + const sec = pace % 60; + return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`; +} + +// --------------------------- + +function clearState() { + distance = 0; + startSteps = Bangle.getStepCount(); + stepHistory.fill(0); + layout.dist.label=locale.distance(distance); + layout.time.label="00:00"; + layout.pace.label=formatPace(0); + layout.hrm.label="--"; + layout.steps.label=0; + layout.cadence.label= "0"; + layout.status.bgCol = "#f00"; +} + +function onStartStop() { + running = !running; + if (running) { + clearState(); + startTime = Date.now(); + } + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + // if stopping running, don't clear state + // so we can at least refer to what we've done + layout.render(); +} + +var layout = new Layout( { + type:"v", c: [ + { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol }, + {type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 }, + {type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"PACE", fillx:1, col:headingCol }, + {type:"txt", font:fontHeading, label:"HEART", fillx:1, col:headingCol } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontValue, label:`__'__"`, id:"pace", fillx:1 }, + {type:"txt", font:fontValue, label:"--", id:"hrm", fillx:1 } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"STEPS", fillx:1, col:headingCol }, + {type:"txt", font:fontHeading, label:"CADENCE", fillx:1, col:headingCol } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontValue, label:"0", id:"steps", fillx:1 }, + {type:"txt", font:fontValue, label:"0", id:"cadence", fillx:1 } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, + {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, + {type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 } + ]}, + + ] +},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]}); +clearState(); +layout.render(); + + + +function onTimer() { + layout.clock.label = locale.time(new Date(),1); + if (!running) { + layout.render(); + return; + } + // called once a second + var duration = Date.now() - startTime; // in ms + // set cadence based on steps over last minute + var stepsInMinute = E.sum(stepHistory); + var cadence = 60000 * stepsInMinute / Math.min(duration,60000); + // update layout + layout.time.label = formatTime(duration); + layout.steps.label = Bangle.getStepCount()-startSteps; + layout.cadence.label = Math.round(cadence); + layout.render(); + // move step history onwards + stepHistory.set(stepHistory,1); + stepHistory[0]=0; +} + +Bangle.on("GPS", function(fix) { + layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; + lastGPS = thisGPS; + thisGPS = fix; + if (running && fix.fix && lastGPS.fix) { + // work out distance - moving from a to b + var a = Bangle.project(lastGPS); + var b = Bangle.project(thisGPS); + var dx = a.x-b.x, dy = a.y-b.y; + var d = Math.sqrt(dx*dx+dy*dy); // this should be the distance in meters + distance += d; + layout.dist.label=locale.distance(distance); + var duration = Date.now() - startTime; // in ms + var speed = distance * 1000 / duration; // meters/sec + layout.pace.label = formatPace(speed); + } +}); +Bangle.on("HRM", function(h) { + layout.hrm.label = h.bpm; +}); +Bangle.on("step", function(steps) { + if (running) { + layout.steps.label = steps-Bangle.getStepCount(); + stepHistory[0] += steps-lastStepCount; + } + lastStepCount = steps; +}); + +// We always call ourselves once a second, if only to update the time +setInterval(onTimer, 1000); + +/* Turn GPS and HRM on right at the start to ensure +we get the highest chance of a lock. */ +Bangle.setHRMPower(true,"app"); +Bangle.setGPSPower(true,"app"); diff --git a/apps/run/app.png b/apps/run/app.png new file mode 100644 index 000000000..7059b8b01 Binary files /dev/null and b/apps/run/app.png differ diff --git a/apps/run/screenshot.png b/apps/run/screenshot.png new file mode 100644 index 000000000..1a813f19d Binary files /dev/null and b/apps/run/screenshot.png differ diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog new file mode 100644 index 000000000..ebde871fa --- /dev/null +++ b/apps/ruuviwatch/ChangeLog @@ -0,0 +1,2 @@ +1.00: Hello Ruuvi Watch! +1.01: Clear gfx on startup. \ No newline at end of file diff --git a/apps/ruuviwatch/README.md b/apps/ruuviwatch/README.md new file mode 100644 index 000000000..bf4358267 --- /dev/null +++ b/apps/ruuviwatch/README.md @@ -0,0 +1,25 @@ +# Ruuvi Watch + +Watch the status of [RuuviTags](https://ruuvi.com) in range. + + - Id + - Temperature (°C) + - Humidity (%) + - Pressure (hPa) + - Battery voltage + + Also shows how "fresh" the data is (age of reading). + + ## Usage + + - Scans for devices when launched and every N seconds. + - Page trough devices with BTN1/BTN3. + - Trigger scan with BTN2. + +## Todo / ideas + + - Allow to "name" known devices + - Prevent flicker when updating + - Include more data + - Support older Ruuvi protocols + diff --git a/apps/ruuviwatch/ruuviwatch.app-icon.js b/apps/ruuviwatch/ruuviwatch.app-icon.js new file mode 100644 index 000000000..7ed27ef6c --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA")) \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js new file mode 100644 index 000000000..46218a323 --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -0,0 +1,151 @@ +require("Storage").write("ruuviwatch.info",{ + "id":"ruuviwatch", + "name":"Ruuvi Watch", + "src":"ruuviwatch.app.js", + "icon":"ruuviwatch.img" + }); + + const lookup = {}; + const ruuvis = []; + let current = 0; + + function int2Hex (str) { + return ('0' + str.toString(16).toUpperCase()).slice(-2); + } + + function p(data) { + const OFFSET = 7; // 0-4 header, 5-6 Ruuvi id + const robject = {}; + robject.version = data[OFFSET]; + + let temperature = (data[OFFSET+1] << 8) | (data[OFFSET+2] & 0xff); + if (temperature > 32767) { + temperature -= 65534; + } + robject.temperature = temperature / 200.0; + + robject.humidity = (((data[OFFSET+3] & 0xff) << 8) | (data[OFFSET+4] & 0xff)) / 400.0; + robject.pressure = ((((data[OFFSET+5] & 0xff) << 8) | (data[OFFSET+6] & 0xff)) + 50000) / 100.0; + + let accelerationX = (data[OFFSET+7] << 8) | (data[OFFSET+8] & 0xff); + if (accelerationX > 32767) accelerationX -= 65536; // two's complement + robject.accelerationX = accelerationX / 1000.0; + + let accelerationY = (data[OFFSET+9] << 8) | (data[OFFSET+10] & 0xff); + if (accelerationY > 32767) accelerationY -= 65536; // two's complement + robject.accelerationY = accelerationY / 1000.0; + + let accelerationZ = (data[OFFSET+11] << 8) | (data[OFFSET+12] & 0xff); + if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement + robject.accelerationZ = accelerationZ / 1000.0; + + const powerInfo = ((data[OFFSET+13] & 0xff) << 8) | (data[OFFSET+14] & 0xff); + robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0; + robject.txPower = (powerInfo & 0b11111) * 2 - 40; + robject.movementCounter = data[OFFSET+15] & 0xff; + robject.measurementSequenceNumber = ((data[OFFSET+16] & 0xff) << 8) | (data[OFFSET+17] & 0xff); + + robject.mac = [ + int2Hex(data[OFFSET+18]), + int2Hex(data[OFFSET+19]), + int2Hex(data[OFFSET+20]), + int2Hex(data[OFFSET+21]), + int2Hex(data[OFFSET+22]), + int2Hex(data[OFFSET+23]) + ].join(':'); + + robject.name = "Ruuvi " + int2Hex(data[OFFSET+22]) + int2Hex(data[OFFSET+23]); + return robject; + } + + function getAge(created) { + const now = new Date().getTime(); + const ago = ((now - created) / 1000).toFixed(0); + return ago > 0 ? ago + "s ago" : "now"; + } + + function redraw() { + if (ruuvis.length > 0 && ruuvis[current]) { + const ruuvi = ruuvis[current]; + g.clear(); + g.setFontAlign(0,0); + g.setFont("Vector",12); + g.drawString(" (" + (current+1) + "/" + ruuvis.length + ")", g.getWidth()/2, 10); + g.setFont("Vector",20); + g.drawString(ruuvi.name, g.getWidth()/2, 30); + g.setFont("Vector",12); + const age = getAge(ruuvi.time); + if(age > (5*60)) { + g.setColor("#ff0000"); + } else if (age > 60) { + g.setColor("#f39c12"); + } else { + g.setColor("#2ecc71"); + } + g.drawString(age, g.getWidth()/2, 50); + g.setColor("#ffffff"); + g.setFont("Vector",60); + g.drawString(ruuvi.temperature.toFixed(2) + "°c", g.getWidth()/2, g.getHeight()/2); + g.setFontAlign(0,1); + g.setFont("Vector",20); + g.drawString(ruuvi.humidity + "% " + ruuvi.pressure + "hPa ", g.getWidth()/2, g.getHeight()-30); + g.setFont("Vector",12); + g.drawString(ruuvi.battery + "v", g.getWidth()/2, g.getHeight()-10); + } else { + g.clear(); + g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); + g.setFontAlign(0,0); + g.setFont("Vector",16); + g.drawString("Looking for Ruuvi...", g.getWidth()/2, g.getHeight()/2 + 50); + } + } + + function scan() { + NRF.findDevices(function(devices) { + let foundNew = false; + devices.forEach(device => { + const data = p(device.data); + data.time = new Date().getTime(); + const idx = lookup[data.name]; + if (idx !== undefined) { + ruuvis[idx] = data; + } else { + lookup[data.name] = ruuvis.push(data)-1; + foundNew = true; + } + }); + redraw(); + if (foundNew) { + Bangle.buzz(); + g.flip(); + } + + }, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] }); + } + + g.clear(); + g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); + + var drawInterval = setInterval(redraw, 1000); + var scanInterval = setInterval(scan, 10000); + setWatch(() => { + current--; + if (current < 0) { + current = ruuvis.length-1; + } + redraw(); + }, BTN1, {repeat:true}); + + setWatch(() => { + scan(); + }, BTN2, {repeat:true}); + + setWatch(() => { + current++; + if (current >= ruuvis.length) { + current = 0; + } + redraw(); + }, BTN3, {repeat:true}); + + scan(); \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.png b/apps/ruuviwatch/ruuviwatch.png new file mode 100644 index 000000000..3737a7e8c Binary files /dev/null and b/apps/ruuviwatch/ruuviwatch.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index a2245a02d..77c7b2040 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -42,3 +42,5 @@ 0.37: Going into passkey menu now saves settings with passkey 0.38: Restructed menus as per forum discussion 0.39: Fix misbehaving debug info option +0.40: Moved off into Utils, put System after Apps +0.41: Stop users disabling all wake-up methods and locking themselves out (fix #1272) diff --git a/apps/setting/README.md b/apps/setting/README.md index 305c0b610..42e3939fb 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -31,9 +31,12 @@ This is Bangle.js's settings menu * **LCD Brightness** set how bright the LCD is. Due to hardware limitations in the LCD backlight, you may notice flicker if the LCD is not at 100% brightness. * **LCD Timeout** how long should the LCD stay on for if no activity is detected. 0=stay on forever * **Wake on X** should the given activity wake up the Bangle.js LCD? + * On Bangle.js 2 when locked the touchscreen is turned off to save power. Because of this, + `Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js. * **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement. + ## Quiet Mode Quiet Mode is a hint to apps and widgets that you do not want to be disturbed. diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 1208018ed..a32b83d3c 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -11,8 +11,18 @@ function updateSettings() { } function updateOptions() { + var o = settings.options; + // Check to make sure nobody disabled all wakeups and locked themselves out! + if (BANGLEJS2) { + if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) { + o.wakeOnBTN1 = true; + } + } else { + if (!(o.wakeOnBTN1||o.wakeOnBTN2||o.wakeOnBTN3||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) + o.wakeOnBTN2 = true; + } updateSettings(); - Bangle.setOptions(settings.options) + Bangle.setOptions(o) } function gToInternal(g) { @@ -63,14 +73,13 @@ const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; function showMainMenu() { const mainmenu = { - '': { 'title': 'Settings' }, + '': { 'title': /*LANG*/'Settings' }, '< Back': ()=>load(), /*LANG*/'Apps': ()=>showAppSettingsMenu(), - /*LANG*/'Bluetooth': ()=>showBLEMenu(), /*LANG*/'System': ()=>showSystemMenu(), + /*LANG*/'Bluetooth': ()=>showBLEMenu(), /*LANG*/'Alerts': ()=>showAlertsMenu(), - /*LANG*/'Utils': ()=>showUtilMenu(), - /*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() } + /*LANG*/'Utils': ()=>showUtilMenu() }; return E.showMenu(mainmenu); @@ -79,7 +88,7 @@ function showMainMenu() { function showSystemMenu() { const mainmenu = { - '': { 'title': 'System' }, + '': { 'title': /*LANG*/'System' }, '< Back': ()=>showMainMenu(), /*LANG*/'Theme': ()=>showThemeMenu(), /*LANG*/'LCD': ()=>showLCDMenu(), @@ -123,7 +132,7 @@ function showAlertsMenu() { } const mainmenu = { - '': { 'title': 'Alerts' }, + '': { 'title': /*LANG*/'Alerts' }, '< Back': ()=>showMainMenu(), /*LANG*/'Beep': beepMenuItem, /*LANG*/'Vibration': { @@ -155,13 +164,13 @@ function showAlertsMenu() { function showBLEMenu() { - var hidV = [false, "kbmedia", "kb", "joy"]; - var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"]; + var hidV = [false, "kbmedia", "kb", "com", "joy"]; + var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"]; E.showMenu({ '': { 'title': 'Bluetooth' }, '< Back': ()=>showMainMenu(), - 'Make Connectable': ()=>makeConnectable(), - 'BLE': { + /*LANG*/'Make Connectable': ()=>makeConnectable(), + /*LANG*/'BLE': { value: settings.ble, format: boolFormat, onchange: () => { @@ -169,7 +178,7 @@ function showBLEMenu() { updateSettings(); } }, - 'Programmable': { + /*LANG*/'Programmable': { value: settings.blerepl, format: boolFormat, onchange: () => { @@ -177,7 +186,7 @@ function showBLEMenu() { updateSettings(); } }, - 'HID': { + /*LANG*/'HID': { value: Math.max(0,0 | hidV.indexOf(settings.HID)), min: 0, max: 3, format: v => hidN[v], @@ -186,11 +195,11 @@ function showBLEMenu() { updateSettings(); } }, - 'Passkey BETA': { + /*LANG*/'Passkey BETA': { value: settings.passkey?settings.passkey:"none", onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, - 'Whitelist': { + /*LANG*/'Whitelist': { value: settings.whitelist?(settings.whitelist.length+" devs"):"off", onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call } @@ -214,7 +223,7 @@ function showThemeMenu() { var m = E.showMenu({ '':{title:'Theme'}, '< Back': ()=>showSystemMenu(), - 'Dark BW': ()=>{ + /*LANG*/'Dark BW': ()=>{ upd({ fg:cl("#fff"), bg:cl("#000"), fg2:cl("#0ff"), bg2:cl("#000"), @@ -222,7 +231,7 @@ function showThemeMenu() { dark:true }); }, - 'Light BW': ()=>{ + /*LANG*/'Light BW': ()=>{ upd({ fg:cl("#000"), bg:cl("#fff"), fg2:cl("#000"), bg2:cl("#cff"), @@ -230,7 +239,7 @@ function showThemeMenu() { dark:false }); }, - 'Customize': ()=>showCustomThemeMenu(), + /*LANG*/'Customize': ()=>showCustomThemeMenu(), }); function showCustomThemeMenu() { @@ -262,9 +271,9 @@ function showThemeMenu() { "< Back": () => showThemeMenu() }; const labels = { - fg: 'Foreground', bg: 'Background', - fg2: 'Foreground 2', bg2: 'Background 2', - fgH: 'Highlight FG', bgH: 'Highlight BG', + fg: /*LANG*/'Foreground', bg: /*LANG*/'Background', + fg2: /*LANG*/'Foreground 2', bg2: /*LANG*/'Background 2', + fgH: /*LANG*/'Highlight FG', bgH: /*LANG*/'Highlight BG', }; ["fg", "bg", "fg2", "bg2", "fgH", "bgH"].forEach(t => { menu[labels[t]] = { @@ -293,7 +302,7 @@ function showThemeMenu() { function showPasskeyMenu() { var menu = { "< Back" : ()=>showBLEMenu(), - "Disable" : () => { + /*LANG*/"Disable" : () => { settings.passkey = undefined; updateSettings(); showBLEMenu(); @@ -321,7 +330,7 @@ function showPasskeyMenu() { function showWhitelistMenu() { var menu = { "< Back" : ()=>showBLEMenu(), - "Disable" : () => { + /*LANG*/"Disable" : () => { settings.whitelist = undefined; updateSettings(); showBLEMenu(); @@ -329,7 +338,7 @@ function showWhitelistMenu() { }; if (settings.whitelist) settings.whitelist.forEach(function(d){ menu[d.substr(0,17)] = function() { - E.showPrompt('Remove\n'+d).then((v) => { + E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { if (v) { settings.whitelist.splice(settings.whitelist.indexOf(d),1); updateSettings(); @@ -338,8 +347,8 @@ function showWhitelistMenu() { }); } }); - menu['Add Device']=function() { - E.showAlert("Connect device\nto add to\nwhitelist","Whitelist").then(function() { + menu[/*LANG*/'Add Device']=function() { + E.showAlert(/*LANG*/"Connect device\nto add to\nwhitelist",/*LANG*/"Whitelist").then(function() { NRF.removeAllListeners('connect'); showWhitelistMenu(); }); @@ -359,7 +368,7 @@ function showLCDMenu() { const lcdMenu = { '': { 'title': 'LCD' }, '< Back': ()=>showSystemMenu(), - 'LCD Brightness': { + /*LANG*/'LCD Brightness': { value: settings.brightness, min: 0.1, max: 1, @@ -370,7 +379,7 @@ function showLCDMenu() { Bangle.setLCDBrightness(settings.brightness); } }, - 'LCD Timeout': { + /*LANG*/'LCD Timeout': { value: settings.timeout, min: 0, max: 60, @@ -381,7 +390,7 @@ function showLCDMenu() { Bangle.setLCDTimeout(settings.timeout); } }, - 'Wake on BTN1': { + /*LANG*/'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { @@ -392,7 +401,7 @@ function showLCDMenu() { }; if (!BANGLEJS2) Object.assign(lcdMenu, { - 'Wake on BTN2': { + /*LANG*/'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { @@ -400,7 +409,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Wake on BTN3': { + /*LANG*/'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { @@ -409,7 +418,7 @@ function showLCDMenu() { } }}); Object.assign(lcdMenu, { - 'Wake on FaceUp': { + /*LANG*/'Wake on FaceUp': { value: settings.options.wakeOnFaceUp, format: boolFormat, onchange: () => { @@ -417,7 +426,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Wake on Touch': { + /*LANG*/'Wake on Touch': { value: settings.options.wakeOnTouch, format: boolFormat, onchange: () => { @@ -425,7 +434,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Wake on Twist': { + /*LANG*/'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { @@ -433,7 +442,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Twist Threshold': { + /*LANG*/'Twist Threshold': { value: internalToG(settings.options.twistThreshold), min: -0.5, max: 0.5, @@ -443,7 +452,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Twist Max Y': { + /*LANG*/'Twist Max Y': { value: settings.options.twistMaxY, min: -1500, max: 1500, @@ -453,7 +462,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Twist Timeout': { + /*LANG*/'Twist Timeout': { value: settings.options.twistTimeout, min: 0, max: 2000, @@ -469,9 +478,9 @@ function showLCDMenu() { function showLocaleMenu() { const localemenu = { - '': { 'title': 'Locale' }, + '': { 'title': /*LANG*/'Locale' }, '< Back': ()=>showSystemMenu(), - 'Time Zone': { + /*LANG*/'Time Zone': { value: settings.timezone, min: -11, max: 13, @@ -481,7 +490,7 @@ function showLocaleMenu() { updateSettings(); } }, - 'Clock Style': { + /*LANG*/'Clock Style': { value: !!settings["12hour"], format: v => v ? "12hr" : "24hr", onchange: v => { @@ -495,29 +504,29 @@ function showLocaleMenu() { function showUtilMenu() { var menu = { - '': { 'title': 'Utilities' }, + '': { 'title': /*LANG*/'Utilities' }, '< Back': ()=>showMainMenu(), - 'Debug Info': { + /*LANG*/'Debug Info': { value: E.clip(0|settings.log,0,2), min: 0, max: 2, - format: v => ["Hide","Show","Log"][E.clip(0|v,0,2)], + format: v => [/*LANG*/"Hide",/*LANG*/"Show",/*LANG*/"Log"][E.clip(0|v,0,2)], onchange: v => { settings.log = v; updateSettings(); } }, - 'Compact Storage': () => { - E.showMessage("Compacting...\nTakes approx\n1 minute",{title:"Storage"}); + /*LANG*/'Compact Storage': () => { + E.showMessage(/*LANG*/"Compacting...\nTakes approx\n1 minute",{title:/*LANG*/"Storage"}); require("Storage").compact(); showUtilMenu(); }, - 'Rewrite Settings': () => { + /*LANG*/'Rewrite Settings': () => { require("Storage").write(".boot0","eval(require('Storage').read('bootupdate.js'));"); load("setting.app.js"); }, - 'Flatten Battery': () => { - E.showMessage('Flattening battery - this can take hours.\nLong-press button to cancel.'); + /*LANG*/'Flatten Battery': () => { + E.showMessage(/*LANG*/'Flattening battery - this can take hours.\nLong-press button to cancel.'); Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat"); @@ -529,19 +538,20 @@ function showUtilMenu() { var i=1000;while (i--); }, 1); }, - 'Reset Settings': () => { - E.showPrompt('Reset to Defaults?',{title:"Settings"}).then((v) => { + /*LANG*/'Reset Settings': () => { + E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => { if (v) { E.showMessage('Resetting'); resetSettings(); setTimeout(showMainMenu, 50); } else showUtilMenu(); }); - } + }, + /*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() } }; if (Bangle.factoryReset) { - menu['Factory Reset'] = ()=>{ - E.showPrompt('This will remove everything!',{title:"Factory Reset"}).then((v) => { + menu[/*LANG*/'Factory Reset'] = ()=>{ + E.showPrompt(/*LANG*/'This will remove everything!',{title:/*LANG*/"Factory Reset"}).then((v) => { if (v) { E.showMessage(); Terminal.setConsole(); @@ -558,7 +568,7 @@ function makeConnectable() { try { NRF.wake(); } catch (e) { } Bluetooth.setConsole(1); var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", ""); - E.showPrompt(name + "\nStay Connectable?", { title: "Connectable" }).then(r => { + E.showPrompt(name + /*LANG*/"\nStay Connectable?", { title: /*LANG*/"Connectable" }).then(r => { if (settings.ble != r) { settings.ble = r; updateSettings(); @@ -574,7 +584,7 @@ function showClockMenu() { .sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { - 'title': 'Select Clock', + 'title': /*LANG*/'Select Clock', }, '< Back': ()=>showSystemMenu(), }; @@ -592,7 +602,7 @@ function showClockMenu() { }; }); if (clockApps.length === 0) { - clockMenu["No Clocks Found"] = () => { }; + clockMenu[/*LANG*/"No Clocks Found"] = () => { }; } return E.showMenu(clockMenu); } @@ -600,47 +610,47 @@ function showClockMenu() { function showSetTimeMenu() { d = new Date(); const timemenu = { - '': { 'title': 'Set Time' }, + '': { 'title': /*LANG*/'Set Time' }, '< Back': function () { setTime(d.getTime() / 1000); showSystemMenu(); }, - 'Hour': { + /*LANG*/'Hour': { value: d.getHours(), onchange: function (v) { this.value = (v+24)%24; d.setHours(this.value); } }, - 'Minute': { + /*LANG*/'Minute': { value: d.getMinutes(), onchange: function (v) { this.value = (v+60)%60; d.setMinutes(this.value); } }, - 'Second': { + /*LANG*/'Second': { value: d.getSeconds(), onchange: function (v) { this.value = (v+60)%60; d.setSeconds(this.value); } }, - 'Date': { + /*LANG*/'Date': { value: d.getDate(), onchange: function (v) { this.value = ((v+30)%31)+1; d.setDate(this.value); } }, - 'Month': { + /*LANG*/'Month': { value: d.getMonth() + 1, onchange: function (v) { this.value = ((v+11)%12)+1; d.setMonth(this.value - 1); } }, - 'Year': { + /*LANG*/'Year': { value: d.getFullYear(), min: 2019, max: 2100, @@ -654,7 +664,7 @@ function showSetTimeMenu() { function showAppSettingsMenu() { let appmenu = { - '': { 'title': 'App Settings' }, + '': { 'title': /*LANG*/'App Settings' }, '< Back': ()=>showMainMenu(), } const apps = storage.list(/\.settings\.js$/) @@ -671,7 +681,7 @@ function showAppSettingsMenu() { return 0; }) if (apps.length === 0) { - appmenu['No app has settings'] = () => { }; + appmenu[/*LANG*/'No app has settings'] = () => { }; } apps.forEach(function (app) { appmenu[app.name] = () => { showAppSettings(app) }; @@ -688,17 +698,17 @@ function showAppSettings(app) { appSettings = eval(appSettings); } catch (e) { console.log(`${app.name} settings error:`, e) - return showError('Error in settings'); + return showError(/*LANG*/'Error in settings'); } if (typeof appSettings !== "function") { - return showError('Invalid settings'); + return showError(/*LANG*/'Invalid settings'); } try { // pass showAppSettingsMenu as "back" argument appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) - return showError('Error in settings'); + return showError(/*LANG*/'Error in settings'); } } diff --git a/apps/simple_clock/LICENSE b/apps/simple_clock/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/simple_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/simplest/ChangeLog b/apps/simplest/ChangeLog index f37015d6a..e7ab5f2c3 100644 --- a/apps/simplest/ChangeLog +++ b/apps/simplest/ChangeLog @@ -1,3 +1,6 @@ 0.01: Modified for use with new bootloader and firmware 0.02: Use Bangle.setUI for button/launcher handling 0.03: Fix display for Bangle 2 +0.04: Use queueDraw(), update every minute, respect theme, use Lato font +0.05: Decided against custom font as it inceases the code size + minimalism is useful when narrowing down issues diff --git a/apps/simplest/app.js b/apps/simplest/app.js index 68564ff33..582c4c2d5 100644 --- a/apps/simplest/app.js +++ b/apps/simplest/app.js @@ -1,28 +1,55 @@ - const h = g.getHeight(); const w = g.getWidth(); function draw() { - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4].substr(0,5); - + var date = new Date(); + var timeStr = require("locale").time(date,1); + g.reset(); - g.clearRect(0, 30, w, 99); - g.setFontAlign(0, -1); - g.setFont("Vector", w/3); - g.drawString(time, w/2, 40); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + g.setFont('Vector', w/3); + g.setFontAlign(0, 0); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, h/2); + + queueDraw(); } -// handle switch display on by pressing BTN1 -Bangle.on('lcdPower', function(on) { - if (on) draw(); +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } }); g.clear(); + +// Show launcher when middle button pressed +//Bangle.setUI("clock"); +// use clockupdown as it tests for issue #1249 +Bangle.setUI("clockupdown", btn=> { + draw(); +}); + + +// Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); -setInterval(draw, 15000); // refresh every 15s draw(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog new file mode 100644 index 000000000..7c83f6988 --- /dev/null +++ b/apps/sonicclk/Changelog @@ -0,0 +1,2 @@ +1.00 Added sonic clock app +1.01 Fixed text alignment issue; Increased acceleration required to activate twist; \ No newline at end of file diff --git a/apps/sonicclk/README.md b/apps/sonicclk/README.md new file mode 100644 index 000000000..a381e0a07 --- /dev/null +++ b/apps/sonicclk/README.md @@ -0,0 +1,13 @@ +# Sonic Clock + +A classic sonic clock featuring run, stop and wait animations. + +![Sonic Clock screenshot](screenshot.png) + +## Usage + +- Sonic will run when the screen is unlocked +- Sonic will stop when the screen is locked +- Sonic will wait when looking at your watch face (when `Bangle.on("twist", fn)` is fired). + +### Made with love by [Joseph](https://github.com/Johoseph) 🤗 diff --git a/apps/sonicclk/app-icon.js b/apps/sonicclk/app-icon.js new file mode 100644 index 000000000..33e22971b --- /dev/null +++ b/apps/sonicclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AE0ZzIACBIgFFC7oTCylEzOIDYeZogX6LwWd7oYCAAOJC82UpoXFAAKnMC6x2CpIVFC/gSCxOYAYP///4C4X/DBIXWIIwWBDAWPAYIXedQJwG/8AgEP//wgAX2CwIADRoQXmSIoXtJAeEoi+C+lEoAcBogX7zOUondolEpvdAAQXgYIgXCAAwXlAAIYC6ENLx4XtAYMZDAvd6gWJC7IKJABgX/C74A/ADY")) diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js new file mode 100644 index 000000000..296677281 --- /dev/null +++ b/apps/sonicclk/app.js @@ -0,0 +1,284 @@ +const vw = g.getWidth(); +const bgWidth = 384; + +const sonic = { + bpp: 4, + transparent: 1, +}; + +const defaultSonic = { + width: 36, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECKYOComZxAVDogRXBQfdCQQABCLITCogRFCQIRYLAgAFCQwRQBoQCC///AYYRWGAoMBAAgRYCYf/gEAB4ICBCKrDDCL4UECMA3DolEB4IDBAAQRb7oMB7vdHIIRWCAYRCAAoRWCoozDCBQRRBQI3FERIRPPIbGBPIQiICKAQCdQRZDAYIRXCQIJB/4RDEhQRQGYP//7FEGpARQCAYREgEzERQRNAAIRCLIKkBCLg4CYoIABB5IRWAQQRcBoIODCRYRQB4INECLkRgH/ZAMACRgRSmbGBCIIFBmYWCCLAAC7oCBdgNEAgIRWBQY2B7oTBAwMzCYoRUBIIQBCQQGBHIYRUABwRRA=")), +}; + +const startingBuffers = [ + { + width: 49, + height: 63, + buffer: require("heatshrink").decompress(atob("iIAFzIAEyMRjICBABkZDAogCCxgRBxAACzOUpIzDFxmYC4WJzNEGYgyQzOd6lIDoJiNGQYwB7tEDoQxQI4NN7BPDGJ54BondQAgvPGCjDDGCQVCCAVESAOIx/4GJYuCSIQwBFwX/GJbyEAAmDn///AwJjLyDAAsz/4xBGBAvJGAfwhIwHF5QwDgAwHXwYADGB6+DAAY2EGBYzHGA8P+EAgLyKGBIBBgD3ObwVPGAMEoAwIe4+ZolEGAVEGBRfDAYOUpvd6gaBpvQF5IuBFoOJDANE7vdC4IEBF5h8DGAQABGQNBF5YuCGAgyDYZwxE6lDmcxR5gAFGINEmczF6IxCob1B+YxIF5AxCeoQxJzLNBAAQFBhOUoEDGAIxJjOQC4cJAoIvBgAwCGJIwWiEAgcwglEpOZGwgvCGBANBmcAaIJ2BC4cPC4QwIga3BmEIeoIwBDoJhDF4j5DBgIABgGIGAROBMIYvEilBGAgvDAARIBGQL0FA4IwFF4YABJQIyBoczF4lEGA0JCwWUEoPd7qBCzIwDgAwGF4YXBpvd6iyBzORGAVAgLEEF4YwCF4IwEMYMzGAjECC4YwBF4IwEycxic0GAsN7vRiMZDIQvDBwItBS4IqBGAgQBAgIXCzovCLwItBS4MwgAwCF4XTn/xGIiODF4QwCF40z/4xERwYvCGAQvF7vf+CUEEobvCAAJgEF4Xd/6UEXgTvDAANAMAwwFCwgwESBAwFC4owCF43f/vUoaSCABAvCMAv/aQKSDAA8UF4JgG7/0oIWJGAhgG/9EC5YwCSA5eKFQiQIC5sQF4QwD6hfMGpAvBL5g1IhovVGAQvUGAMEF6owBF6owB")), + }, + { + width: 42, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIAEA4QNEAAwUSBwOIAAWZogXECjgWFzPdogECCj5pDondIggUeSAYUjCISRDCQX/CpIUTXQgAEmf/CoIUaCZIUDgAUZRoYADCkIXHCg4BBgAUZX4y+BogUCogUccwgTB7vdCwIEBFA4UTHQYACCgQACogoJCiYAFCoYTHCigTHCoQnICioaJCRwUUHYbhBHpwUTXoQTCmczCpgUTcwYUB/8zAIIUfgDeBCoQUC/4UeRYQPBCoITCHxQUXCYMAgAVBmboKCjCUCAAQPFToIUaMgOIAAIUGBoIUYCoS9DNYIVECYwUWmYVBcIYVBokRBQIUbBoLhB7pjFOYQTGCixpCAwidCCY4UVCoYGFCYIUeBwLaGCZYUVDQ4TRChA")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5CbobeBAAPdCCgPECAYADoggHCCIRFB4YQQXYwSDOgwQPAAoQDBwMzmYRICB5SCGIYQZB4h0CEBoQKmf/oj/CCQQgHCB4PBAAJ1CxAWBEBAQRmYPBAgJCKCCIyCfoYQXCIKQBB4ndGoIQWiL4BgDaBS4IPBGw4QPBwQSCAgMzCoScECCg4FAAQQVB4IREABAQSAAQtGCCoRDOQYiMCBpxGCCoA==")), + }, + { + width: 44, + height: 62, + buffer: require("heatshrink").decompress(atob("iIAEzIADBAgPFCrIQBxAABAYIZDxAVfC4QSBogaFCsQAB7oWFCsAUCogVmSAQVCCQP/CxgVUYggACmf/CwQVdCYwVEgGIgAVbSoQADCsgYGCowACCroWFolECoVECr7wCAALyB7oXBAYJCJCqgUECoYADokzmYWFCrYWFNoIVceQwXDS4QUBCrYACRwQVDAoIXBCrznCMwRAFAAYpFCqf/mf/CgKZFohQGCrIACS4hFCCQ3dIgIVXIQIUDxAVIBAIVXCYJAFxGITIIVfCwMziOZAIIXEAAIVeAAQFBgBFEDITADCrUAAAoeDCwP/IAX/CrAPBog/CJQoWBHwJsFCrAWBCYpqCCgQNECqwUBCpJrCBooVUIBgABEQwVWokzChRPDCrA/NKIgxJCp4FEAB4VVA")), + }, + { + width: 38, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECSgNBxAABAYIWDxASZCgQPBogXFCTgAB7oTFCTQRCogSgPYQSCB4P/CZISRVIgACmf/CYQSXCAwSEgGIgASVdgbvDCToVGCQwACCS4TFolECQVECTLPCe4b4BAAPdCTARECQYADogkJCSYTFCIoSSaIwUDEYwSTAATKCCQa4GCSwRC/8zog3CehASYaIkAmYTGCSYTBCIISEBIITGCSStEZ4YSceo7PBQpASSBAQVFaBISRgChBAIIRCxAkKCSAPBeQI5CK4ITG7oJBCSgRCPoYSFCIISVTwaiEzIREEAISTABH/AAIDBG4YSce4IAB/43MCSQRD7q1CEhwSQRwS1CCTqzDWwgSTA==")), + }, + { + width: 40, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIAEA4QNEAAoTRBoOIAAWZogWECbQVFzPdogECCbpkDondIIgTcRIYThCASKDCIX/ChATSWIgAEmf/CgITYCRITDgATXQwYADCb4WHCY4BBgATXWwy1BogTCogTabYgSB7vdCoIDBEwwTSG4YACB4QACogmICaQAFCgYSGCaYSHCgQlHCagZIWIISNCaQ5DmYTB/8zCbq0DogThbYgTDExwTQWIszmazKCagWFbQNEgATcHwpoBomZgAULCaYUFgA8SCZlE7q5DHIITdBQPda4ITDRpQTXMQoUJCbLhCCgUzCgwTWCgIJFMxATVCgQABBYoACCa4IEBQwAICZwA=")), + }, + { + width: 36, + height: 60, + buffer: require("heatshrink").decompress(atob("iIANzIPOCI+ZABWICKwTDxGZogDBAAIRaHAfdCQoRXKQdECLhWCBIQRBAgP/CQwRQB4YAFmf/CQQRUK4YRIgGIgARUdIoReCQgRGAAQRVCQtEogRCogRXBoLqEdYIAB7oRWCAczmYRCAAdEEQ4RNOIR4CCQoQECKJxBogQCCIJcCPYPdCKo1CCIwIBG4QRUSYZ8BGgszUoQRSFIQSBdQbGDQQIVBCKIQCGILJCCAYRBRIKuCCKf/xARIiIRYK4ZYExBYDCKjDBxGICQdEAYQQCCKjcBBQKjFGgayDCKYAGT4gRSCQYRICAgRVCRYRYcIIRdCQSZCG44YFCKArCEiARQAAYTCChwRKA==")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5C7oADCQLABCCgPBBwkACgogECBwJFCAoPDCCAPFJgJ5BKYJjECCBqBBwf/mYQBAoMzmYRCCCIpCBAIPCA4gQSB4RrBboY6CoggGCCC+DB4YQBOogQRXwYQGU4YQUSYRTEdQgQTWYQzDB4gQVKooABBwYQSRAIQECYgQCHoQQWHQIQDXYLdDCB4RELYQPDCCyOGXYIPKCCAPGCAIgOCBpBLCCKyDBxYQMA==")), + }, + { + width: 36, + height: 60, + buffer: require("heatshrink").decompress(atob("iIANzIPOCI+ZABWICKwTDxGZogDBAAIRaHAfdCQoRXKQdECLhWCBIQRBAgP/CQwRQB4YAFmf/CQQRUK4YRIgGIgARUdIoReCQgRGAAQRVCQtEogRCogRXBoLqEdYIAB7oRWCAczmYRCAAdEEQ4RNOIR4CCQoQECKJxBogQCCIJcCPYwRRGoQRGBAITCCKoACPgI0FUYQSGCJpNDdQbGDQQQRTJooQECIIMBCMMRCKhwDPIMALAoZBCLQADxAABCAQRXogOBUYgUBCLTIDAAQSB/4RYB4oAB/4QDWQYROYowADgDHCCLASGCIYJDCKI2MDQwRQCQgcGQwwRRBAIAGB44RPA==")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5CbobeBAAPdCCgPDmczCAQADoggGCBkAA4RPBCIoPDCCAPDCAZXCOgoQQJYgQEBwJbBCIQQUBAIxFCCgPDboi7DEA4QPXwYPDCAJ1GCDQJCA4YQPB4hTEdQYQXgCXDB4YQVawQDBxAFBBwYQVAAYQBGAoABCCCqBBAcAAAIQGXYQQPCIcAYApACCCgPFAAX/XoQ5FCCwPBGQIPFCCIREmYOCB44QRTAabHCCAA==")), + }, +]; + +const stoppingBuffers = [ + { + width: 44, + height: 60, + buffer: require("heatshrink").decompress(atob("iIAEzIACA4gOFAAwVTCQVEzOIDIdECsAWC7oWCAAIVkogVFCwIVgNggAECsARCAQX//4VC/4WJCqg8FCgIWCAQIVdM43/gEACYICBCriVBAAQVmIg4VmIQdETAVEogZBogVhzNE7oTB7oACCoYbBCrCbECQgVFIoIVeCwcACg4VbN4xrGCrgCBBwMAC4QNBBwYVeM4IABIYYABBgIUGCqv/iK1CCQOINwdEAwIVDC4QVVmf/G4QNBCooFCCoRRBCqv//4WBCpJsECrb0DC4QVDYYgHBCq8zCohEDNAIGBCrgTBCob4DZAQGCCgYZBCqoICAAqUBdwQUCKYLaCCsACBCwP/IAP/IBQVVCgQECCgRrBmYVfAAhACe4QUOCqzwCCoKCLCrYWDCiIVWeQYVY")), + }, + { + width: 48, + height: 60, + buffer: require("heatshrink").decompress(atob("iIAEzIAFBIYQFAAwXXDAVEzOIxAaCogXlDAPdDAYZBC8wPBC4oYBC8h2FC84VBB4QCB///C4X/DBQXWIIwWBDAQCBC8AWGCQMAgAVBAQIXgOwR4DC9JLGC83/JIuIoimColEDYNEC7wYCHYIACondCoPdAAQXDDoIXYCoT7CC4gAFC4RLBC7IWHDAkACw4XZToY9CJI51EC7ZICAoIJBCAMADIQJCC8IADCAQXDbIJHBDgIWJC7AABCgOIPAdEAwIXDDJIXWCwQXFAoQXCcIIXnOwgXjDIQXDaogHBC8JKDOQIGBC8zaGAwQWDDYIXeDYkADAIPBUoX/IxAXeAQIYB/5GB/5GMC64WCAgQWCOoMzC8YAEIwVEapIXhDANEC4KONC7oYDCyYXYDAIABC7QA==")), + }, +]; + +const waitingBuffers = [ + { + width: 36, + height: 62, + offset: -1, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECKYOComZxAVDogRXBQfdCQQABCLITCogRFCQIRYLAgAFCQwRQBoR7C/5aECKwwF/4ABBAQDBCK4TD/8AgH/HQYRUYYYRGzICCCKgUECIoDBCLA3DolEB4IDBAAQRb7oMB7vdHIIRWCAYRCAAoRWCoozDCBQRRBQI3FERIRPPIbGBPIQiICKAQCdQRZDAYIRXCQIJB/4RDEhQRQGYP//7FEGpARQCAYREgEzERQRNAAIRCLIKkBCLg4CYoIABB5IRWAQQRcBoIODCRYRQB4INECLkRgH/ZAMACRgRSmbGBCIIFBmYWCCLAAC7oCBdgNEAgIRWBQY2B7oTBAwMzCYoRUBIIQBCQQGBHIYRUABwRRA")), + }, + { + width: 38, + height: 62, + offset: 1, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECSoPComZxAWDogSZBYfdCYQABCTYUCogSFCYISaLogAFCY4SRBwSCC/5fECTAzF/4ABBAQDBCTIUD/8AgH/HoYSWZwYSGzICCCSwVECQoDBCTQ6DolECAIDBAAQSd7oNB7vdHgISYCIYSCAAoSYCwo2DCJYSSBYI6FEhQSQQAbRBQAQkJCSIRCe4ReDAYISZCYIKB/4SDExYSRGwP//7QEHBISRCIYSEgEzEhYSOAAISCLwKtBCTw7CaAIABCBQSXAQQSeBwIPDCZgSRCAIOECT0RgH/aYMACYUzAgQSambRBBoIFBmdEgAAECSwAC7oCBfINECgIABCSwLDHIPdCgIRDEYgSWBYIRBCYSFBQwwSTABKpICR4A=")), + }, +]; + +const bg = { + width: 384, + height: 153, + bpp: 8, + transparent: 254, + buffer: require("heatshrink").decompress(atob("i4ASj0evF4pFIDKYA/AEp//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5/bx+PB5vX64RFC54v3P/4v/P/4v/P/4v/P/4v/P/4v7CY5/VHIKxZACYvvP/4v/P/4v/P4/GAAR/BAAJP/P/5P/P/5P/F8sjkYnBO4oAHP64vHGKJfdF7p//P/5//P/4dTP4dIpB//P/5//P/5//F+p/TIIpDXMIohLF95//P/5//P/4APj0eP/5//P/5//P/4vxB4Z/XAH4AfP/4A/P/4A/P/4A/P/4A/P/4A/AB5/JTIaXJWZobFDpovvP/5//P/5//P/5//P/5//P/5//AAIPFCpp/JEpoABIIZHBL7IvvR45//P/5//P/5//P/5//P/5//P/IbTP6IAbJIpLXF+Z//F/5//F/5//F/5//F/5//F/5/tAH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH4AP4wACP4IABI/5//I/5//I/5//I/5//I/5//I/6B7PoNIpBF/P/5F/P/5F/P/5F/P/5F/P/5F/AHMejx//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//AHfGAAR/BAAJH/P/5H/P/5H/P/5H/P/5H/P/5H/QPZ9BpFIIv5//Iv4Aj6XSP/4A/P/4A/P/4A96XSP/4A/ACcejx//P/5//AEplB6XSAYJ//AH5//AH5//AHJlB6XSAYJ//AH5//AH5//AGPSAARhBA4oABA4ILBA4p//AH5//AH5//AE/SAARhBA4oABA4ILBA4p//AH5//AH5//AE5dB6QACAofX64HFAoZ//AH5//AH5//AFJdB6QACAofX64HFAoZ//AH5//AH5//AFJdB6XS6/XAYIFLCYJ//AH5//AH5//AFJdB6XS6/XAYIFLCYJ//AH5//AH5//AFZfB6XS6/XhgAEA4ILBB4J//AH5//AH5//AFpfB6XS6/XhgAEA4ILBB4J//AH5//AH5//AE5ZBAAvS6UMhhnBiQACA4ILBCo5//AH5//AH5//AEJZBAAvS6UMhhnBiQACA4ILBCo5//AH4AD4wACRZIA/P/4A/AB/SAA0MhkejwDBAA4VHP/4A/P/4A/P/4Ah6QAGhkMj0eAYIAHCo5//AH4AFP4dIpBF/P/5F/ACpZB6QAC6/XhkMiUSAYIAHB4IABCoIbBP74jBP/7//AH5//AH5ZB6QAC6/XhkMiUSAYIAHB4IABCoIbBP74jBP/4Ajj0ef/5//P/4AF64ACCqJbBAAPS6UMABgPBCoZ/fG4YlJAH7//AH5//AEPXAAQVRLYIAB6XShgAMB4IVDP743DEpIA/f/4A/P/4Af6/X5YACAoIbTL4PS6UMABALBB4J/bD4IhFA4YABHooHDAH7//AH5//ADvX6/LAAQFBDaZfB6XShgAIBYIPBP7YfBEIoHDAAI9FA4YA/f/4A/P/4AV5YACH4PX64HDAAYJBBoIHDEZYRB6XSC4MMAAgHBBYJ/dFoYABGYoHFAobl/f/4A/P/4AX5YACH4PX64HDAAYJBBoIHDEZYRB6XSC4MMAAgHBBYJ/dFoYABGYoHFAobl/f/4A/P/4AX5fL43GH4IFBABINBCIIFBEJIPB6XS6/XAYIABhkMAoYLDP7YvJApITBcv7//AH5//AC/L5fG4w/BAoIAJBoIRBAoIhJB4PS6XX64DBAAMMhgFDBYZ/bF5IFJCYLl/f/4A/P/4AXHYIAD5YAKCIohL6QAEA4MMhgLHP7olD6/XFoIADA4I5Dcf7//AH5//ADI7BAAfLABQRFEJfSAAgHBhkMBY5/dEofX64tBAAYHBHIbj/f/4A/P/4AZ6/X5fLH4IABAoIAFBYoVBEJPSAAQVDNIcMhgJDCIZ/VDoYhFFIIXBiQACA4I9FIIoA/f/4A/P/4AR6/X5fLH4IABAoIAFBYoVBEJPSAAQVDNIcMhgJDCIZ/VDoYhFFIIXBiQACA4I9FIIoA/ACPGAARb/P/5/56/X5YAEIIfSAAQHDCIoZBEY5dJj0ehkMCJp/PIYYADE4IrDAA4VHdf7//AH5//ACPX6/LAAhBD6QACA4YRFDIIjHLpMej0MhgRNP55DDAAYnBFYYAHCo7r/ACp/DpFIIv5//HOfX6/LABHS6QNBAAIFBCJINBF58ej0Mhh/dBIJBBJIYnBiUSAYIAHLIobBdP7//AH5//AB/X6/LABHS6QNBAAIFBCJINBF58ej0Mhh/dBIJBBJIYnBiUSAYIAHLIobBdP4AVSoL/3AH5//6/X5YAI6XSBoIABAoIRJBoJpRhkMP74LBAAJFBE4IALB4IVDc/7//AH5//ACPX6/LABHS6QNBAAIFBCJINBNKMMhh/fBYIABIoInBABYPBCobn/f/4A/P/4AR6/X5YAIIYPSAAQFBCJIdBMJIABiUSA4cMhgHBBoZ/dJYYpBAA5VDcf7//AH5//ACvX6/LABBDB6QACAoIRJDoJhJAAMSiQHDhkMA4INDP7pLDFIIAHKobj/f/4A/P/4AX6/X5YAEIIIHFBJIZBLo4JBAAYHBiUSAYoPHP7IPB6XSEIMMAAgHBBYLh/f/4A/P/4AZ6/X5YAEIIIHFBJIZBLo4JBAAYHBiUSAYoPHP7IPB6XSEIMMAAgHBBYLh/f/4A/P/4Ab6/X5fLH4IDBABINDCoIdFhkMLoIABiUSA4IADBIIHFB4IVDA4J/VBoPS6Q/BAYIABEIIFDBYbj/f/4A/P/4AZ6/X5fLH4IDBABINDCoIdFhkMLoIABiUSA4IADBIIHFB4IVDA4J/VBoPS6Q/BAYIABEIIFDBYbj/f/4A/P/4AZHYIAD6/X5YAGBIIRFD48MAARhBAAMSiUAgAFBAYIHBBoYVDP64NB6QAEA4IjBBY7j/f/4A/P/4AZHYIAD6/X5YAGBIIRFD48MAARhBAAMSiUAgAFBAYIHBBoYVDP64NB6QAEA4IjBBY7j/f/4A/P/4AZ5fL43GH4IAB6/XBIIABAoILDCIIJBEZcMAAUAgBpDA4YNDP7fSAARFDF4oJDCIbl/f/4A/P/4AX5fL43GH4IAB6/XBIIABAoILDCIIJBEZcMAAUAgBpDA4YNDP7fSAARFDF4oJDCIbl/AC6rBX4pH/P/4/35YACHoYDB64ACBIoTDFacej0Mhh/hBpIvHT/b//AH5//AD/LAAQ9DAYPXAAQJFCYYrTj0ehkMP8INJF46f7AEJ/DpFIIv4AhZoJ//MMMMhgfxP7JPfAH7//AFrNBP/5hhhkMD+J/ZJ74A/Mo7//AEvS6R//ZMMMhgfxP7JPfAH7//AFvS6R//ZMMMhgfxP7JPfAH7//MtvS6USiR//La4ABLYYFBhkMA4INDD9Z/RJ74A/f/5l16XSaYZ//IKoABLYYFBhkMA4INDD9Z/RJ74A/f/4Az6QACaYZ//HqPXAAgHBLoIDFB44flP54vhAH7//AGvSAATNBP/49T64AEA4JdBAYoPHD8p/PF8IA/f/5j16QACZoIABP/4ANhkMHoIABKoIHBAAYJBA4oPBCoYHBD8J/PF8YA/f/5j16QACY4IABP/4ANhkMHoIABKoIHBAAYJBA4oPBCoYHBD8J/PF8YA/f/4Ar6XSXYJjFAAILDBo4HFP/4ADhgACLoZRBgEAAoIDBA4INDCoYflP54vhAH4APf/oAd6XSX4JjFAAILDBo4HFP/4ADhgACLoZRBgEAAoIDBA4INDCoYflP54vhAH4APf/oAd6XSX4IABMYYABBZIJDP/4ALhgACgEAJIYHDBoYfrP6JPfAH4ANf/4Ab6XSiQACMYYABBZIJDP/4ALhgACgEAJIYHDBoYfrP6JPfAH4AL4wACP4IABI/4AVYIPS6USiTRDBIrJDCY5//NqZfDD9p/bJ74A/f/4AhYIPS6USiQHBY4IJFZ4YTHP/5tTL4YftP7ZPfAH6BHPoNIpBF/eK/S6USAATHBBIoHHAAJ//NqpfBD+J/ZJ74A/f/4AhYIPS6USAATHBBIoHHAAJ//NqpfBD+J/ZJ74A/Mo7//LrvS6USiTHBA5p//Na5fBD+J/ZJ74A/f/5dl6XSiUSY4IHNP/5rXL4IfxP7JPfAH7//LsvS6USiTHBA44FDAAJ//MZ4ABKYYFBL4IHBBoYfrP6JPfAH6dPf/5dd6XSYYLHBA44FDbYZ//IJoABKYYFBL4IHBBoYfrP6JPfAH6dPf/5ff6XSYoLJBZobXDbIZ//HpfXAAhZDAYoPHD8p/PF8IA/YKL/7L8fS6THBhkMA4IDDBIIABP/49N64AELIYDFB44flP54vhAH7BRf/ZbfYIMMhgFHBJPS6R//AApLDKYoADMIYADB4IVDA4IfhP54vjAH4APf/ZbhZYYFHBJPS6R//AApLDKYoADMIYADB4IVDA4IfhP54vjAH4APf/YAd6QACZooFDYoYLDCoYNDP/4AFJ4JdDLIcAgAFBAYJlDAAIVDD8p/PF8IA/AB7/9ADfSAATJDaIYFBZIYLDCoYNDP/4AFJ4JdDLIcAgAFBAYJlDAAIVDD8p/PF8IA/AB7/9Lb/S6TLDaYYFBA4YPBA4YABP/4ALKoIABgEAJIYHDBoYfrP6JPfAH4ANf/5bd6XSAYMSiTBBAoYHDB4IHDAAJ//ABZVBAAMAgBJDA4YNDD9Z/RJ74A/ABfGAAR/BAAJH/ADbHBiQAEA4IJDP/5nbhkMD95/bJ74A/f/4AnY4MSAAgHBBIZ//M7cMhgfvP7ZPfAGPSAARVBAoYH/A/4H/A/4H/A/4H/A/4H/A/4H/A+4FBiUSA/4H/A/4H/A/4H/A/4H/A/4H/A/4H7AoNIpALBA/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H3AAIFBAAYH/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A/4H/A+8SiQH/A/4H/A/4H/A/4H/A/4H/A/4H/A/oAFvF4A4vG4wHNC44f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D7IZDAoYHFC4IZDAoYHFD/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4fjABYZFABIf/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D/4f/D8IA=")), +}; + +const topSpeed = 15; +const timeout = 200; +let currentSpeed = 0; +let currentSonic = -1; + +let drawTimeout, drawInterval, waitTimeout; +let bgScroll = [0, null]; + +const start = () => { + if (drawTimeout) clearTimeout(drawTimeout); + if (waitTimeout) clearTimeout(waitTimeout); + if (drawInterval) clearInterval(drawInterval); + + drawInterval = setInterval(() => { + draw("start"); + bgScroll[0] += currentSpeed; + if (bgScroll[1]) bgScroll[1] += currentSpeed; + if (currentSpeed < topSpeed) currentSpeed++; + }, timeout); +}; + +const stop = () => { + if (drawTimeout) clearTimeout(drawTimeout); + if (drawInterval) clearInterval(drawInterval); + + drawInterval = setInterval(() => { + if (currentSpeed <= 0) { + clearInterval(drawInterval); + draw("reset"); + } else { + draw("stop"); + bgScroll[0] += currentSpeed; + if (bgScroll[1]) bgScroll[1] += currentSpeed; + currentSpeed--; + } + }, timeout); +}; + +const wait = () => { + currentSonic = -1; + currentSpeed = 0; + if (drawTimeout) clearTimeout(drawTimeout); + if (drawInterval) clearInterval(drawInterval); + Bangle.setLCDPower(1); + + drawInterval = setInterval(() => draw("wait"), timeout); + + waitTimeout = setTimeout(() => { + clearInterval(drawInterval); + currentSonic = -1; + draw("reset"); + }, 7500); +}; + +const queueDraw = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw("reset"); + }, 60000 - (Date.now() % 60000)); +}; + +const drawSonic = (action) => { + let target; + + if (action === "reset" || currentSonic === -1) { + target = defaultSonic; + } else if (action === "start") { + target = startingBuffers[currentSonic]; + } else if (action === "stop") { + if (currentSonic > 1) currentSonic = 0; + target = stoppingBuffers[currentSonic]; + } else { + target = waitingBuffers[currentSonic]; + } + + sonic.width = target.width; + sonic.height = target.height; + sonic.buffer = target.buffer; + sonic.offset = target.offset; + + g.drawImage( + sonic, + vw / 2 - 30 + (50 - sonic.width) + (sonic.offset || 0), + 86 + (65 - sonic.height) + ); + + if (action === "start") { + if (currentSonic === startingBuffers.length - 1) { + currentSonic = 6; + } else { + currentSonic++; + } + } else if (action === "stop") { + if (currentSpeed <= 2) { + currentSonic = -1; + } else if (currentSpeed <= 14) { + currentSonic = 1; + } else { + currentSonic = 0; + } + } else { + if (currentSonic === waitingBuffers.length - 1) { + currentSonic = 0; + } else { + currentSonic++; + } + } +}; + +const drawTime = () => { + const x = vw / 2; + const y = 24 + 25; + + const date = new Date(); + const timeStr = require("locale").time(date, 1).trim(); + const dateStr = require("locale").date(date).toUpperCase(); + + g.setColor("#000"); + g.setFontAlign(0, 0).setFont("6x8", 5); + g.drawString(timeStr, x + 3, y + 2); + + g.setFont("6x8", 1.5); + g.drawString(dateStr, x + 1, y + 29); + + g.setColor("#fff"); + g.setFontAlign(0, 0).setFont("6x8", 5); + g.drawString(timeStr, x, y); + + g.setFont("6x8", 1.5); + g.drawString(dateStr, x, y + 28); +}; + +const draw = (action) => { + if (bgWidth - bgScroll[0] < 0) { + bgScroll[0] = bgScroll[1]; + bgScroll[1] = null; + } + + g.drawImage(bg, -bgScroll[0], 24); + + if (bgWidth - bgScroll[0] < vw) { + bgScroll[1] = bgScroll[0] - bgWidth; + g.drawImage(bg, -bgScroll[1], 24); + } + + drawSonic(action); + drawTime(); + if (action === "reset") queueDraw(); +}; + +g.setTheme({ bg: "#0099ff", fg: "#fff", dark: true }).clear(); + +Bangle.on("lock", (locked) => { + if (locked) { + stop(); + } else { + start(); + } +}); + +Bangle.on("twist", () => wait()); + +Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 12000, + twistThreshold: 1600, +}); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +draw("reset"); + +if (Bangle.isLocked()) { + stop(); +} else { + start(); +} diff --git a/apps/sonicclk/app.png b/apps/sonicclk/app.png new file mode 100644 index 000000000..db359d1e5 Binary files /dev/null and b/apps/sonicclk/app.png differ diff --git a/apps/sonicclk/screenshot.png b/apps/sonicclk/screenshot.png new file mode 100644 index 000000000..63173989a Binary files /dev/null and b/apps/sonicclk/screenshot.png differ diff --git a/apps/themesetter/LICENSE b/apps/themesetter/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/themesetter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/timeandlife/ChangeLog b/apps/timeandlife/ChangeLog new file mode 100644 index 000000000..c7b309a74 --- /dev/null +++ b/apps/timeandlife/ChangeLog @@ -0,0 +1 @@ +0.1: New app diff --git a/apps/timeandlife/README.md b/apps/timeandlife/README.md new file mode 100644 index 000000000..4a638c952 --- /dev/null +++ b/apps/timeandlife/README.md @@ -0,0 +1,5 @@ +# Time and Life + +A simple watchface which displays the time when the screen is tapped and decays according to the rules of [Conway's game of life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). + +![](screenshot.png) diff --git a/apps/timeandlife/app-icon.js b/apps/timeandlife/app-icon.js new file mode 100644 index 000000000..d7608fca4 --- /dev/null +++ b/apps/timeandlife/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkB/4AGCY4PHC/4X/C/4X/C/4XvJ/4X/C/4X/C/4X3AH4A/AH4A/AH4A/")) diff --git a/apps/timeandlife/app.js b/apps/timeandlife/app.js new file mode 100644 index 000000000..4fe758815 --- /dev/null +++ b/apps/timeandlife/app.js @@ -0,0 +1,225 @@ +// Globals +const X = 176, + Y = 176; // screen resolution of bangle 2 +const STEP_TIMEOUT = 1000; +const PAUSE_TIME = 3000; + +const ONE = [ + [0, 1, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [1, 1, 1], +]; +const TWO = [ + [0, 1, 0], + [1, 0, 1], + [0, 0, 1], + [0, 1, 0], + [1, 0, 0], + [1, 0, 0], + [1, 1, 1], +]; +const THREE = [ + [0, 1, 0], + [1, 0, 1], + [0, 0, 1], + [0, 1, 0], + [0, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const FOUR = [ + [0, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 1, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], +]; +const FIVE = [ + [1, 1, 1], + [1, 0, 0], + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const SIX = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 0], + [1, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const SEVEN = [ + [1, 1, 1], + [1, 0, 1], + [0, 0, 1], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], +]; +const EIGHT = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const NINE = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const ZERO = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const NUMBERS = [ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE]; + +// Arraybuffers to store game state +// 484 8 bit integers that are either 1 or 0 form the 22 x 22 grid +let data = new Uint8Array(484); +let nextData = new Uint8Array(484); + +let palette = new Uint16Array(256); // palette for rendering data +palette[0] = g.theme.bg; +palette[1] = g.theme.fg; + +let lastPaused = new Date(); + +// Conway's game of life +// if < 2 neighbours, set off +// if 2 or 3 neighbours, set on +// if > 3 neighbours, set off +/*const updateStateC = E.compiledC(` +// void run(int, int) +void run(char* n, char* m){ + // n is a pointer to the first byte in data, m is for nextdata + int count = 0; + for (int i=0;i<484;i++) { + // Add 8 neighbours, wrapping around + count = + *(n+(i+484-23)%484) + + *(n+(i+484-22)%484) + + *(n+(i+484-21)%484) + + *(n+(i+484-1)%484) + + *(n+(i+484+1)%484) + + *(n+(i+484+21)%484) + + *(n+(i+484+22)%484) + + *(n+(i+484+23)%484); + if (count < 2 || count > 3) { + *(m+i) = 0; + } else { + *(m+i) = 1; + } + } +} +`);*/ +// precompiled - taken from file downloaded from Bangle.js storage after +// Web IDE upload +const updateStateC=function(a){return a=atob('ACLwtU/08nMBJxZGAvLNFQL1536V+/P0A/sUVJ778/UD+xXlAvLPHhD4BMBEXZ778/UD+xXlZERFXQLy4x4sRJ778/UD+xXlAvLlHkVdLESe+/P1A/sV5QLy+R5FXSxEnvvz9QP7FeUC9f1+RV0sRJ778/UD+xXlAvL7HkVdLESe+/P1A/sV5UVdLEQCPAIsNL88RjRGjFQBMrL18n+10fC9AAA='),{run:E.nativeCall(1,'void(int, int)',a)}}(); + +function draw() { + g.drawImage({ + width:22, height:22, bpp: 8, + palette : palette, // ideally we'd just have BPP 1 and would render direct but it makes the code tricky + buffer : data.buffer, + },0,0,{scale:8}); +} + +const step = () => { + if (new Date() - lastPaused < PAUSE_TIME) { + return; + } + let startTime = new Date(); + const dataAddr = E.getAddressOf(data, true); + const nextDataAddr = E.getAddressOf(nextData, true); + updateStateC.run(dataAddr, nextDataAddr); + draw(); + data.set(nextData); +}; + +const setPixel = (i, j) => { + data[i * 22 + j] = 1; + nextData[i * 22 + j] = 1; +}; + +const setNum = (character, i, j) => { + const startJ = j; + character.forEach(row => { + j = startJ; + row.forEach(pixel => { + if (pixel) setPixel(i, j); + j++; + }); + i++; + }); +}; + +const setDots = () => { + setPixel(10, 10); + setPixel(12, 10); +}; + +const drawTime = () => { + lastPaused = new Date(); + g.clear(); + data.fill(0); + const d = new Date(); + const hourTens = Math.floor(d.getHours() / 10); + const hourOnes = d.getHours() % 10; + const minuteTens = Math.floor(d.getMinutes() / 10); + const minuteOnes = d.getMinutes() % 10; + setNum(NUMBERS[hourTens], 8, 1); + setNum(NUMBERS[hourOnes], 8, 6); + setDots(); + setNum(NUMBERS[minuteTens], 8, 13); + setNum(NUMBERS[minuteOnes], 8, 18); + draw(); +}; + +const start = () => { + Bangle.setUI("clock"); // Show launcher when middle button pressed + g.clear(); + Bangle.setLCDTimeout(20); // backlight/lock timeout in seconds + let stepInterval = setInterval(step, STEP_TIMEOUT); + + // Handlers + Bangle.on('touch', drawTime); + + // Sleep mode + Bangle.on('lock', isLocked => { + if (stepInterval) { + clearInterval(stepInterval); + } + stepInterval = undefined; + if (!isLocked) { + drawTime(); + stepInterval = setInterval(step, STEP_TIMEOUT); + } + }); + + drawTime(); +}; + +start(); diff --git a/apps/timeandlife/app.png b/apps/timeandlife/app.png new file mode 100644 index 000000000..b1e837d25 Binary files /dev/null and b/apps/timeandlife/app.png differ diff --git a/apps/timeandlife/screenshot.png b/apps/timeandlife/screenshot.png new file mode 100644 index 000000000..3058c9346 Binary files /dev/null and b/apps/timeandlife/screenshot.png differ diff --git a/apps/touchmenu/ChangeLog b/apps/touchmenu/ChangeLog new file mode 100644 index 000000000..c5277e465 --- /dev/null +++ b/apps/touchmenu/ChangeLog @@ -0,0 +1 @@ +0.01: App launched diff --git a/apps/touchmenu/README.md b/apps/touchmenu/README.md new file mode 100644 index 000000000..0e81f3755 --- /dev/null +++ b/apps/touchmenu/README.md @@ -0,0 +1,40 @@ +# TouchMenu + +A redesign of the built-in `E.showMenu()` to take advantage of the full touch screen on the Bangle.js 2. + +![screenshot](touchmenu.gif) + +## Features + +- All of the features of the built-in `E.showMenu()` +- Icon support for menu items: + ```javascript + menu.items[0].icon = Graphics.createImage(...); + ``` +- Custom accent colors: + ```javascript + E.showMenu({ + "": { + cAB: g.theme.bg2, // Accent background + cAF: g.theme.fg2 // Accent foreground + } + }) + ``` +- Automatic back button detection - name a button `< Back` and it will be given a special position and icon + +## Controls + +- Scroll through the options +- Tap on an option to select it +- Tap on a button again to use it +- Tap on a selected Boolean to toggle it +- Tap on a selected number to change - tap the right side of the screen to decrease, left side to increase +- If detected, tap on the back button in the upper left to go back + +## Requests + +Contact information is on my website: [kyleplo](https://kyleplo.com) + +## Creator + +[kyleplo](https://kyleplo.com) diff --git a/apps/touchmenu/touchmenu.boot.js b/apps/touchmenu/touchmenu.boot.js new file mode 100644 index 000000000..93a0ba1c8 --- /dev/null +++ b/apps/touchmenu/touchmenu.boot.js @@ -0,0 +1,197 @@ +E.showMenu = function(items) { + const gw = g.getWidth(); + const gh = g.getHeight(); + Bangle.removeAllListeners("drag"); + if(!items){ + delete m; + g.clearRect(0, 30, gw, gh - 30); + return false; + } + var loc = require("locale"); + var m = { + info: { + title: "Menu", + cB: g.theme.bg, + cF: g.theme.fg, + cHB: g.theme.bgH, + cHF: g.theme.fgH, + cAB: g.theme.bg2, + cAF: g.theme.fg2, + predraw : () => {}, + preflip : () => {} + }, + scroll: 0, + items: [], + selected: -1, + draw: () => { + g.reset().setFont('12x20'); + m.info.predraw(g); + g.setColor(m.info.cB).fillRect(0, 50, gw, gh - 30).setColor(m.info.cF); + m.items.forEach((e, i) => { + const s = (i * 48) - m.scroll + 50; + if(s < 30 || s > gh - 74){ + return false; + } + if(i == m.selected){ + g.setColor(m.info.cHB).fillRect(0, s, gw, Math.min(s + 48, gh - 30)).setColor(m.info.cHF); + }else{ + g.setColor(m.info.cF); + } + g.drawString(e.title, (e.icon ? 30 : 10), s + 5); + if(e.icon){ + g.drawImage(e.icon, 5, s + 5); + } + if(e.type && s < gh - 72){ + if(e.format){ + g.setFontAlign(1, -1, 0).drawString(e.format(e.value), gw - 10, s + 25).setFontAlign(-1, -1, 0); + }else{ + g.setFontAlign(1, -1, 0).drawString(e.value, gw - 10, s + 25).setFontAlign(-1, -1, 0); + } + } + }); + g.setColor(m.info.cAB).fillRect(0, 30, gw, 50); + g.setColor(m.info.cAF).drawString(m.info.title, (m.back ? 30 : 10), 32); + if(m.back){ + g.drawLine(5, 40, 20, 40); + g.drawLine(5, 40, 15, 33); + g.drawLine(5, 40, 15, 47); + } + m.info.preflip(g, m.scroll > 0, m.scroll < (m.items.length - 1) * 48); + }, + select: (x, y) => { + if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0)){ + if(y){ + if(y < 50 || y > gh - 30){ + return false; + }else{ + m.selected = Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0); + } + }else{ + m.selected = Math.floor(m.scroll / 48); + } + m.draw(); + }else{ + if(m.items[m.selected].type && m.items[m.selected].type === "boolean"){ + m.items[m.selected].value = !m.items[m.selected].value; + m.items[m.selected].onchange(m.items[m.selected].value); + m.draw(); + }else if(m.items[m.selected].type && m.items[m.selected].type === "number"){ + if(x && x < (gw / 2)){ + m.items[m.selected].value = m.items[m.selected].value - (m.items[m.selected].step ? m.items[m.selected].step : 1); + }else{ + m.items[m.selected].value = m.items[m.selected].value + (m.items[m.selected].step ? m.items[m.selected].step : 1); + } + if(m.items[m.selected].value > (m.items[m.selected].max ? m.items[m.selected].max : Infinity)){ + m.items[m.selected].value = m.items[m.selected].min ? m.items[m.selected].min : 0; + } + if(m.items[m.selected].value < (m.items[m.selected].min ? m.items[m.selected].min : 0)){ + m.items[m.selected].value = m.items[m.selected].max ? m.items[m.selected].max : 10; + } + m.items[m.selected].onchange(m.items[m.selected].value); + m.draw(); + }else{ + if(m.items[m.selected]){ + m.items[m.selected](); + } + } + } + }, + move: d => { + m.scroll += (d * 48); + m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48); + m.selected = Math.max(Math.min(Math.floor((m.scroll - 50) / 48), m.items.length - 1), 0); + m.draw(); + }, + }; + Object.keys(items).forEach(i => { + if(i == ""){ + m.info = Object.assign(m.info, items[i]); + }else if(i === "< Back" && items[i]){ + m.back = items[i]; + }else if(items[i]){ + m.items.push(items[i]); + m.items[m.items.length - 1].title = loc.translate(i); + if(items[i].hasOwnProperty("value")){ + if(typeof items[i].value === "boolean"){ + m.items[m.items.length - 1].type = "boolean"; + }else{ + m.items[m.items.length - 1].type = "number"; + } + } + } + }); + m.info.title = loc.translate(m.info.title); + m.draw(); + Bangle.on("drag", d => { + if(!d.b){ + return false; + } + if(d.dx == 0 && d.dy == 0){ + if(d.x < 30 && d.y < 50){ + m.back(); + return false; + } + m.select(d.x, d.y); + }else{ + m.selected = -1; + m.scroll -= d.dy; + m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48); + m.draw(); + } + }); + return m; +}; + +E.showAlert = function (e, t){ + if(!e){ + E.showMenu(); + return false; + } + return new Promise(r => { + const menu = { + "": { + "title": (t ? t : "Alert") + }, + Ok: () => { + E.showMenu(); + r(); + } + }; + menu[e] = () => {}; + E.showMenu(menu); + }); +}; +E.showMessage = E.showAlert; + +E.showPrompt = function (e, t){ + if(!e){ + E.showMenu(); + return false; + } + return new Promise(r => { + const menu = { + "": { + "title": (t && t.title ? t.title : "Choose") + } + }; + menu[e] = () => {}; + if(t && t.buttons){ + Object.keys(t.buttons).forEach(b => { + menu[b] = () => { + E.showMenu(); + r(t.buttons[b]); + }; + }); + }else{ + menu.Yes = () => { + E.showMenu(); + r(true); + }; + menu.No = () => { + E.showMenu(); + r(false); + }; + } + E.showMenu(menu); + }); +}; diff --git a/apps/touchmenu/touchmenu.gif b/apps/touchmenu/touchmenu.gif new file mode 100644 index 000000000..3df4b3462 Binary files /dev/null and b/apps/touchmenu/touchmenu.gif differ diff --git a/apps/touchmenu/touchmenu.png b/apps/touchmenu/touchmenu.png new file mode 100644 index 000000000..58733cbc7 Binary files /dev/null and b/apps/touchmenu/touchmenu.png differ diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 910cd4658..101da48e1 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -11,3 +11,4 @@ 0.12: Allow hiding the widget 0.13: Tweak Bangle.js 2 light theme colors 0.14: Use weather condition code for icon selection +0.15: Fix widget icon diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 8afdfe6df..1d48116e1 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -53,6 +53,16 @@ exports.get = function() { scheduleExpiry(storage.readJSON('weather.json')||{}); +/** + * + * @param cond Weather condition, as one of: + * {number} code: (Preferred form) https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + * {string} weather description (in English: breaks for other languages!) + * {object} use cond.code if present, or fall back to cond.txt + * @param x Left + * @param y Top + * @param r Icon Size + */ exports.drawIcon = function(cond, x, y, r) { var palette; @@ -249,32 +259,35 @@ exports.drawIcon = function(cond, x, y, r) { g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); } - function chooseIcon(condition) { - if (!condition) return () => {}; - condition = condition.toLowerCase(); - if (condition.includes("thunderstorm")) return drawThunderstorm; - if (condition.includes("freezing")||condition.includes("snow")|| - condition.includes("sleet")) { + /* + * Choose weather icon to display based on weather description + */ + function chooseIconByTxt(txt) { + if (!txt) return () => {}; + txt = txt.toLowerCase(); + if (txt.includes("thunderstorm")) return drawThunderstorm; + if (txt.includes("freezing")||txt.includes("snow")|| + txt.includes("sleet")) { return drawSnow; } - if (condition.includes("drizzle")|| - condition.includes("shower")) { + if (txt.includes("drizzle")|| + txt.includes("shower")) { return drawRain; } - if (condition.includes("rain")) return drawShowerRain; - if (condition.includes("clear")) return drawSun; - if (condition.includes("few clouds")) return drawFewClouds; - if (condition.includes("scattered clouds")) return drawCloud; - if (condition.includes("clouds")) return drawBrokenClouds; - if (condition.includes("mist") || - condition.includes("smoke") || - condition.includes("haze") || - condition.includes("sand") || - condition.includes("dust") || - condition.includes("fog") || - condition.includes("ash") || - condition.includes("squalls") || - condition.includes("tornado")) { + if (txt.includes("rain")) return drawShowerRain; + if (txt.includes("clear")) return drawSun; + if (txt.includes("few clouds")) return drawFewClouds; + if (txt.includes("scattered clouds")) return drawCloud; + if (txt.includes("clouds")) return drawBrokenClouds; + if (txt.includes("mist") || + txt.includes("smoke") || + txt.includes("haze") || + txt.includes("sand") || + txt.includes("dust") || + txt.includes("fog") || + txt.includes("ash") || + txt.includes("squalls") || + txt.includes("tornado")) { return drawMist; } return drawUnknown; @@ -298,7 +311,6 @@ exports.drawIcon = function(cond, x, y, r) { case 531: return drawShowerRain; default: return drawRain; } - break; case 6: return drawSnow; case 7: return drawMist; case 8: @@ -308,16 +320,21 @@ exports.drawIcon = function(cond, x, y, r) { case 802: return drawCloud; default: return drawBrokenClouds; } - break; default: return drawUnknown; } } - if (cond.code && cond.code > 0) { - chooseIconByCode(cond.code)(x, y, r); - } else { - chooseIcon(cond.txt)(x, y, r); + function chooseIcon(cond) { + if (typeof (cond)==="object") { + if ("code" in cond) return chooseIconByCode(cond.code); + if ("txt" in cond) return chooseIconByTxt(cond.txt); + } else if (typeof (cond)==="number") { + return chooseIconByCode(cond.code); + } else if (typeof (cond)==="string") { + return chooseIconByTxt(cond.txt); + } + return drawUnknown; } - + chooseIcon(cond)(x, y, r); }; diff --git a/apps/weather/widget.js b/apps/weather/widget.js index f2ddf0b5b..2905d776b 100644 --- a/apps/weather/widget.js +++ b/apps/weather/widget.js @@ -52,8 +52,8 @@ if (!w) return; g.reset(); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); - if (w.txt) { - weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5); + if (w.code||w.txt) { + weather.drawIcon(w, this.x+10, this.y+8, 7.5); } if (w.temp) { let t = require('locale').temp(w.temp-273.15); // applies conversion diff --git a/apps/wid_a_battery_widget/ChangeLog b/apps/wid_a_battery_widget/ChangeLog index b04824ae8..8a1538479 100644 --- a/apps/wid_a_battery_widget/ChangeLog +++ b/apps/wid_a_battery_widget/ChangeLog @@ -1,3 +1,4 @@ 1.00: Release for Bangle 2 (2021/11/18) 1.01: Internal id update to wid_* as per Gordon's request (2021/11/21) -1.02: Support dark themes \ No newline at end of file +1.02: Support dark themes +1.03: Increase screen update rate when charging diff --git a/apps/wid_a_battery_widget/widget.js b/apps/wid_a_battery_widget/widget.js index 8ab644ab3..74c76784d 100644 --- a/apps/wid_a_battery_widget/widget.js +++ b/apps/wid_a_battery_widget/widget.js @@ -1,4 +1,7 @@ (function(){ + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + let COLORS = { 'white': g.theme.dark ? "#000" : "#fff", 'black': g.theme.dark ? "#fff" : "#000", @@ -36,10 +39,14 @@ g.setFontAlign(0,0); g.setFont('6x8'); g.drawString(l, x + 14, y + 10); + + if (Bangle.isCharging()) changeInterval(id, intervalHigh); + else changeInterval(id, intervalLow); } + Bangle.on('charging',function(charging) { draw(); }); - setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), 60000); + var id = setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), intervalLow); WIDGETS["wid_a_battery_widget"]={area:"tr",width:30,draw:draw}; })(); diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 99822b5a9..e70093659 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -11,3 +11,5 @@ 0.12: Fixed for Bangle 2 0.13: Fillbar setting added, see README 0.14: Fix drawing the bar when charging +0.15: Added option to always display the icon when charging (useful if 'hide if charge greater than' is enabled) +0.16: Increase screen update rate when charging diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index b7a5db9e6..b45fc6749 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -13,6 +13,7 @@ 'fillbar': false, 'charger': true, 'hideifmorethan': 100, + 'alwaysoncharge': false, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -68,6 +69,11 @@ format: x => x+"%", onchange: save('hideifmorethan'), }, + 'Show on charge': { // Not sure if this is readable enough in the 'big' menu + value: s.alwaysoncharge, + format: onOffFormat, + onchange: save('alwaysoncharge'), + }, } E.showMenu(menu) }) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 3e5ff47b4..529923386 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,6 +1,9 @@ (function(){ + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + let COLORS = {}; - + if (process.env.HWVERSION == 1) { COLORS = { 'white': -1, // White @@ -17,31 +20,32 @@ 'high': "#0f0", // Green 'ok': "#ff0", // Orange 'low': "#f00", // Red - }; + }; } - const SETTINGS_FILE = 'widbatpc.json' + const SETTINGS_FILE = 'widbatpc.json'; - let settings + let settings; function loadSettings() { - settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; const DEFAULTS = { 'color': 'By Level', 'percentage': true, 'charger': true, 'hideifmorethan': 100, + 'alwaysoncharge': false, }; Object.keys(DEFAULTS).forEach(k=>{ - if (settings[k]===undefined) settings[k]=DEFAULTS[k] + if (settings[k]===undefined) settings[k]=DEFAULTS[k]; }); } function setting(key) { - if (!settings) { loadSettings() } + if (!settings) { loadSettings(); } return settings[key]; } const levelColor = (l) => { // "charging" is very bright -> percentage is hard to read, "high" is ok(ish) - const green = setting('percentage') ? COLORS.high : COLORS.charging + const green = setting('percentage') ? COLORS.high : COLORS.charging; switch (setting('color')) { case 'Monochrome': return COLORS.white; // no chance of reading the percentage here :-( case 'Green': return green; @@ -58,21 +62,26 @@ if (l >= 15) return COLORS.ok; return COLORS.low; } - } + }; const chargerColor = () => { - return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging - } + return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging; + }; + // sets width, returns true if it changed function setWidth() { var w = 40; if (Bangle.isCharging() && setting('charger')) w += 16; - if (E.getBattery() > setting('hideifmorethan')) + if (E.getBattery() > setting('hideifmorethan')) { w = 0; + if( Bangle.isCharging() && setting('alwaysoncharge') === true) + w = 56; + } var changed = WIDGETS["batpc"].width != w; WIDGETS["batpc"].width = w; return changed; } + function draw() { // if hidden, don't draw if (!WIDGETS["batpc"].width) return; @@ -102,11 +111,11 @@ if (!setting('percentage')) { return; } - let gfx = g + let gfx = g; if (setting('color') === 'Monochrome') { // draw text inverted on battery level gfx = Graphics.createCallback(g.getWidth(),g.getHeight(), 1, - (x,y) => {g.setPixel(x,y,x<=xl?0:-1)}) + (x,y) => {g.setPixel(x,y,x<=xl?0:-1);}); } gfx.setFontAlign(-1,-1); if (l >= 100) { @@ -118,19 +127,24 @@ gfx.drawString(l, x + 6, y + 4); } } + // reload widget, e.g. when settings have changed function reload() { - loadSettings() + loadSettings(); // need to redraw all widgets, because changing the "charger" setting // can affect the width and mess with the whole widget layout - setWidth() + setWidth(); g.clear(); Bangle.drawWidgets(); } + // update widget - redraw just widget, or all widgets if size changed function update() { if (setWidth()) Bangle.drawWidgets(); else WIDGETS["batpc"].draw(); + + if (Bangle.isCharging()) changeInterval(id, intervalHigh); + else changeInterval(id, intervalLow); } Bangle.on('charging',function(charging) { @@ -138,20 +152,13 @@ update(); g.flip(); }); - var batteryInterval; + Bangle.on('lcdPower', function(on) { - if (on) { - update(); - // refresh once a minute if LCD on - if (!batteryInterval) - batteryInterval = setInterval(update, 60000); - } else { - if (batteryInterval) { - clearInterval(batteryInterval); - batteryInterval = undefined; - } - } + if (on) update(); }); + + var id = setInterval(()=>WIDGETS["batpc"].draw(), intervalLow); + WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; setWidth(); -})() +})(); diff --git a/apps/widhwt/ChangeLog b/apps/widhwt/ChangeLog index 4c21f3ace..76c4fcec9 100644 --- a/apps/widhwt/ChangeLog +++ b/apps/widhwt/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget! +0.02: Ported to Bangle.js2 diff --git a/apps/widhwt/app.js b/apps/widhwt/app.js new file mode 100644 index 000000000..f18e78643 --- /dev/null +++ b/apps/widhwt/app.js @@ -0,0 +1,13 @@ +// Replace the "Loading..." box +// with our own message +g.clearRect(38, 68, 138, 108); +g.drawRect(38, 68, 138, 108); +g.setFontVector(13); +g.setFontAlign(0, 0, 0); +g.drawString("Wash...", g.getWidth()/2, g.getHeight()/2); + +Bangle.buzz(); +setTimeout(() => { + Bangle.buzz(1E3, 1); + setTimeout(() => load(), 2E3); +}, 35E3); diff --git a/apps/widhwt/widget.js b/apps/widhwt/widget.js index d178a5b5d..5e1f95a41 100644 --- a/apps/widhwt/widget.js +++ b/apps/widhwt/widget.js @@ -6,9 +6,7 @@ g.reset().setColor(color).drawImage(require("heatshrink").decompress(atob("jEYwIKHgwCBhwCBh4CEggPCkACBmAXDBwVZ+EB+F4gEsjl8EgMP+EChk/gEMh+ehkA+YIBxwxBnF/4HggH/wEAj0AA==")), this.x + 1, 0); } - WIDGETS["widhwt"] = { area: "tr", width: 26, draw: draw }; - - Bangle.on('swipe', function() { + function startTimer() { color = 0x41f; Bangle.buzz(); Bangle.drawWidgets(); @@ -17,6 +15,14 @@ Bangle.buzz(1E3, 1); Bangle.drawWidgets(); }, 35E3); + } - }); + if (process.env.HWVERSION == 1) { + WIDGETS["widhwt"] = { + area: "tr", + width: 26, + draw: draw, + }; + Bangle.on('swipe', startTimer); + } })(); diff --git a/apps/widlock/ChangeLog b/apps/widlock/ChangeLog index 3b1436feb..8aeb75429 100644 --- a/apps/widlock/ChangeLog +++ b/apps/widlock/ChangeLog @@ -1,3 +1,4 @@ 0.01: First commit 0.02: Handle new firmwares with 'lock' event 0.03: Don't try to be fancy - just bail out on firmwares without a lock event +0.04: Set sortorder to -1 so that widget always takes up the furthest left position diff --git a/apps/widpa/ChangeLog b/apps/widpa/ChangeLog new file mode 100644 index 000000000..5197bb4bd --- /dev/null +++ b/apps/widpa/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Size widget after step count is reset diff --git a/apps/widpa/README.md b/apps/widpa/README.md new file mode 100644 index 000000000..92fbb8c11 --- /dev/null +++ b/apps/widpa/README.md @@ -0,0 +1,16 @@ +# Simple Pedometer Widget + +*Displays the current step count from `Bangle.getHealthStatus("day").steps` in (6x8,2) font, Requires firmware v2.11.21 or later* + +* Designed to be small, minimal, does one thing well, no settings +* Supports Bangle 1 and Bangle 2 + +## Notes + +* Requires firmware v2.11.21 or later +* `Bangle.getHealthStatus("day").steps` is reset to zero if you reboot your watch with a long BTN Press +* The step count displayed may be a few steps more than that reported by widpedpm as widpedom may not always be loaded. + +![](screenshot_widpa.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/widpa/screenshot_widpa.png b/apps/widpa/screenshot_widpa.png new file mode 100644 index 000000000..e33550f50 Binary files /dev/null and b/apps/widpa/screenshot_widpa.png differ diff --git a/apps/widpa/widpa.wid.js b/apps/widpa/widpa.wid.js new file mode 100644 index 000000000..1c0f27394 --- /dev/null +++ b/apps/widpa/widpa.wid.js @@ -0,0 +1,17 @@ +Bangle.on('step', function(s) { WIDGETS["widpa"].draw(); }); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["widpa"].draw(); +}); +WIDGETS["widpa"]={area:"tl",width:13,draw:function() { + if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off + var steps = Bangle.getHealthStatus("day").steps; + var w = 1 + (steps.toString().length)*12; + if (w != this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); + g.setColor(g.theme.fg); + g.setFont('6x8',2); + g.setFontAlign(-1, 0); + g.drawString(steps, this.x, this.y + 12); +}}; diff --git a/apps/widpb/ChangeLog b/apps/widpb/ChangeLog new file mode 100644 index 000000000..1409a81ff --- /dev/null +++ b/apps/widpb/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Fixed widget id to wibpb, Size widget after step count is reset diff --git a/apps/widpb/README.md b/apps/widpb/README.md new file mode 100644 index 000000000..bec127b6b --- /dev/null +++ b/apps/widpb/README.md @@ -0,0 +1,17 @@ +# Lato Pedometer Widget + +*Displays the current step count from `Bangle.getHealthStatus("day").steps` in the Lato font, Requires firmware v2.11.21 or later* + +* Designed to be minimal, does one thing well, no settings +* Supports Bangle 1 and Bangle 2 + +## Notes + +* Requires firmware v2.11.21 or later +* Uses the Lato custom font, so memory footprint is 500 bytes larger than 'Simple Pedometer Widget' +* `Bangle.getHealthStatus("day").steps` is reset to zero if you reboot your watch with a long BTN Press +* The step count displayed may be a few steps more than that reported by widpedpm as widpedom may not always be loaded. + +![](screenshot_widpb.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/widpb/screenshot_widpb.png b/apps/widpb/screenshot_widpb.png new file mode 100644 index 000000000..af1222e7e Binary files /dev/null and b/apps/widpb/screenshot_widpb.png differ diff --git a/apps/widpb/widpb.wid.js b/apps/widpb/widpb.wid.js new file mode 100644 index 000000000..6129fac51 --- /dev/null +++ b/apps/widpb/widpb.wid.js @@ -0,0 +1,20 @@ +// on.step version +Bangle.on('step', function(s) { WIDGETS["bata"].draw(); }); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["widpb"].draw(); +}); +WIDGETS["widpb"]={area:"tl",width:13,draw:function() { + if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off + var steps = Bangle.getHealthStatus("day").steps; + var w = 1 + (steps.toString().length)*12; + if (w != this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background + g.setColor(g.theme.fg); + // Lato from fonts.google.com, Actual height 17 (17 - 1), Numeric only + const scale = 1; + g.setFontCustom(atob("AAAAABwAAOAAAgAAHAADwAD4AB8AB8AA+AAeAADAAAAOAAP+AH/8B4DwMAGBgAwMAGBgAwOAOA//gD/4AD4AAAAAAAABgAAcAwDAGAwAwP/+B//wAAGAAAwAAGAAAAAAAAIAwHgOA4DwMA+BgOwMDmBg4wOeGA/gwDwGAAAAAAAAAGAHA8A4DwMAGBhAwMMGBjgwOcOA+/gDj4AAAAABgAAcAAHgADsAA5gAOMAHBgBwMAP/+B//wABgAAMAAAAAAAgD4OB/AwOYGBjAwMYGBjBwMe8Bh/AIHwAAAAAAAAAfAAP8AHxwB8GAdgwPMGBxgwMOOAB/gAH4AAAAAAABgAAMAABgAwMAeBgPgMHwBj4AN8AB+AAPAABAAAAAAAMfAH38B/xwMcGBhgwMMGBjgwP+OA+/gDj4AAAAAAAAOAAH4AA/gQMMGBgzwME8BhvAOPgA/4AD8AAEAAAAAAGAwA4OAHBwAAA="), 46, atob("BAgMDAwMDAwMDAwMBQ=="), 21+(scale<<8)+(1<<16)); + g.setFontAlign(-1, 0); + g.drawString(steps, this.x, this.y + 12); +}}; diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index c033ea505..54f6b203b 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -20,3 +20,5 @@ Fix issue with widget overwrite in large font mode Memory usage enhancements 0.20: Fix issue where step count would randomly reset +0.21: Memory usage improvements, fix widget initial width (fix #1170) +0.22: Fix 'stps' regression for 0.21 (fix #1233) diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 0ec0780c9..cc7fdb579 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -1,5 +1,4 @@ (() => { - const PEDOMFILE = "wpedom.json" // Last time Bangle.on('step' was called let lastUpdate = new Date(); // Last step count when Bangle.on('step' was called @@ -8,19 +7,14 @@ let settings; function loadSettings() { - const d = require('Storage').readJSON(PEDOMFILE, 1) || {}; - settings = d.settings || {}; - } - - function setting(key) { - if (!settings) { loadSettings() } - const DEFAULTS = { + const d = require('Storage').readJSON("wpedom.json", 1) || {}; + settings = Object.assign({ 'goal': 10000, 'progress': false, 'large': false, 'hide': false - } - return (key in settings) ? settings[key] : DEFAULTS[key]; + }, d.settings || {}); + return d; } Bangle.on('step', stepCount => { @@ -31,10 +25,10 @@ if (lastUpdate.getDate() == date.getDate()){ stp_today += steps; } else { - // TODO: could save this to PEDOMFILE for lastUpdate's day? + // TODO: could save this to "wpedom.json" for lastUpdate's day? stp_today = steps; } - if (stp_today === setting('goal') + if (stp_today === settings.goal && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { let b = 3, buzz = () => { if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) @@ -51,29 +45,31 @@ }); // When unloading, save state E.on('kill', () => { - if (!settings) { loadSettings() } - let d = { + require("Storage").writeJSON("wpedom.json",{ lastUpdate : lastUpdate.valueOf(), stepsToday : stp_today, settings : settings, - }; - require("Storage").write(PEDOMFILE,d); + }); }); // add your widget - WIDGETS["wpedom"]={area:"tl",width:26, - redraw:function() { // work out the width, and queue a full redraw if needed + WIDGETS["wpedom"]={area:"tl",width:0, + getWidth:function() { let stps = stp_today.toString(); let newWidth = 24; - if (setting('hide')) + if (settings.hide) newWidth = 0; else { - if (setting('large')) { + if (settings.large) { newWidth = 12 * stps.length + 3; - if (setting('progress')) + if (settings.progress) newWidth += 24; } } + return newWidth; + }, + redraw:function() { // work out the width, and queue a full redraw if needed + let newWidth = this.getWidth(); if (newWidth!=this.width) { // width has changed, re-layout all widgets this.width = newWidth; @@ -84,14 +80,14 @@ } }, draw:function() { - if (setting('hide')) return; + if (settings.hide) return; if (stp_today > 99999) stp_today = stp_today % 100000; // cap to five digits + comma = 6 characters let stps = stp_today.toString(); g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background - if (setting('progress')) { + if (settings.progress) { const width = 23, half = 11; - const goal = setting('goal'), left = Math.max(goal-stps,0); + const goal = settings.goal, left = Math.max(goal-stps,0); // blue or dark green g.setColor(left ? "#08f" : "#080").fillCircle(this.x + half, this.y + half, half); if (left) { @@ -113,10 +109,10 @@ } g.reset(); } - if (setting('large')) { + if (settings.large) { g.setFont("6x8",2); g.setFontAlign(-1, 0); - g.drawString(stps, this.x + (setting('progress')?28:4), this.y + 12); + g.drawString(stps, this.x + (settings.progress?28:4), this.y + 12); } else { let w = 24; if (stps.length > 3){ @@ -137,11 +133,12 @@ getSteps:()=>stp_today }; // Load data at startup - let pedomData = require("Storage").readJSON(PEDOMFILE,1); + let pedomData = loadSettings(); if (pedomData) { if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); stp_today = pedomData.stepsToday|0; delete pedomData; } + WIDGETS["wpedom"].width = WIDGETS["wpedom"].getWidth(); })() diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog index 084ca6ed5..2ca405365 100644 --- a/apps/wohrm/ChangeLog +++ b/apps/wohrm/ChangeLog @@ -5,4 +5,7 @@ 0.05: Improved buzz timing and rendering 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed 0.07: Home button fixed and README added -0.08: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) +0.08: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799) +0.09: Ported to Bangle.js2 + Home returns to clock, instead of menu + Add settings diff --git a/apps/wohrm/README.md b/apps/wohrm/README.md index ad9e82525..87b1a65da 100644 --- a/apps/wohrm/README.md +++ b/apps/wohrm/README.md @@ -8,6 +8,9 @@ and will notify you with a buzz whenever your heart rate falls below or jumps ab [Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html). ## Setting the limits + +Use the settings menu to set the limits. On the Bangle.js1 these can in addition be set with the buttons: + For setting the lower limit press button 4 (left part of the watch's touch screen). Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch. @@ -22,7 +25,7 @@ the received value: For 85% and above the bars are green, between 84% and 50% th and below 50% they turn red. ## Closing the app -Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher. +Pressing middle button will switch off the HRM of the watch and return you to the launcher. # HRM usage The HRM is switched on when the app is started. It stays switch on while the app is running, even diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index c9c060e99..ab579463c 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -1,327 +1,400 @@ -/* eslint-disable no-undef */ -const Setter = { - NONE: "none", - UPPER: 'upper', - LOWER: 'lower' -}; - -const shortBuzzTimeInMs = 80; -const longBuzzTimeInMs = 400; - -let upperLimit = 130; -let upperLimitChanged = true; - -let lowerLimit = 100; -let lowerLimitChanged = true; - -let limitSetter = Setter.NONE; - -let currentHeartRate = 0; -let hrConfidence = -1; -let hrChanged = true; -let confidenceChanged = true; - -let setterHighlightTimeout; - -function renderUpperLimitBackground() { - g.setColor(1,0,0); - g.fillRect(125,40, 210, 70); - g.fillRect(180,70, 210, 200); - - //Round top left corner - g.fillEllipse(115,40,135,70); - - //Round top right corner - g.setColor(0,0,0); - g.fillRect(205,40, 210, 45); - g.setColor(1,0,0); - g.fillEllipse(190,40,210,50); - - //Round inner corner - g.fillRect(174,71, 179, 76); - g.setColor(0,0,0); - g.fillEllipse(160,71,179,82); - - //Round bottom - g.setColor(1,0,0); - g.fillEllipse(180,190, 210, 210); -} - -function renderLowerLimitBackground() { - g.setColor(0,0,1); - g.fillRect(10, 180, 100, 210); - g.fillRect(10, 50, 40, 180); - - //Rounded top - g.setColor(0,0,1); - g.fillEllipse(10,40, 40, 60); - - //Round bottom right corner - g.setColor(0,0,1); - g.fillEllipse(90,180,110,210); - - //Round inner corner - g.setColor(0,0,1); - g.fillRect(40,175,45,180); - g.setColor(0,0,0); - g.fillEllipse(41,170,60,179); - - //Round bottom left corner - g.setColor(0,0,0); - g.fillRect(10,205, 15, 210); - g.setColor(0,0,1); - g.fillEllipse(10,200,30,210); -} - -function drawTrainingHeartRate() { - //Only redraw if the display is on - if (Bangle.isLCDOn()) { - renderUpperLimit(); - - renderCurrentHeartRate(); - - renderLowerLimit(); - - renderConfidenceBars(); - } - - buzz(); -} - -function renderUpperLimit() { - if(!upperLimitChanged) { return; } - - g.setColor(1,0,0); - g.fillRect(125,40, 210, 70); - - if(limitSetter === Setter.UPPER){ - g.setColor(255,255, 0); - } else { - g.setColor(255,255,255); - } - g.setFontVector(13); - g.drawString("Upper: " + upperLimit, 125, 50); - - upperLimitChanged = false; -} - -function renderCurrentHeartRate() { - if(!hrChanged) { return; } - - g.setColor(255,255,255); - g.fillRect(55, 110, 165, 150); - - g.setColor(0,0,0); - g.setFontVector(24); - g.setFontAlign(1, -1, 0); - g.drawString(currentHeartRate, 130, 117); - - //Reset alignment to defaults - g.setFontAlign(-1, -1, 0); - - hrChanged = false; -} - -function renderLowerLimit() { - if(!lowerLimitChanged) { return; } - - g.setColor(0,0,1); - g.fillRect(10, 180, 100, 210); - - if(limitSetter === Setter.LOWER){ - g.setColor(255,255, 0); - } else { - g.setColor(255,255,255); - } - g.setFontVector(13); - g.drawString("Lower: " + lowerLimit, 20,190); - - lowerLimitChanged = false; -} - -function renderConfidenceBars(){ - if(!confidenceChanged) { return; } - - if(hrConfidence >= 85){ - g.setColor(0, 255, 0); - } else if (hrConfidence >= 50) { - g.setColor(255, 255, 0); - } else if(hrConfidence >= 0){ - g.setColor(255, 0, 0); - } else { - g.setColor(255, 255, 255); - } - - g.fillRect(45, 110, 55, 150); - g.fillRect(165, 110, 175, 150); - - confidenceChanged = false; -} - -function renderPlusMinusIcons() { - if (limitSetter === Setter.NONE) { - g.setColor(0, 0, 0); - } else { - g.setColor(1, 1, 1); - } - - g.setFontVector(14); - - //+ for Btn1 - g.drawString("+", 222, 50); - - //- for Btn3 - g.drawString("-", 222,165); - - return; -} - -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 buzz() { - // Do not buzz if not confident - if(hrConfidence < 85) { return; } - - if(currentHeartRate > upperLimit) - { - Bangle.buzz(shortBuzzTimeInMs); - setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); - } - - if(currentHeartRate < lowerLimit) - { - Bangle.buzz(longBuzzTimeInMs); - } -} - -function onHrm(hrm){ - if(currentHeartRate !== hrm.bpm){ - currentHeartRate = hrm.bpm; - hrChanged = true; - } - - if(hrConfidence !== hrm.confidence) { - hrConfidence = hrm.confidence; - confidenceChanged = true; - } -} - -function setLimitSetterToLower() { - resetHighlightTimeout(); - - limitSetter = Setter.LOWER; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderUpperLimit(); - renderLowerLimit(); - renderPlusMinusIcons(); -} - -function setLimitSetterToUpper() { - resetHighlightTimeout(); - - limitSetter = Setter.UPPER; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderLowerLimit(); - renderUpperLimit(); - renderPlusMinusIcons(); -} - -function setLimitSetterToNone() { - limitSetter = Setter.NONE; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderLowerLimit(); - renderUpperLimit(); - renderPlusMinusIcons(); -} - -function incrementLimit() { - resetHighlightTimeout(); - - if (limitSetter === Setter.UPPER) { - upperLimit++; - renderUpperLimit(); - upperLimitChanged = true; - } else if(limitSetter === Setter.LOWER) { - lowerLimit++; - renderLowerLimit(); - lowerLimitChanged = true; - } -} - -function decrementLimit(){ - resetHighlightTimeout(); - - if (limitSetter === Setter.UPPER) { - upperLimit--; - renderUpperLimit(); - upperLimitChanged = true; - } else if(limitSetter === Setter.LOWER) { - lowerLimit--; - renderLowerLimit(); - lowerLimitChanged = true; - } -} - -function resetHighlightTimeout() { - if (setterHighlightTimeout) { - clearTimeout(setterHighlightTimeout); - } - - setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); -} - -function switchOffApp(){ - Bangle.setHRMPower(0,"wohrm"); - Bangle.showLauncher(); -} - -Bangle.on('lcdPower', (on) => { - g.clear(); - if (on) { - Bangle.drawWidgets(); - - renderHomeIcon(); - renderLowerLimitBackground(); - renderUpperLimitBackground(); - lowerLimitChanged = true; - upperLimitChanged = true; - drawTrainingHeartRate(); - } -}); - -Bangle.setHRMPower(1,"wohrm"); -Bangle.on('HRM', onHrm); - -setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); -setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); -setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); -setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); - -setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); - -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -renderHomeIcon(); -renderLowerLimitBackground(); -renderUpperLimitBackground(); - -setInterval(drawTrainingHeartRate, 1000); +/* eslint-disable no-undef */ +const Setter = { + NONE: "none", + UPPER: 'upper', + LOWER: 'lower' +}; +const SETTINGS_FILE = "wohrm.setting.json"; +var settings = require('Storage').readJSON(SETTINGS_FILE, 1) || { + upperLimit: 130, + lowerLimit: 100 +}; + +const shortBuzzTimeInMs = 80; +const longBuzzTimeInMs = 400; + +let upperLimitChanged = true; +let lowerLimitChanged = true; + +let limitSetter = Setter.NONE; + +let currentHeartRate = 0; +let hrConfidence = -1; +let hrChanged = true; +let confidenceChanged = true; + +let setterHighlightTimeout; + +const isB1 = process.env.HWVERSION==1; +const upperLshape = isB1 ? { + right: 125, + left: 210, + bottom: 40, + top: 210, + rectWidth: 30, + cornerRoundness: 5, + orientation: -1, + color: '#f00' +} : { + right: Bangle.appRect.x2-100, + left: Bangle.appRect.x2, + bottom: 24, + top: Bangle.appRect.y2, + rectWidth: 26, + cornerRoundness: 4, + orientation: -1, // rotated 180° + color: '#f00' +}; + +const lowerLshape = { + left: isB1 ? 10 : Bangle.appRect.x, + right: 100, + bottom: upperLshape.top, + top: upperLshape.bottom, + rectWidth: upperLshape.rectWidth, + cornerRoundness: upperLshape.cornerRoundness, + orientation: 1, + color: '#00f' +}; + +const centerBar = { + minY: (upperLshape.bottom + upperLshape.top - upperLshape.rectWidth)/2, + maxY: (upperLshape.bottom + upperLshape.top + upperLshape.rectWidth)/2, + confidenceWidth: isB1 ? 10 : 8, + minX: isB1 ? 55 : upperLshape.rectWidth + 14, + maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14 +}; + +const fontSizes = isB1 ? { + limits: 13, + heartRate: 24 +} : { + limits: 12, + heartRate: 20 +}; + +function fillEllipse(x, y, x2, y2) { + g.fillEllipse(Math.min(x, x2), + Math.min(y, y2), + Math.max(x, x2), + Math.max(y, y2)); +} + +/** + * @param p.left: the X coordinate of the left side of the L in its orientation + * @param p.right: the X coordinate of the right side of the L in its orientation + * @param p.top: the Y coordinate of the top side of the L in its orientation + * @param p.bottom: the Y coordinate of the bottom side of the L in its orientation + * @param p.strokeWidth: how thick we draw the letter. + * @param p.cornerRoundness: how much the corners should be rounded + * @param p.orientation: 1 == turned 0°; -1 == turned 180° + * @param p.color: the color to draw the shape + */ +function renderLshape(p) { + g.setColor(p.color); + + g.fillRect(p.right, p.bottom, p.left, p.bottom-p.orientation*p.rectWidth); + g.fillRect(p.left+p.orientation*p.rectWidth, + p.bottom-p.orientation*p.rectWidth, + p.left, + p.top+p.orientation*p.cornerRoundness*2); + + //Round end of small line + fillEllipse(p.right+p.orientation*p.cornerRoundness*2, + p.bottom, + p.right-p.orientation*p.cornerRoundness*2, + p.bottom-p.orientation*p.rectWidth); + + //Round outer corner + g.setColor(g.theme.bg); + g.fillRect(p.left+p.orientation*p.cornerRoundness, + p.bottom, + p.left, + p.bottom-p.orientation*p.cornerRoundness); + g.setColor(p.color); + fillEllipse(p.left+p.orientation*p.cornerRoundness*4, + p.bottom, + p.left, + p.bottom-p.orientation*p.cornerRoundness*2); + + //Round inner corner + g.fillRect(p.left+p.orientation*(p.rectWidth+p.cornerRoundness+1), + p.bottom-p.orientation*(p.rectWidth+1), + p.left+p.orientation*(p.rectWidth+1), + p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness-1)); + g.setColor(g.theme.bg); + fillEllipse(p.left+p.orientation*(p.rectWidth+p.cornerRoundness*4), + p.bottom-p.orientation*(p.rectWidth+1), + p.left+p.orientation*(p.rectWidth+1), + p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness*3-1)); + + //Round end of long line + g.setColor(p.color); + fillEllipse(p.left+p.orientation*p.rectWidth, + p.top+p.orientation*p.cornerRoundness*4, + p.left, + p.top); +} + +function drawTrainingHeartRate() { + //Only redraw if the display is on + if (Bangle.isLCDOn()) { + renderUpperLimit(); + + renderCurrentHeartRate(); + + renderLowerLimit(); + + renderConfidenceBars(); + } + + buzz(); +} + +function renderUpperLimit() { + if(!upperLimitChanged) { return; } + + renderLshape(upperLshape); + + if(limitSetter === Setter.UPPER){ + g.setColor(1,1,0); + } else { + g.setColor(g.theme.fg); + } + g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0); + g.drawString("Upper: " + settings.upperLimit, + upperLshape.right, + upperLshape.bottom+upperLshape.rectWidth/2); + + upperLimitChanged = false; +} + +function renderCurrentHeartRate() { + if(!hrChanged) { return; } + + g.setColor(g.theme.fg); + g.fillRect(centerBar.minX, centerBar.minY, + centerBar.maxX, centerBar.maxY); + + g.setColor(g.theme.bg); + g.setFontVector(fontSizes.heartRate); + g.setFontAlign(1, 0, 0); + g.drawString(currentHeartRate, + Math.max(upperLshape.right+upperLshape.cornerRoundness, + lowerLshape.right-lowerLshape.cornerRoundness), + (centerBar.minY+centerBar.maxY)/2); + + //Reset alignment to defaults + g.setFontAlign(-1, -1, 0); + + hrChanged = false; +} + +function renderLowerLimit() { + if(!lowerLimitChanged) { return; } + + renderLshape(lowerLshape); + + if(limitSetter === Setter.LOWER){ + g.setColor(1,1,0); + } else { + g.setColor(g.theme.fg); + } + g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0); + g.drawString("Lower: " + settings.lowerLimit, + lowerLshape.left + lowerLshape.rectWidth/2, + lowerLshape.bottom - lowerLshape.rectWidth/2); + + lowerLimitChanged = false; +} + +function renderConfidenceBars(){ + if(!confidenceChanged) { return; } + + if(hrConfidence >= 85){ + g.setColor(0, 1, 0); + } else if (hrConfidence >= 50) { + g.setColor(1, 1, 0); + } else if(hrConfidence >= 0){ + g.setColor(1, 0, 0); + } else { + g.setColor(g.theme.fg); + } + + g.fillRect(centerBar.minX-centerBar.confidenceWidth, centerBar.minY, centerBar.minX, centerBar.maxY); + g.fillRect(centerBar.maxX, centerBar.minY, centerBar.maxX+centerBar.confidenceWidth, centerBar.maxY); + + confidenceChanged = false; +} + +function renderPlusMinusIcons() { + if (limitSetter === Setter.NONE) { + g.setColor(g.theme.bg); + } else { + g.setColor(g.theme.fg); + } + + g.setFontVector(14); + + //+ for Btn1 + g.drawString("+", 222, 50); + + //- for Btn3 + g.drawString("-", 222,165); + + return; +} + +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 buzz() { + // Do not buzz if not confident + if(hrConfidence < 85) { return; } + + if(currentHeartRate > settings.upperLimit) + { + Bangle.buzz(shortBuzzTimeInMs); + setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); + } + + if(currentHeartRate < settings.lowerLimit) + { + Bangle.buzz(longBuzzTimeInMs); + } +} + +function onHrm(hrm){ + if(currentHeartRate !== hrm.bpm){ + currentHeartRate = hrm.bpm; + hrChanged = true; + } + + if(hrConfidence !== hrm.confidence) { + hrConfidence = hrm.confidence; + confidenceChanged = true; + } +} + +function setLimitSetterToLower() { + resetHighlightTimeout(); + + limitSetter = Setter.LOWER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderUpperLimit(); + renderLowerLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToUpper() { + resetHighlightTimeout(); + + limitSetter = Setter.UPPER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToNone() { + limitSetter = Setter.NONE; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function incrementLimit() { + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + settings.upperLimit++; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + settings.lowerLimit++; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function decrementLimit(){ + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + settings.upperLimit--; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + settings.lowerLimit--; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function resetHighlightTimeout() { + if (setterHighlightTimeout) { + clearTimeout(setterHighlightTimeout); + } + + setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); +} + +function switchOffApp(){ + Bangle.setHRMPower(0,"wohrm"); + load(); +} + +Bangle.on('lcdPower', (on) => { + if (on) { + Bangle.drawWidgets(); + + if (typeof(BTN5) !== typeof(undefined)) { + renderHomeIcon(); + } + renderLshape(lowerLshape); + renderLshape(upperLshape); + lowerLimitChanged = true; + upperLimitChanged = true; + drawTrainingHeartRate(); + } +}); + +Bangle.setHRMPower(1,"wohrm"); +Bangle.on('HRM', onHrm); + +g.setTheme({bg:"#000",fg:"#fff",dark:true}); +g.reset(); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +if (typeof(BTN5) !== typeof(undefined)) { + renderHomeIcon(); + setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); + setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); + setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); + setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); + + setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); +} else { + setWatch(switchOffApp, BTN1, {edge:"falling", debounce:50, repeat:true}); +} + +setInterval(drawTrainingHeartRate, 1000); diff --git a/apps/wohrm/settings.js b/apps/wohrm/settings.js new file mode 100644 index 000000000..6d31688f4 --- /dev/null +++ b/apps/wohrm/settings.js @@ -0,0 +1,35 @@ +(function menu(back) { + const SETTINGS_FILE = "wohrm.setting.json"; + + // initialize with default settings... + const storage = require('Storage'); + var settings = storage.readJSON(SETTINGS_FILE, 1) || { + upperLimit: 130, + lowerLimit: 100 + }; + + function save() { + storage.write(SETTINGS_FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Workout HRM' }, + '< Back': back, + 'Upper limit': { + value: settings.upperLimit, + min: 100, max: 200, + onchange: v => { + settings.upperLimit = v; + save(); + } + }, + 'Lower limit': { + value: settings.lowerLimit, + min: 50, max: 150, + onchange: v => { + settings.lowerLimit = v; + save(); + } + } + }); +}) diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index e50256fb6..98fd1275f 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -58,6 +58,7 @@ const APP_KEYS = [ ]; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; +const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports' const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"]; @@ -90,7 +91,7 @@ apps.forEach((app,appIdx) => { if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`); else { app.supports.forEach(dev => { - if (!["BANGLEJS","BANGLEJS2"].includes(dev)) + if (!SUPPORTS_DEVICES.includes(dev)) ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`); }); } @@ -140,6 +141,13 @@ apps.forEach((app,appIdx) => { if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`) if (fileNames.includes(file.name) && !file.supports) // assume that there aren't duplicates if 'supports' is set ERROR(`App ${app.id} file ${file.name} is a duplicate`); + if (file.supports && !Array.isArray(file.supports)) + ERROR(`App ${app.id} file ${file.name} supports field must be an array`); + if (file.supports) + file.supports.forEach(dev => { + if (!SUPPORTS_DEVICES.includes(dev)) + ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`); + }); fileNames.push(file.name); allFiles.push({app: app.id, file: file.name}); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); @@ -271,7 +279,8 @@ while(fileA=allFiles.pop()) { if (globA.test(nameB)||globB.test(nameA)) { if (isGlob(nameA)||isGlob(nameB)) ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) - else WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + else if (fileA.app != fileB.app) + WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) } }) } diff --git a/core b/core index 5a5957714..649489412 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 5a5957714d4aa04413329f57c03e6de0cfb74caf +Subproject commit 649489412e27ef770bc0c8ed12cfca6a17a98c0d diff --git a/loader.js b/loader.js index 768f5f38f..a0c280634 100644 --- a/loader.js +++ b/loader.js @@ -40,7 +40,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) { if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { - showToast(`You're using an old Bangle.js firmware (${deviceVersion}). You can update with the instructions here` ,"warning", 20000); + showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update with the instructions here` ,"warning", 20000); } diff --git a/modules/Layout.js b/modules/Layout.js index 6dc4b6368..cb64183ea 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -34,7 +34,7 @@ layoutObject has: optional `scale` specifies if image should be scaled up or not * `"custom"` - a custom block where `render(layoutObj)` is called to render * `"h"` - Horizontal layout, `c` is an array of more `layoutObject` - * `"v"` - Veritical layout, `c` is an array of more `layoutObject` + * `"v"` - Vertical layout, `c` is an array of more `layoutObject` * A `id` field. If specified the object is added with this name to the returned `layout` object, so can be referenced as `layout.foo` * A `font` field, eg `6x8` or `30%` to use a percentage of screen height @@ -42,8 +42,8 @@ layoutObject has: and `fillx`/`filly` to be set. Not compatible with text rotation. * A `col` field, eg `#f00` for red * A `bgCol` field for background color (will automatically fill on render) -* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center -* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center +* A `halign` field to set horizontal alignment WITHIN a `v` container. `-1`=left, `1`=right, `0`=center +* A `valign` field to set vertical alignment WITHIN a `h` container. `-1`=top, `1`=bottom, `0`=center * A `pad` integer field to set pixels padding * A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space * A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space @@ -261,6 +261,7 @@ Layout.prototype.render = function (l) { x,y+4 ], bg = l.selected?g.theme.bgH:g.theme.bg2; g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly); + if (l.col) g.setColor(l.col); if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad)); else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); }, "img":function(l){