diff --git a/README.md b/README.md index 20ae8afb2..8e186cf79 100644 --- a/README.md +++ b/README.md @@ -384,14 +384,18 @@ Example `settings.js` ```js // make sure to enclose the function in parentheses (function(back) { - function get(key, def) { return require('Settings').get('myappid', key, def); } - function set(key, value) { require('Settings').set('myappid', key, value); } + let settings = require('Storage').readJSON('myappid.json',1)||{}; + if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value + function save(key, value) { + settings[key] = value; + require('Storage').write('myappid.json', settings); + } const appMenu = { '': {'title': 'App Settings'}, '< Back': back, 'Monkeys': { - value: get('monkeys', 12), - onchange: (m) => set('monkeys', m) + value: settings.monkeys, + onchange: (m) => {save('monkeys', m)} } }; E.showMenu(appMenu) diff --git a/apps.json b/apps.json index 2e338a432..c379a1de4 100644 --- a/apps.json +++ b/apps.json @@ -1,9 +1,9 @@ [ { "id": "fwupdate", - "name": "Firmware Update (BETA)", - "version": "0.01", - "description": "Uploads new Espruino firmwares to Bangle.js 2", + "name": "Firmware Update", + "version": "0.02", + "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", "tags": "tools,system", @@ -11,12 +11,12 @@ "custom": "custom.html", "customConnect": true, "storage": [], - "sortorder": -20 + "sortorder": 20 }, { "id": "boot", "name": "Bootloader", - "version": "0.36", + "version": "0.39", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -29,10 +29,55 @@ ], "sortorder": -10 }, + { + "id": "hebrew_calendar", + "name": "Hebrew Calendar", + "shortName": "HebCal", + "version": "0.04", + "description": "lists the date according to the hebrew calendar", + "icon": "app.png", + "allow_emulator": false, + "tags": "tool,locale", + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "hebrew_calendar.app.js", + "url": "app.js" + }, + { + "name": "hebrewDate", + "url": "hebrewDate.js" + }, + { + "name": "hebrew_calendar.img", + "url": "app-icon.js", + "evaluate": true + } + ] + }, + { "id": "golfscore", + "name": "Golf Score", + "shortName":"golfscore", + "version":"0.02", + "description": "keeps track of strokes during a golf game", + "icon": "app.png", + "tags": "outdoors", + "allow_emulator": true, + "supports" : ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"golfscore.app.js","url":"app.js"}, + {"name":"golfscore.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "messages", "name": "Messages", - "version": "0.07", + "version": "0.13", "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", "type": "app", @@ -47,14 +92,15 @@ {"name":"messages","url":"lib.js"} ], "data": [{"name":"messages.json"},{"name":"messages.settings.json"}], + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}], "sortorder": -9 }, { "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.04", - "description": "(BETA) App to display notifications from Gadgetbridge on Android. This will eventually replace the Gadgetbridge widget.", + "version": "0.05", + "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", "dependencies": {"messages":"app"}, @@ -70,8 +116,8 @@ { "id": "ios", "name": "iOS Integration", - "version": "0.03", - "description": "(BETA) App to display notifications from iOS devices", + "version": "0.07", + "description": "Display notifications/music/etc from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", "dependencies": {"messages":"app"}, @@ -87,7 +133,7 @@ "id": "health", "name": "Health Tracking", "version": "0.08", - "description": "Logs health data and provides an app to view it (BETA - requires firmware 2v11)", + "description": "Logs health data and provides an app to view it (requires firmware 2v10.100 or later)", "icon": "app.png", "tags": "tool,system,health", "supports": ["BANGLEJS","BANGLEJS2"], @@ -104,7 +150,7 @@ "id": "launch", "name": "Launcher", "shortName": "Launcher", - "version": "0.08", + "version": "0.10", "description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "icon": "app.png", "type": "launch", @@ -112,14 +158,16 @@ "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"launch.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]}, - {"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]} + {"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}, + {"name":"launch.settings.js","url":"settings.js","supports":["BANGLEJS2"]} ], + "data": [{"name":"launch.json"}], "sortorder": -10 }, { "id": "setting", "name": "Settings", - "version": "0.33", + "version": "0.37", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -135,7 +183,7 @@ { "id": "about", "name": "About", - "version": "0.11", + "version": "0.12", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "icon": "app.png", "tags": "tool,system", @@ -170,7 +218,7 @@ { "id": "locale", "name": "Languages", - "version": "0.10", + "version": "0.14", "description": "Translations for different countries", "icon": "locale.png", "type": "locale", @@ -236,16 +284,17 @@ "id": "mywelcome", "name": "Customised Welcome", "shortName": "My Welcome", - "version": "0.12", + "version": "0.13", "description": "Appears at first boot and explains how to use Bangle.js. Like 'Welcome', but can be customised with a greeting", "icon": "app.png", "tags": "start,welcome", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "screenshots": [{"url":"bangle1-customized-welcome-screenshot.png"}], "storage": [ {"name":"mywelcome.boot.js","url":"boot.js"}, - {"name":"mywelcome.app.js","url":"app.js"}, + {"name":"mywelcome.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, + {"name":"mywelcome.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]}, {"name":"mywelcome.settings.js","url":"settings.js"}, {"name":"mywelcome.img","url":"app-icon.js","evaluate":true} ], @@ -254,8 +303,8 @@ { "id": "gbridge", "name": "Gadgetbridge", - "version": "0.24", - "description": "The default notification handler for Gadgetbridge notifications from Android. This will eventually be replaced by the 'Android' app.", + "version": "0.25", + "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", @@ -269,6 +318,20 @@ ], "data": [{"name":"gbridge.json"}] }, + { "id": "gbdebug", + "name": "Gadgetbridge Debug", + "shortName":"GB Debug", + "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": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"gbdebug.app.js","url":"app.js"}, + {"name":"gbdebug.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "mclock", "name": "Morphing Clock", @@ -449,22 +512,22 @@ ] }, { - "id": "mandlebrotclock", - "name": "Mandlebrot Clock", + "id": "mandelbrotclock", + "name": "Mandelbrot Clock", "version": "0.01", - "description": "A mandlebrot set themed clock cool", - "icon": "mandlebrotclock.png", - "screenshots": [{ "url": "screenshot_mandlebrotclock.png" }], + "description": "A mandelbrot set themed clock cool", + "icon": "mandelbrotclock.png", + "screenshots": [{ "url": "screenshot_mandelbrotclock.png" }], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "storage": [ - { "name": "mandlebrotclock.app.js", "url": "mandlebrotclock.js" }, + { "name": "mandelbrotclock.app.js", "url": "mandelbrotclock.js" }, { - "name": "mandlebrotclock.img", - "url": "mandlebrotclock-icon.js", + "name": "mandelbrotclock.img", + "url": "mandelbrotclock-icon.js", "evaluate": true } ] @@ -496,7 +559,7 @@ "icon": "clock-impword.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"bangle1-impercise-word-clock-screenshot.png"}], "allow_emulator": true, "storage": [ @@ -685,10 +748,11 @@ { "id": "gpsrec", "name": "GPS Recorder", - "version": "0.26", + "version": "0.27", "description": "Application that allows you to record a GPS track. Can run in background", "icon": "app.png", "tags": "tool,outdoors,gps,widget", + "screenshots": [{"url":"screenshot.png"}], "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "interface": "interface.html", @@ -781,7 +845,7 @@ { "id": "weather", "name": "Weather", - "version": "0.11", + "version": "0.13", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], @@ -872,7 +936,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", - "version": "0.13", + "version": "0.14", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "icon": "widget.png", "type": "widget", @@ -1084,7 +1148,7 @@ { "id": "qrcode", "name": "Custom QR Code", - "version": "0.02", + "version": "0.04", "description": "Use this to upload a customised QR code to Bangle.js", "icon": "app.png", "tags": "qrcode", @@ -1635,14 +1699,15 @@ "id": "rtorch", "name": "Red Torch", "shortName": "RedTorch", - "version": "0.01", - "description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets", + "version": "0.02", + "description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or on Bangle 1 press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets", "icon": "app.png", "tags": "tool,torch", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, "storage": [ {"name":"rtorch.app.js","url":"app.js"}, - {"name":"rtorch.wid.js","url":"widget.js"}, + {"name":"rtorch.wid.js","url":"widget.js", "supports": ["BANGLEJS"]}, {"name":"rtorch.img","url":"app-icon.js","evaluate":true} ] }, @@ -1712,7 +1777,7 @@ "id": "cliock", "name": "Commandline-Clock", "shortName": "CLI-Clock", - "version": "0.14", + "version": "0.15", "description": "Simple CLI-Styled Clock", "icon": "app.png", "screenshots": [{"url":"screenshot_cli.png"}], @@ -1894,6 +1959,19 @@ {"name":"widmp.wid.js","url":"widget.js"} ] }, + { + "id": "widmpsh", + "name": "Moon Phase Widget Southern Hemisphere", + "version": "0.01", + "description": "Display the current moon phase in blueish for the southern hemisphere in eight phases", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widmpsh.wid.js","url":"widget.js"} + ] + }, { "id": "minionclk", "name": "Minion clock", @@ -1914,11 +1992,12 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.09", - "description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are", + "version": "0.11", + "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "icon": "app.png", - "tags": "outdoors,gps", + "tags": "outdoors,gps,osm", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], "custom": "custom.html", "customConnect": true, "storage": [ @@ -1948,11 +2027,12 @@ "id": "chronowid", "name": "Chrono Widget", "shortName": "Chrono Widget", - "version": "0.03", + "version": "0.04", "description": "Chronometer (timer) which runs as widget.", "icon": "app.png", "tags": "tool,widget", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ {"name":"chronowid.wid.js","url":"widget.js"}, @@ -2041,12 +2121,12 @@ "id": "numerals", "name": "Numerals Clock", "shortName": "Numerals Clock", - "version": "0.09", + "version": "0.10", "description": "A simple big numerals clock", "icon": "numerals.png", "type": "clock", "tags": "numerals,clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, "screenshots": [{"url":"bangle1-numerals-screenshot.png"}], "storage": [ @@ -2086,6 +2166,20 @@ {"name":"snake.img","url":"snake-icon.js","evaluate":true} ] }, + { "id": "snek", + "name": "The snek game", + "shortName":"Snek", + "version": "0.02", + "description": "A snek game where you control a snek to eat all the apples!", + "screenshots": [{"url":"screenshot_snek.png"}], + "icon": "snek.png", + "supports": ["BANGLEJS2"], + "tags": "game,fun", + "storage": [ + {"name":"snek.app.js","url":"snek.js"}, + {"name":"snek.img","url":"snek.icon.js","evaluate":true} + ] + }, { "id": "calculator", "name": "Calculator", @@ -2335,7 +2429,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.02", + "version": "0.03", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], @@ -2345,8 +2439,10 @@ "allow_emulator": true, "storage": [ {"name":"calendar.app.js","url":"calendar.js"}, + {"name":"calendar.settings.js","url":"settings.js"}, {"name":"calendar.img","url":"calendar-icon.js","evaluate":true} - ] + ], + "data": [{"name":"calendar.json"}] }, { "id": "hidjoystick", @@ -2584,12 +2680,12 @@ "id": "widviz", "name": "Widget Visibility Widget", "shortName": "Viz Widget", - "version": "0.02", + "version": "0.03", "description": "Swipe left to hide top bar widgets, swipe right to redisplay.", "icon": "eye.png", "type": "widget", "tags": "widget", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widviz.wid.js","url":"widget.js"} ] @@ -3201,7 +3297,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.05", + "version": "0.07", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", @@ -3212,8 +3308,11 @@ "storage": [ {"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]}, {"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]}, + {"name":"dtlaunch.settings.js","url":"settings-b1.js", "supports": ["BANGLEJS"]}, + {"name":"dtlaunch.settings.js","url":"settings-b2.js", "supports": ["BANGLEJS2"]}, {"name":"dtlaunch.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"dtlaunch.json"}] }, { "id": "HRV", @@ -3722,7 +3821,7 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.07", + "version": "0.08", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], @@ -3797,10 +3896,11 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.03", - "description": "Automatically turn Quiet Mode on or off at set times", + "version": "0.05", + "description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.", "icon": "app.png", - "screenshots": [{"url":"screenshot_edit.png"},{"url":"screenshot_main.png"},{"url":"screenshot_widget_alarms.png"},{"url":"screenshot_widget_silent.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"}], "tags": "tool,widget", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", @@ -3914,11 +4014,11 @@ { "id": "thermom", "name": "Thermometer", - "version": "0.03", + "version": "0.04", "description": "Displays the current temperature in degree Celsius, updated every 20 seconds", "icon": "app.png", "tags": "tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, "storage": [ {"name":"thermom.app.js","url":"app.js"}, @@ -4021,7 +4121,7 @@ {"name":"carcrazy.img","url":"app-icon.js","evaluate":true}, {"name":"carcrazy.settings.js","url":"settings.js"} ], - "data": [{"name":"app.json"}] + "data": [{"name":"CarCrazy.csv"}] }, { "id": "shortcuts", @@ -4043,14 +4143,17 @@ { "id": "vectorclock", "name": "Vector Clock", - "version": "0.02", + "version": "0.03", "description": "A digital clock that uses the built-in vector font.", "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, - "screenshots": [{"url":"bangle1-vector-clock-screenshot.png"}], + "screenshots": [ + {"url":"bangle2-vector-clock-screenshot.png"}, + {"url":"bangle1-vector-clock-screenshot.png"} + ], "storage": [ {"name":"vectorclock.app.js","url":"app.js"}, {"name":"vectorclock.img","url":"app-icon.js","evaluate":true} @@ -4106,9 +4209,10 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.07", - "description": "A Configurable clock with custom fonts and background", + "version": "0.08", + "description": "A Configurable clock with custom fonts and background. 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"}], "type": "clock", "tags": "clock", @@ -4149,7 +4253,7 @@ "id": "waveclk", "name": "Wave Clock", "version": "0.02", - "description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: This requires firmware 2v11 or later Bangle.js 1**", + "description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: Works on any Bangle.js 2, but requires firmware 2v11 or later on Bangle.js 1**", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", @@ -4165,7 +4269,7 @@ "id": "floralclk", "name": "Floral Clock", "version": "0.01", - "description": "A clock with a flower background by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: This requires firmware 2v11 or later Bangle.js 1**", + "description": "A clock with a flower background by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: Works on any Bangle.js 2 but requires firmware 2v11 or later on Bangle.js 1**", "icon": "app.png", "screenshots": [{"url":"screenshot_floral.png"}], "type": "clock", @@ -4280,7 +4384,7 @@ "version": "0.01", "description": "A touch based GPS watch, shows OS map reference", "icon": "gpstouch.png", - "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}], + "screenshots": [{"url":"screenshot4.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot1.png"}], "tags": "tools,app", "supports": ["BANGLEJS2"], "readme": "README.md", @@ -4309,7 +4413,7 @@ "name": "Q Alarm and Timer", "shortName": "Q Alarm", "icon": "app.png", - "version": "0.02", + "version": "0.03", "description": "Alarm and timer app with days of week and 'hard' option.", "tags": "tool,alarm,widget", "supports": ["BANGLEJS", "BANGLEJS2"], @@ -4327,7 +4431,7 @@ "id": "emojuino", "name": "Emojuino", "shortName": "Emojuino", - "version": "0.02", + "version": "0.03", "description": "Emojis & Espruino: broadcast Unicode emojis via Bluetooth Low Energy.", "icon": "emojuino.png", "screenshots": [ @@ -4349,7 +4453,7 @@ "id": "cliclockJS2Enhanced", "name": "Commandline-Clock JS2 Enhanced", "shortName": "CLI-Clock JS2", - "version": "0.02", + "version": "0.03", "description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design. Also added HID media controlls, just swipe on the clock face to controll the media! Gadgetbride support coming soon(hopefully) Thanks to t0m1o1 for media controls!", "icon": "app.png", "screenshots": [{"url":"screengrab.png"}], @@ -4367,9 +4471,9 @@ "name": "A Battery Widget (with percentage)", "shortName":"A Battery Widget", "icon": "widget.png", - "version":"1.01", + "version":"1.02", "type": "widget", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "description": "Simple and slim battery widget with charge status and percentage", "tags": "widget,battery", @@ -4433,7 +4537,7 @@ "shortName": "AuthWatch", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version": "0.01", + "version": "0.04", "description": "Google Authenticator compatible tool.", "tags": "tool", "interface": "interface.html", @@ -4462,7 +4566,7 @@ {"name":"schoolCalendar.img","url":"app-icon.js","evaluate":true} ], "data": [ - {"name":"app.json"} + {"name":"calendarItems.csv"} ] }, { "id": "timecal", @@ -4515,7 +4619,7 @@ "shortName":"93 Dub", "icon": "93dub.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.03", + "version":"0.05", "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", @@ -4533,9 +4637,10 @@ "version":"0.01", "description": "Simple app to power off your Bangle.js", "icon": "app.png", - "tags": "poweroff, shutdown", - "supports" : ["BANGLEJS", "BANGLEJS2"], + "tags": "tool, poweroff, shutdown", + "supports" : ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", + "allow_emulator": true, "storage": [ {"name":"poweroff.app.js","url":"app.js"}, {"name":"poweroff.img","url":"app-icon.js","evaluate":true} @@ -4545,9 +4650,17 @@ "id": "sensible", "name": "SensiBLE", "shortName": "SensiBLE", - "version": "0.02", + "version": "0.04", "description": "Collect, display and advertise real-time sensor data.", "icon": "sensible.png", + "screenshots": [ + { "url": "screenshot-top.png" }, + { "url": "screenshot-acc.png" }, + { "url": "screenshot-bar.png" }, + { "url": "screenshot-gps.png" }, + { "url": "screenshot-hrm.png" }, + { "url": "screenshot-mag.png" } + ], "type": "app", "tags": "tool,sensors", "supports" : [ "BANGLEJS2" ], @@ -4557,5 +4670,338 @@ { "name": "sensible.app.js", "url": "sensible.js" }, { "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true } ] -} +}, + { + "id": "widbars", + "name": "Bars Widget", + "version": "0.01", + "description": "Display several measurements as vertical bars.", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbars.wid.js","url":"widget.js"} + ] +}, +{ + "id":"a_speech_timer", + "name":"Speech Timer", + "icon": "app.png", + "version":"1.01", + "description": "A timer designed to help keeping your speeches and presentations to time.", + "tags": "tool,timer", + "readme":"README.md", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"a_speech_timer.app.js","url":"app.js"}, + {"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true} + ] +}, + { "id": "mylocation", + "name": "My Location", + "shortName":"My Location", + "icon": "mylocation.png", + "type": "app", + "screenshots": [{"url":"screenshot_1.png"}], + "version":"0.01", + "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", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"mylocation.app.js","url":"mylocation.app.js"}, + {"name":"mylocation.img","url":"mylocation.icon.js","evaluate": true } + ], + "data": [ + {"name":"mylocation.json"} + ] + }, + { + "id": "pebble", + "name": "Pebble Clock", + "shortName": "Pebble", + "version": "0.04", + "description": "A pebble style clock to keep the rebellion going", + "readme": "README.md", + "icon": "pebble.png", + "screenshots": [{"url":"pebble_screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"pebble.app.js","url":"pebble.app.js"}, + {"name":"pebble.settings.js","url":"pebble.settings.js"}, + {"name":"pebble.img","url":"pebble.icon.js","evaluate":true} + ] + }, + { "id": "pooqroman", + "name": "pooq Roman watch face", + "shortName":"pooq Roman", + "version":"0.03", + "description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"pooqroman.app.js","url":"app.js"}, + {"name":"pooqroman.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"pooqroman.json"} + ] + }, + { + "id": "widbata", + "name": "Battery Level Widget (Themed)", + "shortName":"Battery Theme", + "icon": "widbata.png", + "screenshots": [{"url":"screenshot_widbata_1.png"}], + "version":"0.01", + "type": "widget", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "description": "Shows the current battery level status in the top right using the clocks colour theme", + "tags": "widget,battery", + "storage": [ + {"name":"widbata.wid.js","url":"widbata.wid.js"} + ] + }, + { + "id": "weatherClock", + "name": "Weather Clock", + "version": "0.04", + "description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).", + "icon": "app.png", + "screenshots": [{"url":"screens/screen1.png"}], + "type": "clock", + "tags": "clock, weather", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"weatherClock.app.js","url":"app.js"}, + {"name":"weatherClock.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "menuwheel", + "name": "Wheel Menus", + "version": "0.01", + "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", + "screenshots": [ + {"url":"screenshot_b1_dark.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_light.png"}, + {"url":"screenshot_b2_dark.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_light.png"} + ], + "type": "boot", + "tags": "system", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"menuwheel.boot.js","url":"boot.js"} + ] + }, + { "id": "widChargingStatus", + "name": "Charging Status", + "shortName":"ChargingStatus", + "icon": "widget.png", + "version":"0.1", + "type": "widget", + "description": "A simple widget that shows a yellow lightning icon to indicate whenever the watch is charging. This way one can see the charging status at a glance, no matter which battery widget is being used.", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widChargingStatus.wid.js","url":"widget.js"} + ] + }, + { + "id": "flow", + "name": "FLOW", + "shortName": "FLOW", + "version": "0.01", + "description": "A game where you have to help a flow avoid white obstacles thing by tapping! This is a demake of an app which I forgot the name of. Press BTN(1) to restart. See if you can get to 2500 score!", + "icon": "app.png", + "tags": "game", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name": "flow.app.js", "url": "app.js" }, + {"name": "flow.img", "url": "app-icon.js","evaluate": true } + ] + }, + { "id": "scribble", + "name": "Scribble", + "shortName":"Scribble", + "version":"0.01", + "type": "app", + "description": "A keyboard on your wrist! Swipe right for space, left for delete.", + "icon": "app.png", + "allow_emulator": true, + "tags": "tools, keyboard, text, scribble", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"scribble.app.js","url":"app.js"}, + {"name":"scribble.img","url":"app-icon.js","evaluate":true} + ], + "screenshots":[ + { "url":"screenshot.png" } + ] + }, + { + "id": "ptlaunch", + "name": "Pattern Launcher", + "shortName": "Pattern Launcher", + "version": "0.10", + "description": "Directly launch apps from the clock screen with custom patterns.", + "icon": "app.png", + "screenshots": [{"url":"main_menu_add.png"}, {"url":"add_pattern.png"}, {"url":"select_app.png"}, {"url":"main_menu_manage.png"}, {"url":"manage_patterns.png"}], + "tags": "tools", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "ptlaunch.app.js", "url": "app.js" }, + { "name": "ptlaunch.boot.js", "url": "boot.js" }, + { "name": "ptlaunch.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{"name":"ptlaunch.patterns.json"}] + }, + { + "id": "rebble", + "name": "Rebble Clock", + "shortName": "Rebble", + "version": "0.02", + "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", + "dependencies": {"mylocation":"app"}, + "screenshots": [{"url":"screenshot_rebble.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"rebble.app.js","url":"rebble.app.js"}, + {"name":"rebble.settings.js","url":"rebble.settings.js"}, + {"name":"rebble.img","url":"rebble.icon.js","evaluate":true} + ] + }, + { "id": "snaky", + "name": "Snaky", + "shortName":"Snaky", + "version":"0.01", + "description": "The classic snake game. Eat apples and don't bite your tail. Control the snake with the touch screen.", + "tags": "game,fun", + "icon": "snaky.png", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"snaky.app.js","url":"snaky.js"}, + {"name":"snaky.img","url":"snaky-icon.js","evaluate":true} + ] + }, + { + "id": "clicompleteclk", + "name": "CLI complete clock", + "shortName":"CLI cmplt clock", + "version":"0.03", + "description": "Command line styled clock with lots of information", + "icon": "app.png", + "allow_emulator": true, + "type": "clock", + "tags": "clock,cli,command,bash,shell,weather,hrt", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"clicompleteclk.img","url":"app-icon.js","evaluate":true}, + {"name":"clicompleteclk.settings.js","url":"settings.js"} + ], + "data": [{"name":"clicompleteclk.json"}] + }, + { + "id":"awairmonitor", + "name":"Awair Monitor", + "icon": "app.png", + "allow_emulator": true, + "version":"0.01", + "description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.", + "tags": "tool,health", + "readme":"README.md", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"awairmonitor.app.js","url":"app.js"}, + {"name":"awairmonitor.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "pooqround", + "name": "pooq Round watch face", + "shortName":"pooq Round", + "version":"0.00", + "description": "A 24 hour analogue watchface with high legibility and a novel style.", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"pooqround.app.js","url":"app.js"}, + {"name":"pooqround.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"pooqround.json"} + ] + }, + { + "id": "coretemp", + "name": "Core Temp Display", + "version": "0.01", + "description": "Display CoreTemp device sensor data", + "icon": "coretemp.png", + "type": "app", + "tags": "health", + "readme": "README.md", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"coretemp.boot.js","url":"boot.js"}, + {"name":"coretemp.app.js","url":"coretemp.js"}, + {"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true} + ] + }, + { + "id": "showimg", + "name": "simple image viewer", + "shortName":"showImage", + "version":"0.1", + "description": "Displays the image file in showimage.user.img. Returns to watch face after 60s or button push. I use it to display my vaccination certificate.", + "icon": "app.png", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"showimg.app.js","url":"app.js"}, + {"name":"showimg.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "lapcounter", + "name": "Lap Counter", + "version": "0.01", + "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"}], + "type": "app", + "tags": "tool,outdoors", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"lapcounter.app.js","url":"app.js"}, + {"name":"lapcounter.img","url":"app-icon.js","evaluate":true} + ] + } ] diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog index 5fbfe4fa3..c1b2588bb 100644 --- a/apps/93dub/ChangeLog +++ b/apps/93dub/ChangeLog @@ -1,3 +1,5 @@ 0.01: Initial version for upload 0.02: DiscoMinotaur's adjustments (removed battery and adjusted spacing) 0.03: Code style cleanup +0.04: Set 00:00 to 12:00 for 12 hour time +0.05: Display time, even on Thursday diff --git a/apps/93dub/README.md b/apps/93dub/README.md index fd24d54d8..4d1ade582 100644 --- a/apps/93dub/README.md +++ b/apps/93dub/README.md @@ -5,7 +5,8 @@ Uses many portions from Espruino documentation, example watchfaces, and the waveclk app. It also sourced from Jon Barlow's 91 Dub v2.0 source code and resources and adapted for Bangle.js 2's screen. Time, date and the battery display works. It is not pixel perfect to the original. Contributors: -Leer10 -Orviwan (original watchface and assets) -Gordon Williams (Bangle.js, watchapps for reference code and documentation) -DiscoMinotaur (adjustments) +* Leer10 +* Orviwan (original watchface and assets) +* Gordon Williams (Bangle.js, watchapps for reference code and documentation) +* DiscoMinotaur (adjustments) +* Ray Holder (minor 12 hour time rendering adjustment, fix Thursdays) diff --git a/apps/93dub/app.js b/apps/93dub/app.js index 92544304c..1b0f69a94 100644 --- a/apps/93dub/app.js +++ b/apps/93dub/app.js @@ -78,6 +78,9 @@ function draw(){ } else { h = " " + h; } + } else if (h === 0) { + // display 12:00 instead of 00:00 for 12 hr mode + h = "12"; } //draw separator @@ -90,7 +93,7 @@ function draw(){ if (w == 1) {imgW = imgMon;} if (w == 2) {imgW = imgTue;} if (w == 3) {imgW = imgWed;} - if (w == 4) {imgW = imgThr;} + if (w == 4) {imgW = imgThu;} if (w == 5) {imgW = imgFri;} if (w == 6) {imgW = imgSat;} g.drawImage(imgW, 85, 63); diff --git a/apps/a_speech_timer/ChangeLog b/apps/a_speech_timer/ChangeLog new file mode 100644 index 000000000..b3aa9e0dd --- /dev/null +++ b/apps/a_speech_timer/ChangeLog @@ -0,0 +1,2 @@ +1.00: Release (2021/12/01) +1.01: Grey font when timer is frozen (2021/12/04) diff --git a/apps/a_speech_timer/README.md b/apps/a_speech_timer/README.md new file mode 100644 index 000000000..098c352f3 --- /dev/null +++ b/apps/a_speech_timer/README.md @@ -0,0 +1,16 @@ +# A Speech Timer + +* A timer designed to help keeping your speeches and presentations to time +* Vibrates 1-2-3 times and changes screen color within the target time range. + * Example for a 5 to 7 minutes speech: vibrates once at 5:00 (green), twice at 6:00 (yellow), thrice at 7:00 (red). +* Use the buttons to start a timer +* Swipe left or right to choose different target times +* Touching the timer on the upper part of the screen locks (or unlocks) the buttons to prevent accidental changes + +![](screenshot0.png) +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) + +## Creator +[@alainsaas](https://github.com/alainsaas) diff --git a/apps/a_speech_timer/app-icon.js b/apps/a_speech_timer/app-icon.js new file mode 100644 index 000000000..1fdb2c509 --- /dev/null +++ b/apps/a_speech_timer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP//kAj//AAP5/+PApH7//PAonvAoXzAonj//nApHggEHAoWAgA5BAAJCCAoU/IYIFCv///w0CAonrv/HAoXLv+DAogLFgPeAoV+nlOAoV4/8+AoV79+eFIVzAof7u/v5xBCs4FL84FE//O74FBu4FB64FD73TAoNz/+eAoV5IIIFCvl8vwFCv8A/wFDO4IFFFIQFCGoSVFUIqtDh65D/1vYof+Y4LLDw7dD/0ndIYRCeoQFC/P/z/+i///oFBGoX8gEfAgI=")) diff --git a/apps/a_speech_timer/app.js b/apps/a_speech_timer/app.js new file mode 100644 index 000000000..440cd92c6 --- /dev/null +++ b/apps/a_speech_timer/app.js @@ -0,0 +1,173 @@ +Graphics.prototype.setFontMichroma36 = function() { +g.setFontCustom(atob("AAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAAAAAAAAAAAAAAAAAAAGAAAAA+AAAAD+AAAAP+AAAA/8AAAD/wAAAf/AAAB/4AAAH/gAAAf+AAAB/4AAAH/gAAAf+AAAAfwAAAAfAAAAAcAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AP///8APwAD+APAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAeAPAAAeAPwAD+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAEAAAAAOAAAAAfAAAAA+AAAAB8AAAAD8AAAAH4AAAAPwAAAAPgAAAAfAAAAAf///+Af///+Af///+Af///+AAAAAAAAAAAAAAAAAAAAAAAAAA/Af+AD/A/+AH/B/+AP/D/+APwD4eAPADweAfADweAeADweAeADweAeADweAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAPgeAeAPAeAeAPAeAeAPAeAeAPAeAfAPAeAPw/AeAP/+AeAH/+AeAD/8AeAB/wAOAAAAAAAAAAAAAAAAAAAAAAAAAB8APgAD8AP4AH8AP8AP8AP8APgAB+AfAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAeAfAeAeAPx/h+AP///+AH///8AD///4AB/h/gAAAAAAAAAAAAAAAAAAAAAAeAAAAA/AAAAA/AAAAB/AAAAD/AAAAH/AAAAPvAAAAPPAAAAfPAAAA+PAAAB8PAAAD4PAAADwPAAAHwPAAAPgPAAAfAPAAA+APAAA8APAAB8APAAD4APAAHwAPAAPgAPAAPAAPAAfAAPAAf///+Af///+Af///+Af///+AAAAPAAAAAPAAAAAPAAAAAPAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAf/8PgAf/8P4Af/8P8Af/8P8AeB4A+AeB4AeAeDwAeAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAfAeDwAeAeD4A+AeD+D+AeB//8AeB//4AeA//4AAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AH///8AP4fB+APAeAeAfA8AeAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAfA8APAPA+AeAPgeAeAP8fh+AH8f/8AD8P/8AA8H/4AAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAACAeAAAGAeAAAOAeAAAeAeAAA+AeAAD+AeAAH8AeAAP4AeAAfwAeAA/gAeAB/AAeAD+AAeAP4AAeAfwAAeA/gAAeB/AAAeD+AAAeH8AAAefwAAAe/gAAAf/AAAAf+AAAAf8AAAAf4AAAAfgAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAMAAB+B/wAD/j/4AH/3/8AP///+AP//A+AfB+AeAeA+AeAeA+APAeA+APAeA+APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA+APAeA+APAeA+APAeA+AOAeA+AeAPh/A+AP///+AP/3/8AH/3/8AB/D/wAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAD/4HAAH/8HwAP/+H4AP5/H8AfAfA8AeAPAeAeAPAeAeAPAeAeAHgfAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHAPAeAPAOAeAPAeAPAPAeAPwfB+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAAAAAAAB8DwAAB8HwAAB8HwAAB8DwAAAAAAAAAAAAA"), 46, atob("CBIkESMjJCMjIyMjCA=="), 36+(1<<8)+(1<<16)); +}; + +Graphics.prototype.setFontMichroma16 = function(scale) { +g.setFontCustom(atob("AAAAGAAYAAAAGAB4A/APwD4AeADgAAAAAAA/8H/4YBjAGMAcwBzAHMAcwBzAHMAYYBh/+D/wAAAAABgAOABwAGAA//h/+AAAAAA4+Hn4YZjhmMOYw5jDmMMYwxjDGOMYYxh/GD4YAAAAADBwcHhgGOAYwBzHHMccxxzHHMcc5xhnGH/4PfAAAAAAAOAB4APgB2AGYAxgHGA4YDBgYGD/+P/4AOAAYAAAAAD+cP547BjsGOwc7BzsHOwc7BzsHOwY7zjv+APgAAAAAD/wf/hmGOYYxhzGHMYcxhzGHOYYZhh3uDP4AeAAAEAA4ADgAOAI4DjgeODw4eDjgOcA7gD8APgA8AAAAAAAAAA58H/4bxjmGMYcxhzGHMYcxhzGHOYYbxh/+DnwAAAAADxgfnBnOOMYwxjDHMMcwxzDHMMY4xhjOH/4P/AAAAAABnAGcAAA"), 46, atob("BAgQCBAQEBAQEBAQBA=="), 16+(scale<<8)+(1<<16)); +}; + +function timeToString(duration) { + var hrs = ~~(duration / 3600); + var mins = ~~((duration % 3600) / 60); + var secs = ~~duration % 60; + var ret = ""; + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); + } + ret += "" + mins + ":" + (secs < 10 ? "0" : ""); + ret += "" + secs; + return ret; +} + +var newtimer_left_from = 60; +var newtimer_left_to = 2*60; + +var newtimer_right_from = 5*60; +var newtimer_right_to = 7*60; + +var current_from = 5*60; +var current_mid = 6*60; +var current_to = 7*60; +var current_value = 0; + +var timerinterval; +var istimeron = false; + +var islocked = false; + +function countDown() { + current_value++; + draw(); + + if (current_value == current_from) { + Bangle.buzz(500); + } else if (current_value == current_mid) { + Bangle.buzz(400).then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 800)); + }).then(()=>{ + return Bangle.buzz(500); + }); + } else if (current_value == current_to) { + Bangle.buzz(300).then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 600)); + }).then(()=>{ + Bangle.buzz(300).then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 600)); + }).then(()=>{ + return Bangle.buzz(500); + }); + }); + } + +} + +Bangle.on('touch',(touchside, touchdata)=>{ + if (!islocked && istimeron && touchdata.y > (100+10)) { + Bangle.buzz(40); + istimeron = false; + clearInterval(timerinterval); + } else if (touchdata.y > 24 && touchdata.y < (100-10)) { + Bangle.buzz(40); + islocked = !islocked; + } else if (!islocked && touchdata.y > (100+10) && touchdata.x > 88 + 10) { + Bangle.buzz(40); + current_from = newtimer_right_from; + current_to = newtimer_right_to; + current_mid = (current_from + current_to) / 2; + current_value = 0; + if (timerinterval) clearInterval(timerinterval); + timerinterval = setInterval(countDown, 1000); + istimeron = true; + } else if (!islocked && touchdata.y > (100+10) && touchdata.x < 88 - 10) { + Bangle.buzz(40); + current_from = newtimer_left_from; + current_to = newtimer_left_to; + current_mid = (current_from + current_to) / 2; + current_value = 0; + if (timerinterval) clearInterval(timerinterval); + timerinterval = setInterval(countDown, 1000); + istimeron = true; + } + showInstructions = false; + draw(); +}); + +Bangle.on('swipe',(swiperight, swipedown)=>{ + console.log(swiperight); + console.log(swipedown); + + if (swiperight == -1) { + if (newtimer_left_from >= 60) { + newtimer_left_from += 60; + newtimer_left_to += 60; + } else { // special case for 0:30 to 1:00 + newtimer_left_from = 60; + newtimer_left_to = 120; + } + newtimer_right_from += 60; + newtimer_right_to += 60; + draw(); + } else if (swiperight == 1) { + if (newtimer_left_from > 60) { + newtimer_left_from -= 60; + newtimer_left_to -= 60; + } else { // special case for 0:30 to 1:00 + newtimer_left_from = 30; + newtimer_left_to = 60; + } + + if (newtimer_right_from > 120) { + newtimer_right_from -= 60; + newtimer_right_to -= 60; + } + draw(); + } +}); + +var drawTimeout; +var showInstructions = true; + +function draw() { + g.reset(); + if (current_value >= current_to) { g.setBgColor("#F00"); } + else if (current_value >= current_mid) { g.setBgColor("#FF0"); } + else if (current_value >= current_from) { g.setBgColor("#8F8"); } + g.clearRect(0,24,176,176); + + g.reset().setFontAlign(0, 0).setColor(istimeron ? "#000" : "#444"); + g.setFont("Michroma36").drawString(timeToString(current_value), 88, 62); + + g.reset().setFontAlign(0, 0); + + g.setFont("HaxorNarrow7x17"); + g.drawString(timeToString(current_from), 44, 62+26); + g.drawString(timeToString(current_mid), 88, 62+26); + g.drawString(timeToString(current_to), 132, 62+26); + + if (current_value >= current_from) { g.drawRect(44-1,62+26+9,44+1,62+26+9+1); } + if (current_value >= current_mid) { g.drawRect(88-1,62+26+9,88+1,62+26+9+1); } + if (current_value >= current_to) { g.drawRect(132-1,62+26+9,132+1,62+26+9+1); } + + if (showInstructions) { + g.setFont("6x8").drawString("Tapping timer locks buttons", 88, 100+5); + g.setFont("6x8").drawString("<= Swipe to change time =>", 88, 168); + } + + g.setColor(islocked ? "#444" : "#000"); + g.setFont("Michroma16"); + g.drawString(timeToString(newtimer_left_from), 44, 138-9); + g.drawString(timeToString(newtimer_left_to), 44, 138+9); + g.drawString(timeToString(newtimer_right_from), 132, 138-9); + g.drawString(timeToString(newtimer_right_to), 132, 138+9); + + g.drawRect(0+8,138-24, 88-9+1, 138+22+1); + g.drawRect(0+8,138-24, 88-9, 138+22); + g.drawRect(88+8,138-24, 176-10+1, 138+22+1); + g.drawRect(88+8,138-24, 176-10, 138+22); +} + +require("FontHaxorNarrow7x17").add(Graphics); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/a_speech_timer/app.png b/apps/a_speech_timer/app.png new file mode 100644 index 000000000..1eb777fa7 Binary files /dev/null and b/apps/a_speech_timer/app.png differ diff --git a/apps/a_speech_timer/screenshot0.png b/apps/a_speech_timer/screenshot0.png new file mode 100644 index 000000000..ee3ababc1 Binary files /dev/null and b/apps/a_speech_timer/screenshot0.png differ diff --git a/apps/a_speech_timer/screenshot1.png b/apps/a_speech_timer/screenshot1.png new file mode 100644 index 000000000..69ea91e95 Binary files /dev/null and b/apps/a_speech_timer/screenshot1.png differ diff --git a/apps/a_speech_timer/screenshot2.png b/apps/a_speech_timer/screenshot2.png new file mode 100644 index 000000000..fd511e0f6 Binary files /dev/null and b/apps/a_speech_timer/screenshot2.png differ diff --git a/apps/a_speech_timer/screenshot3.png b/apps/a_speech_timer/screenshot3.png new file mode 100644 index 000000000..7b67b6f01 Binary files /dev/null and b/apps/a_speech_timer/screenshot3.png differ diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index 03e920a9a..f5638fdd2 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -9,3 +9,4 @@ 0.09: Actual Bangle.js 1 pixels as of 13 Oct 2021 0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021) 0.11: Bangle.js2: New pixels, btn1 to exit +0.12: Actual pixels as of 29th Nov 2021 diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js index 32e5bafae..978d36193 100644 --- a/apps/about/app-bangle2.js +++ b/apps/about/app-bangle2.js @@ -6,7 +6,7 @@ var ENV = process.env; var MEM = process.memory(); var s = require("Storage"); -var img = atob("sIwDkm2S66DYwA2AAAAAHAHGSRxJEkAAgmGGBxDIADIdAFJIbAHF9HP00kBUC6DtzDgAgGOxwkgAGbA86CW2222kkgB4hO26/XDDwAwkEEEgYYA+VW22wEAAggwAG2AZZZTFotMIDAA9vB520AJUnXAtwAgAgGxOw2wo+bAmiSAH4AQUkAHMkO2/66TY2GwgggghB5/+SRxJAEAAlm2ABxADLKYFFFBADA/99HP00kHoC6DuzAAAAGOxwkg+uzG86CQH7bSUgAB+iSQAAADDAAtEkEkAAAA2khxIAHAAgmGLADIDLLoAAAEDDSQQCAAAAAHA4AAuwAAAAADDDDDAwAIIAAMgAYQUAAA4iongAAAGABqEkkkAHHGGhhxIHHXa66ADYbAACcEHzUBDbQCSSQAAAAHAttDDDDAAAADDDDA14GGGABEEAYQWBAIDiQ84AAowwIYQkkiS4g42khxIA4inNPAA1wAATkkABCSAASQQikm2SQHAFAAAGwAAAAotoouJwAIIABEgYYAAJIIoCI84AFt2wBCAEkbYEEHPABxIAAfSqqSQ1wACcEEAACSAAAAAggmACtv/91gGgEwH/AtoFFG2wGGAABEEDYAAJJAtAI2Gw9twwwYAAm2AH/55AR0k0RAHNPKo2wAT4AAoFCShYCSAgkmwCoAAFWoWgEyCSAottoub2GAAAAMgAAAAJJAFCAwww9FAGwA49th5Ag/PER22T/AC66KoBGCd8kksAEATQCAAggmFCtsnFawGgEwEkAAADbutwGAIBIAAAAAJmhIYAAwA35xAg2249t5PQA4AERySM+4kAEikAAzo+22vJPAZgCCEAgm2CogAoFGvGwBJAAAGADGGGGxBBAAAJBBMkkIASQAA5+2EE0zb9tn/QH4uAR1to/An4kkkgCeA9ttsAEAAACSAAAAAAogAooAGAABAAxwGADGGGAAIBIAAIBBMkkP/QUkAH5xAnGLD4AAUkQQuQRtSV4AEAEkkATow4AAAAASjDFCSAAAAQAgAtAAoAABIAvgGDDG2GAAAAAGwJBAJkhPJSG2CSwAACTzb4EEAgGCuQQty14HkAAkggdAG4AQAAA0zDFCAC2wDDAEnooH19At2AywGAYGGAAAAAAwAAAABMJH/QQAACGAASFYA4DjAgwCuCAtS1oAABJEEDbbbbbbYAAiTbtqVCbYQQQAAoFAAoFAGAQAG2GGGGwAAAQwwAAAkhIGmLTIkCwDAC4PIAAAAgAQuQAtJNoAABAEn/JIAIMAEEAADDFCAFAoDDAAAAAAAoAu2SRJAAIAAAAAAAAwGAAAkgAEkNK6iCDDAAA+PADbAG2AuCAtJVoIopSEz7JAABEgEgAADDFCAAAAAQEEAAAAAoAFAQQIAJIAKcu4AACGwAH/kn/GmjaSkSAYAYAJ4ADrAAFAuAQFKNAAtpBmXvBNBIMkEEAADDFCAAJJIAkkgAAAAFFoASQMghIAAAAAAACAAA/H4/HAAZK6EAAAAAAAAADbDYFAuACFrtowFBMydIBAABEgG22AAAEsHAALbAkkgAABJACQSAFIgg8k8/kn8AACaAAJ/4AGoQRYkEAHA/+AGAAAYAAtAwAVvdu2ABmToABIAIMAwAAwAAggn/4LAAEkAAABAIQCAVlwHA+22+++3AAwH/BxJAYEAgHHAA4AAd2w22EADAAAAAAqo7owAKSdIAA44AAAGG2AAAEkA//LAAAgAA4BAISSAVu2kA6q6666XEkwH/BIIDbDggn4EgmwAMWGGGEAAYAAAAEtsHdoAjTpwAAEAAAAGAAAbbYgHI4LFdHHC2ABBAQCSlo3cA7b7777fEAwH/IIAbbaAkAAskGGASWEmwEgbEHEAAAxOtFpJzdIAAA44AAAA2wAAYAABJLbDDH4CGABJAQCklAAkA/9999v9EkCaAJIADbBoglttAmwGQWgmGE84H//AAACQAAAhRpAoCA84ACSSwAAYYoAHI4AFdAACSJJJJJJklAwkADAAYAGAEAAbYMIAAYAAglttAGGGSQkgAEmgA44SSSSIEAAadIAgkg/gACJKGAADYAACQAAHXAAAYAAAABMkoAwAADADAQ1wEADFAggAAAADL1ttAwAwCAggIE84CHAWy2QLoAmbpggYggkgttJKAYAAAAAVqBgSSS/kIGAwwxJkoAAAADAYAQGAH/DuokgAAA2AatttoAAAigwBBAAAB/4SSSQJAEyd84YQkgAAACJKJJJkkAAQCAnS6S/kYwwwwx5koAwAADD2QQAGHXDFDlgAAAwxVlttAkgEkkJAIwAABkgSSSQLomT5/gCIDADAACSSQQQ//AACQAgWAS/kI2w2wxJkoAwAADe2CAAgg/DADFAAAA2CLM8loBgAggBAAggQAAASSSQIEydI84IADDDHXAAAAAASSAAADbAAAS/kYwwwwB5kNAAAADe2AAEGEHAbYlAAAAwzZf/9ABgAAAGGAbYAAAGAEbAAmTsAABJADYYH6AADbAAAAAAMIY2w2w2kIwwwwwJJNAAAADDwAAgGAgAAEFYAAAAELM8loBgAAABJAggACAAAEYYEydJMJEkHnAACSQG2zBSJllIJLQGAwA2kAAAAAAKSIAH/ADAYASHMbtbh4kAAAAwFJYAABBgGWWWWAAwAAAAEEbAmTpBJJEEEkGJwAAA2YbAAlAIMLb0w2wEkEAgnJJKSIA4A4DADACHMkkkh4HAAAASGTJJIgIgAAAAAE8888G2AgYcydABhhABHnGkwQCA2YYAAAJxIAAAAwAEkEgg/O2LJIAoAoAAAFksAkEkkHAHAABJPYOuIEkAAAGOEkQAAAG2AEkmkhABJJAACSBkIAAAwAEggAAIAH/khJAEkEEgJO1NAAFjDlAAGAsgAgAAQAAAAwxJOgNNIAADbIBxAgQEAkG2AEEicIAEAAAAki0AACAAAAEEgAAIAH/khJA44EAgA2FtAAFkclAGGGEAAkACA4HJ4AAJFoFtAA/ADIGOAiQEEAm2AEmjkkJ0w/4ACigwCAAUkkkkgAAAAH/khJAHAAAAGwADAAAckeAGGGAAAEAQAAIAACABJwFIAAADDAACQAZYkEmQQEylIBN01/4AAgwDASSH//8kgAAAAA4EAICXkG4E82AbAgkjjGwAwwAAAkEkkHAHEkkBJIFwYAHAYkkgeYLIAAWSQkEhABJAEgAAGAgYYEgBJJIkAAAAoAAAAAAXgHOP/AwAA//4Y2AAA444AAAtoSXAEEEBJQEGYAHAAAgQYYZfA4XQUEdgAAIgggAAAmAbYEAAAAAA2bk9oA4EHNtHAm008AIAAJJIAGySQ444AAAAoQAgAAAAcgEAwAHAAAgCkgAAHHSQmUpgAAIEgAAAAAAYZEgE8H/AGAwACQAAHHBA2HOIEWAAAPkkkgQAQEAgAAAFASAb2QQAjYYYbfHAAAgAEGwHAAAEycIgAAAgjCDAESQAQAgE8EkAGGEMQCAAX9pIwG4AAiAAAPn//4AAQgggAAWVAQAf2AAAcAEkAA6SSQgAEGBJHHAmkhAAAAEgg44886AHXEg8kA/4GwBAQAAAXHBA2AAACEAAAPhJJIQCAkgh5DADASW59QQAjSQiAACQCX/AEAxAHBAxkMdGA2AGADAn/iAwQwAAAH//GGBAQSAAXFAAAooAIAAAAkkgAAAAAggnHo2woAwwAAAAcQAiAAaVaRJAkmxIHBhRhTr2wwwwwII886HAAHAAAAkgGA0MQCAAQAAAAtooAAAiA//4A4AAAAAB5gwwgEGEAFAgjSACQAqTCXPAAABAHBRdIUd2www2wCAHnSCCCAAAAEEEAAABCQBJu2AAYoooAAEAQJJISSQAA444AAwwAAkgAbYwcQDbAFYdAHPAAABAAhJpASw1N2AwwAAzbAAEUQwAA9gg4AAEAAABoBADDAAHHAAiAAXn/AtoAAAAAAGASQEAADAQjQgYAAEEEAAAAAHAbafIJIwAIAAtoAG2YBJMkQ/4H4EA/AEIAABJohCDbAHAwAAgQA45JA2wAAAbYAAAACECAYYAcAYZBJA2wAwAAA64bb64IwwAwAAooCCoSQJMUQkgtvAHAbYAAGxAoACDAAA/4AACIInUkbbYAADDDbbbYAUQAAAhAAbfB5EEEGGOO64AbdAAJGSQoAAtoCSoSRIAAAwAto/4AA4AAGGJbbaAbAA44AIIIIQBIbAAYADDDtttvHEAAGA+QkSvB5AAAAxxxSQtbdoAIASQwAAoAiCtQQP/H4wAtvgnAw4SAGGAAA/HAAIAAABAII4kkAAAD4AbY///77EXNkgO8gSIAAtoACAO064rbboAIASQAAAowgAE0BPJJ4DYf8EE/bYSQGGAAHA5AAIgggQQJIBJJIAAAYAPItttv/EzjEAAUkRLADrokiQMAAbbbbbYIwAAHgDAmgAGOADAAYDbY4gg4AAQAGwC8A/HA/JEEACAEESAAttAADA/4bbbY4AIAggIBkJLtrto2wQOmAcjjjkYAAAAHkJY0wDE0AB//IAbAAEmRACSAAAC8AggA/CqqAggGmSAFAAoAAYPIAAAH/HKXIA4AkALmDgAAkIIkmcktskewwAAH8JDAAbAAAAAAAAAAAkyNgASFo/68EssATAVWGEwBxSAo2wFAAAAAAAAHH4R4FtkM/2IgwlllhjbEybbbbbeAwAggjYAbDAYAAAAAAAACCWRtwoQQt4CEEssA/ACG2GAAISwowGFIIACCQAAHHAAgAoUkA/Mm2kEAkI4ADpAAAA2CSwggjDEAYAAAAAAQAQbaSQAIIoCAo/CEAjgA/AABxAwAASAowGHHHACCAAS8ADAgAoMM/tAwwwFAAA8AFIDvrAACCAEMDYxwAAAAAAASCQtqCFtJIAYYY4CEAjgBJAHAwHGAASAo2wBABASCQAX4AYE8ooRJkAGww2EAAAAABADvrAACSABhDEIMAALJBAAQQQ2wACSILDDDD/4AAbYBJAAoAowAASAowAvHHAAAAA/gmxJ/ooJAQAWG2GFAAAAAIIDvrAlKCAd9bAxwAALIYDAAAAAAAFtIIYAAAAAAAAAG2VRPv4AAASYFwFAQAAAAAAgEAwIntoRISCWwA2H44AAADADvrAlLApscpAEAhgLJBAAAAABJJMkAHPAAAHnSQAYe2VRpvvAAASOGtoAAA4HAHBIAARIn/4JAQQQ22wHHGGGGAYDHrHlIooskoAAAIAAAAAAYAAAAAH/kh5CBEBJSCAbEkSRNv9HwAAIAEA84HHAAAwBAACCSHgR/QAQAAAH/+wAwCAAAAAlLFoFlAwAAgADCAAD7AAH//8k23PAAAHnQSAbEkVRpvvA+ggW2kgkgH/AEA2IAAwAEEAJAAAgAAHFtuGAwIQoAA4lLAsEojwEAIADUQADAAAAgggAAAAAAAAACSAYYAVRNv4HwggQEEE84HHHAHwBAAAAAgAoJBYAwE49AAAAAeAoAA1tAAggggwgghgbSQSQYAAB444A/4D///7AAAQARAIASAAAAEAAPMAAAAAA/8hIAH/AkkgGADIAwC44tAAAAIwoAA9tEgE0A/wEAAAbQQAQDA1Ak02w8kAEkkAACQAABLICQAPgAAEk58AQAbbYAm0AA6S4ggg22ADA2249AAgkAYYoAAFtm0AgwwEikH/kn/AT7G1uGww288AAEAAkQCAABZICQkkkkkG2PIAAAYYYEGwgHyS3AAAGABBA2KHDrYwwwIwoAHkgm0AGGGACAGJ03nQQYASA2222/8AAEYYkgATAEkkAMkkkkkHGwEiQAbb8822A+yC24AgAADIAwCAAYGwAADAoBDWQEgAGGGACAEk03/CDAAQAAAAA8kDEEbYtgkKIAggBBkkkkkH+wEtVAc0f/zeA+QC24EAEkAAEQGgDbYA23AHoAHQQCAkkmACCCAAAAFteYGwoAAAAQAbckYYJgERAAgEPHIAAEkHGAEtVA/H88DDA+QW24EgEk/4AAAAABAIkwAAoBYEASACCAAASQAAIANVbYwAtbAAC6DYbAAAJEgAAAAAIAIAAEkAAAEiQA//4AEwA+yW24EAEk44IBBIJAAAA3HHoA4AACQCCACAAAHABBFtYYwAtbAHXXXbYoAoAAAAAAAABJwAAEkAAAAAAA//4AEGAf2W3EkgAA/4AAAILBAIwwAwAAoHACAAAQSQQDA4DLAQAAwAbdoAC6ADAoAoAAAABSIAAAwAAEkABIAFFtP/IAEGAb//4AuoDYA4IBBIIZAI2wAIAA4AAAAE8QCAQrACDDCSkAGwbiIAAQ/4GFFBJAAAB/I4A4wAAkkABB21FAJJIAEGAb4/4AywYDkgIBAAADJAkgAQAAAAAoAggiSSATA4AACCAAAAb1gAA484w1FBBAAABSIAAA222EkABIJNFo5J4AAAAb/A4ALIYD0wIBAAAAY4AAQwgHAAAAEAAESQATHAX/CSg0AAbKYBAA/4/woBBAAAAAAAwADMQSkQBBkltA4A4AAAAbH/AAAAYDkgBIAAEADAAAHPAAAAQAgAAAgAAtAAGACCA0D4bbABrwAG/3HIIAAACAAgAgZiCCCABIAAAA//4AIImTA4AAAAAYAAC4AAkgAYgEAAAACCXvAAA/n8/k8Q2wCCk0HYbbABrYAw2CSAAAwQDAQEkDMCCCQAASDAYADewAJMyYAAAAEAAA//HCAEkkADEgAIIBCCrroAAHn8nn8W2wAAAAAAAABBtYggAkkg44ADFDAAgAAHDAAACADDbAYeGGOIAAAAAAkgFtA46HAkkggDcgAIIIKCAAAAAHk88n/Wx2AbkkkkkkhJBAggAn/g/4AAuoA/4AAHYYwFCADAYAYeGGOIAJaQwEEEFAA/HQEkkHEAAAAww2wQAAAAAHn88k8W22Aee22222wAA4GFdnvg/4Cd31akgCAHYYwFASbAAADeGmGEAAYQwAkgFtw4wwAkkgAA2wA2wwwAAAAAA/EAAAAW22AbAAPIH/AngAgjrn/g/4AAuoASQAA/DGAFtAAAAEAG050kAIaQAEEEFA22wowEkkHA2AAAAAAGAAAAA2E/8ro2w2AYYA/44A4/4Agldkkg/4ADFDAoEkkAoFEEkkETA2DAnOgggJYQwAAAFAwwwwAAkgAAAH/4wwwGebbAA2E88dY2AGADAAPI4H4nmwABJAAA/4AQDAQoAAAFAkEkkkkjEkYA5AEkAAAH/AAAAAAAAuoAEAAH/44HwwAG222AA/E/8rowmlFtoAAA444AGBBBHAAAIAQACAAobYAqVFE0kk0TGzAAAAAFtCSAHAAAA4CCAAAA/AAHAH4H2wwWebbAA+glVGgm21tFEkAAH/AAGxIBBAAkMgCAB//oeYFAgFEGkmcjEaBJgAAFtCSA4AAAAFQQQwig4k2HCA4HwwwQAgAAF+giKGEGmlFFAQQSAAAAGBBBJAAgIiSJJJJobYqVAEEA0zcTDyBEEAADbCSH2SQEECCCAARQ/gYYVQ/4IAAWAggAA2glVGEGkk64ASAQQAAAAAAHArrAAAAAAkkoYFAhttEDebcjfyBIwgAttoA4AQAwwwAQAAigAEbCACBJBASSQkAH/3gggG/+AgigAQASAAFtAAAA4trAAwAwAAAoYqVZusEDebcTDyBGEAABJ85JGSAQAXS2kFAHYkAAH////AAVtggHfkAEEk8ggg64EgYQQQArA64/4rtAABBAwEgoFAoZtt8DeAcjAaBAgAEBJ85JAQADDBS2kFovD4HoskkkkEAVAgEH/AAAAA/4kgDYYAAAAAArYURxIAAAABhGGEAoqUAZus8YADETADBEE2gh5855AQAAoGS2kFFHY4Hov////EAVFAAkAbAQAQBIkAYDAA//AJIrY6+2wEkgABBAwH4tAoAZtt8YADEjAAYAmm0B/8/5AAAAADAAAFAHDFItoAAAEkAVtAAggYYQQQIAggDYYA8nAIFtAABxKSkjYAAQBBAoEAAbYE8DYAcTAADAEGAAAAASSAAAAAAwAFAHYBoooAAAERJIAAAkAbGyCGxIkxJgAA8ngJIAAbYAAQkgDAkgABIAAHIAABJAAAAAAAAACAYYY2w//AAAAAGwAAAAGuAAywAFEhAAAAAgAAG222wAAxICA4/8gAI44Aa8QQgAAkonAoHHH/P///3/H/H/H/HASAbYYwAJJJJIAAAwAAGwFtAIWQAAAhAH/9AggAAG2wAAJiCCAAAAAJIAAYYASUgAAYCADAAAAJJJIAwAAAAAAAAACAYYY22AAAAAAADwYAzGGuHGywAAAhJA4AFkAAAA2toAJBJJAAA4AAEAEbekDYbAbYAGH/4DD/P/4AADDAYYAAAAAAEkkgw////4AADzYAwGAAQgQAAAAhAA/FogAAAA2oFDbFAF22wAAAEEEAG0GGGAAYkgHAHDYbkgCAIYAL47AAAGwGEEAgwAbbbYAAbzbYGwAAGHAAFAAhAA4SWGIJAA2oFYAFotAwAAAAAggAAAtttoAY4kHAADDDmgQQAAoAbYwQAGwwEkggttttttpJAAAAOIArDIAAFtAhAY4CG2BBSQ2ooYAFFFAwAHAAAAAAAFEkkFAYEgH//4AAkgGAAYAIAAwQAGGGYAFo223///5JFAAA2wFoYPIHFFFmsA4SWGABRI2AADbFFFAwGgA44AAAFAm22goEEg////BJBIwzDBBAAA0QAAAAYDAAbbdtttpJAto2OJ2rDPIHAFtiFAYDAAAASQEkkAAdFFAwGgHHHHXAtEyaa0FAkA////A4MhwwgAABbZwQAAAAYDoFAAbbbbaSFAAxwB2QAJIAwFFhWDAYG22wJIEEEDbFAFDwGgA4446AAm7rr+goAA/H4/HHBIGoArbDADF+AAADYbFoAAAA/kiCAH/2/5KSk/IAwAAgAAYYAAAxawEgkA/4AAYYAYHHHHXY4ndllfgoAw/4H/AAG2AFtAYDGDF+SQQAAAAAHIPHH2iQAH/3//4AAJIAoAAgJAACAG2xW4A2wAXQAAYAEEA4466YAmdklegoAGH//4AAwAwA4AYDADAGSQEEAAgAAAAAH0iCEH/3//4AG54AoAAbZAAAQGGxawACAA/QAADAIYPHHHXYomTsrWguwGA//DTGSreAAkgBbZEkSQEkAAgAEAEAH2iSnn////t2wJIAgAAYZQSSQAYwJIACAEAAAGAZAgB4464YogydawgoGwAAACqGAoGHHkgAAA/n4AEEAAgAA2wEHkgAEH//n/9tAAAAgAAbYQQAgDbAAA4CBW2GAGYYAz3HHHXYFEGTWEFGzbAAADTASrYAgHDDA3///wEEAAgAAAAAAAAAYT/8k/4AAAAAYAAACQSAAAABJIAIOOwGAGDAAbY4464YoogzwksADbAAAAAAQAYHHHltA/k8n4B8AggAACQCAAAAYXf/n/20kkkgRAQAAGwGBJABAIAIRuOo2wDAAz3HHHbYAFEGEBBADbHJIIIASrYAAHgoG/kkn+B8AkgAJQCCAwAAaHf/n/km222xAKQAAwAGBhABIIBMH3DwwkQgAAA4444FAFogmoIwAAHPJIIAAAAAwAgoA38k/wAFllllJICCGGCWAH//n/2wAAABBCQAAGEmBJFoAABIoooOwgPoAddoCEkFFFAEwwAGwAHJIIJBAAAG2AjDAG/n+AAAAAAAggQCwAyQ13////4AAAABICQDAA0mBAFqABIHCSH2wkgw4FDCCHnAooAoGAAAAAwFttGAAAAGGAkkkkn/ttrADAAAAAQCgAiCtv////4AmbYBBCQZY2AGAAACABIoqSookEAAADFCSEnAAAAFigAAAMEAFA2wAAA/q6gAAAA4AAFBBEAkgEAAgAgS13////4A0cYBAIDLLAAAAAkCABAHCSHCAkBBHAJKCDbFAoAAEEgkIMkAFAGAAAAvq6DAAAYGPOFAAEAggEQCGGGSbf////4AmbYAAABZZA2wEggiQQQAowoAQCBIHBICIDAFFAA2GEEENMEAFAGAQQYtqSwwADDAHAFCCEYggcAAAwAAfb////AA0BJA9FALIAAAggkACCkEnAHACopB/JJJJDbFoDSww0AEAIwAFAkgSTYAgAwwgjbGIOFBJEYkgcAAH/BpbbAAADAAmIQHFoABHMkgEAggAAgkgggEgEAAG222PvvFFCAwwwBJJIAAAAAAQQYAgA2wgjbAAAADAAbADYAUg4FFkjAAADgE0IQA9FAABMkgAAgEAAkEEABJJJAA2GG2PvvFAoSDADBJJAAGWAAAoAAAAAwwAAAAAARP8gYYYYAWg4FIijbDDbkEmQSQAAAAEkkgAAAAgAAAAggAAggA2222PvvAAAADYbBJJAACiAAAooAgA4YDH/CACCBP8gbADYAUg4BFkjDADDgk0gQQkgAAGAAAGABJg2AAHEHAAEAF2AA1PvvDbAYDDDYBAYAGWAAAoEkA/ADY//6CCARP8gSQAAAAAAAAtrADDDwAmgSQAkkkAYAAFGSIkGAAASkkAggA+228N//AYDDDAAbBAYAbYAAAov/gA/DY446HCCAJJFQFAEkAAAMgSrDDDDSQMEkB4AAwGAAAouWIIGAAAW0kA44AFotBJASbaQYDAAGxYYYYYAAos//8HAYD446A6AQABFQFAEAgAAIESrDDAbQBIoQYEgGAAYAAFCWJBwAAASkkAIIkQDAADbabaAA2AAABbbYbYgAtt7D8AAto//6/6CAABFQFAEAgAAIEtobAAgSIIGABAAAAGAAADH/JBAAAA4A4ABAigFAASTiaahABgdoBAAAAFtAAAgAgAA/oH/CQSAQIBFSVAEkAAJMgJIBJEgRAIqSA4gAAAYAAYfnAkgHHAAH/AIIkQBQAATjTThABgYqBAAA2ttoAAFEAJA94A+2SWywBIFtFtEAgAAAA54BAggAAECABkgAAGAAADH/AgEnHAAAAAAAigoDAATcccZhhgdqCH/AwssoAAAwAIA/4AGACGAQ4w4w4w4w4AAAAJIBBkkAAACSP/4AAAYAFvAAAkggAGAAAAJABOAEAATbjjYgggaCSJJA2ttoAAAAAIAAAAGGAGAwbbgEJMMAAAAAA2wBJAgAg5//J/4AAGAAF/AAAAgmWWWWLIJGBIAoAQQbbbAEAkiSA//AwllgAAARAJAAwAA2AAGwbbAAG2wAG2wAAOIAAAAEEPJJJP8kkAEUlvAJIkggwwwwLIAGAAGAACJASQYAAAACAIAA2EkAAACIABAAAAAFAAAADAAAwAG2wnOAAEAAAAX4E5/JJJ8nkFEigAA/4AEgwxwwLIAGAA4AAAIIAQb7AwASQAAAAgAkgARIEBAAA4AAAAAHHHEn8gA2AIgAAggAQAH4gAAAJJM/8tEkSAAtqSJAGAGAAAgAAgIEAAIOCG4GAAGFwAAuAgAgACIBtJAAAAAEAA5IggEn8gGAwgIM3EACQAAEkAAbeecnkFEAQQ1wAQIIwxwwkgkkkACYAAIIQAYAwwwAQAGAIgAkgRABoAAAAAomgoxgkgEn8gADY5hg+EAAAFtAAH4YGmkkkAICCCGuCQIIAAAAoA222AAAAAJASQ4AG2AAozAGAgAAgIJItAAAAAAECA8gEAAAAAAoFQDPcggAQFFAQAADEgkngBxAQQA1wAJAAHgAFAkkkAFwQAQAGAD7AwAQQAAoAkgkgFEkoGAAoooAWSACAQAAAAAHAQYAxggGwFtDbbAAYAE//AIACAAAAAJIIk/AAoAAgAHIUkQAAABJAAAGAAGMIAAAArsJtwwAFFFFSSQSQQAkQ/9AoEAGGVQk1AAFroAbYAEngBxFtAA/HABBA4kASQ4EAAAAQQQCCAFotAFFoAtAAAAAFdEMAwAAoooAQSCCCQAmgJIbAkkG2qoG2wADrtoqSJEACAIFkoAAHABGIE4AAAAkkAAAiSgCCAIAIAAAAAAAAH//FoFFAGbbYAAAACACAQAUgSQSX4AGGqsE2ASDbvooABEATQbdkoI/HQAAACQAkkgAAAAAkEgAAAJAIGG2GAYAAYHAQQFFAAwZJAAEgkBIAA444QQQX6SSSVUkxwXUgttqSBEAQQYdtBI/AAAAAQCw/X4w84AAgggAQAIIIGGAGADADknAEAYAYwwZAH/EEEIAAABJASQSABAiBqsE2ISU8oooBBECACbdAOxICSAAAWyA6S4AigAAgAgAQAIBIGAGGADDD2nGEGD7AGAZBHPEEEIAAAAgAQAQQBkEBVUExwQUgooqSJMgFAYFFhJJSSQAAQCAkkgA84AAgAgCCAIAI2G2GwAYY03A2wXAH/6ZJH/EAEBIAAHHAQASABgkB"); +var img = atob("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n/2wAAABBCQAEGEmBJFoAABIoooxwgPoAddoCEkFFFAEwwAGwAHJIIJBAAAG2AjDAG/n+AABBBBAggXi3/xI13/////4AAABICQDEk0mBAFqABIHCSH2wkgw4FDCCHnAooAoGAAAAAwFttGAAAAGGAkkkkn/ttrADAAAAAX6nPhStv////+mmbYBBCQZY2AGAAACABIoqSookEAAADFCSEnbbbAFigAAAMEAFA2wAAA/q6gEkAA4AAFBBEAkgEHghJiR13////880cYBAIDLLAAAAAkCABAHCSHCAkBBHAJKCDbFArAAEEgkIMkAFAGAAAAvq6DE8AYGPOFAAEAggEUiOOIJbf////+mmbYAAABZZA2wEggiQQQAowoAQCBIHBICIDAFFYA2GEEENMEAFAGAQQYtqSw0kDDAHAFCCEYggcHgJxIAfb////840BJA9FALIAAAggkACCkEnAHACopB/JJJJDbFrDSww0AEAIwAFAkgSTZJmBwwgjbGIOFBJEYkgcAAH/BpbbAAADAAmIQHFoABHMkkEAggAAgkgggEgEDbG222PvvFdCAwwwBJJIAAAAAAQQZBgB2wgjbAAAADAAbADYAUg4FFkjAAHDgE0IQA9FAABMkgAAgEAAkEEABJJJDr2GG2PvvFAoSDADBJJAAGWAAAoABJgBwwAAAAAARP8gYYYYAWg4FIijbDDbkEmQSQAAAAEkkgAAAAgAAAAggAAgjb2222PvvbDbADYbBJJAACiAAAooBhk5YDH/CACCBP8gbADYAUg4BFkjDADDgk0gQQkgAAGAAAGABJg2AAHEHAAEAF2AA1PvvDbAYDDDYBAYAGWAAAoEkA/ADY//6CCARP8gSQAAAbbYAAtrADDDwAmgSQAkkkAYDAFGSIkGAAASkkAggA+228N//AYDDDAAbBAYAbYAAAov/gA/DY446HCCAJJFQFAEkAYAMgSrDDDDSQMEkB4AAwGADAouWIIGAAAW0kA44AFotBJASbaQYDAAGxYYYYYAAos//8HAYD446A6AQABFQFAEAgbYIESrDDAbQBIoQYEgGAAYAYFCWJBwAAASkkAIIkQDAADbabaAA2AAABbbYbYgAtt778AAAA//6/6CAABFQFAEAgYYIEtobAAgSIIGABAAAAGADDDH/JBAAAA4A4ABAigFAASTiaahABgdoBAAAAFtAAAn/gAAtoH/CQSAQIBFSVAEkAbJMgJIBJEgRAIqSA4gAAAYDYYfnAAAHHAAH/AIIkQBQAATjTThABgYqBAAA2ttoAAd8YJAAoA+2SWywBIFtFtEAgYYAA54BAggAAECABkgAAGADADH/AAAHHAAAAAAAigoDAATcccZhhgdqCH/AwssoAAbzYIAFAAGACGAQ4w4w4w4w4bYAAJIBBkkAAACSP/4AAAYAdvQQAAAAAGAAAA/AH+AEAATbjjYgggaCSJJA2ttoAAYYYIAAAA+GAGAwbbgEJMMAAAAAA2wBJAgAg5//J/4AAGAAF/CAAAAGWWWWLI/GH4AoAQQbbbAEAkiSA//AwllgAAYRYJAA3AH2AAGwbbAAG2wAG2wAAOIAAAAEEPJJJP8kkAEUlvCJIAAAwwwwLIAGJJGAACJASQYAAAACAIAA2EkAAAaIYBAA/AHFAAAADAAAwAG2wnOAAEAAAAX4E5/JJJ8nkFEigAC/4AAAwxwwLIAGJB4AAAIIAQb7AwASQYAAAgAkgARIEBAAA/4AAAAHHHEn8gA2AIgAAggAQAH4gAAAJJM/8tEkSAAtqSJAGAGAAAgAIhIEAAIOCG4GAAGFzbYuAgAgACIBtJAAA44EAA5IggEn8gGAwgIM3EACQAAEkAAbe20nkFEAQQ1wAQIIwxwwkgkkkACYAAIIQAYAwwwAQYGAIgAkgRABoAAAAAomgoxgkgEn8gADY5hg+EAAAFtAAH4YGSkkkAICCCGuCQIIAAAAoA222AAAAAJASQ4AG2AAozAGAgAAgIJItAAAAAAECA8gEAAAAAAoFQDPcggAQFFAQAADEkkngBxAQQA1wAJA23gJFAkkkAFwQAQAGAD7AwAQUEAoAkgkgFEkoGDtoooAWSACAQAAAAAHAQYAxggGwFtDbbAAYAE//AIACAAAAAJIOk/JAoAAgAHIUkQAAABJAAAGEgGMIAAAArsJtwztFFFFSSQSQQAkQ/9AoEAGGVQk1AAFroAbYAEngBxFtAA/HABBG4kISQ4EAAAAQQQCCFFotAFFsEtAAAAAFdEMAwDdoooAQSCCCQAmgJIbAkkG2qoG2wADrtoqSJEACAIFkoAAHABGOE4IAAAkkAAAiSgCCAIAIAAAAAAAAH//FoFFAGbbYAAAACACAQAUgSQSX4AGGqsE2ASDbvooABEATQbdkoI/HQAAACQAkkgAAAAAkEgAAFJANGG2GAYAAYHAQQFFAAwZJAAEgkBIAA444QQQX6SSSVUkxwXUgttqSBEAQQYdtBI/AAAAAQCw/X4w84AAgggAQFIINGGAGADADknAEAYAYwwZAH/EEEIAAABJASQSABAiBqsE2ISU8oooBBECACbdAOxICSAAAWyA6S4AigAAgAgAQFIBNGAGGADDD2nGEGD7AGAZBHPEEEIAAAAgAQAQQBkEBVUExwQUgooqSJMgFAYFFhJJSSQAAQCAkkgA84AAgAgCCFIAN2G2GwAYY03A2wXAH/6ZJH/EAEBIAAHHAQASABgkB"); var imgHeight = g.imageMetrics(img).height; var imgScroll = Math.floor(Math.random()*imgHeight); diff --git a/apps/alarm/alarm.js b/apps/alarm/alarm.js index bb5722106..a655dad1e 100644 --- a/apps/alarm/alarm.js +++ b/apps/alarm/alarm.js @@ -21,8 +21,8 @@ function showAlarm(alarm) { Bangle.loadWidgets(); Bangle.drawWidgets(); E.showPrompt(msg,{ - title:alarm.timer ? "TIMER!" : "ALARM!", - buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins + title:alarm.timer ? /*LANG*/"TIMER!" : /*LANG*/"ALARM!", + buttons : {/*LANG*/"Sleep":true,/*LANG*/"Ok":false} // default is sleep so it'll come back in 10 mins }).then(function(sleep) { buzzCount = 0; if (sleep) { diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 53c7154bc..17062d44a 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -33,16 +33,16 @@ function getCurrentHr() { function showMainMenu() { const menu = { '': { 'title': 'Alarm/Timer' }, - '< Back' : ()=>{load();}, - 'New Alarm': ()=>editAlarm(-1), - 'New Timer': ()=>editTimer(-1) + /*LANG*/'< Back' : ()=>{load();}, + /*LANG*/'New Alarm': ()=>editAlarm(-1), + /*LANG*/'New Timer': ()=>editTimer(-1) }; alarms.forEach((alarm,idx)=>{ if (alarm.timer) { - txt = "TIMER "+(alarm.on?"on ":"off ")+formatMins(alarm.timer); + txt = /*LANG*/"TIMER "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatMins(alarm.timer); } else { - txt = "ALARM "+(alarm.on?"on ":"off ")+formatTime(alarm.hr); - if (alarm.rp) txt += " (repeat)"; + txt = /*LANG*/"ALARM "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatTime(alarm.hr); + if (alarm.rp) txt += /*LANG*/" (repeat)"; } menu[txt] = function() { if (alarm.timer) editTimer(idx); @@ -70,27 +70,27 @@ function editAlarm(alarmIndex) { as = a.as; } const menu = { - '': { 'title': 'Alarm' }, - '< Back' : showMainMenu, - 'Hours': { + '': { 'title': /*LANG*/'Alarm' }, + /*LANG*/'< Back' : showMainMenu, + /*LANG*/'Hours': { value: hrs, onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' }, - 'Minutes': { + /*LANG*/'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' }, - 'Enabled': { + /*LANG*/'Enabled': { value: en, format: v=>v?"On":"Off", onchange: v=>en=v }, - 'Repeat': { + /*LANG*/'Repeat': { value: en, format: v=>v?"Yes":"No", onchange: v=>repeat=v }, - 'Auto snooze': { + /*LANG*/'Auto snooze': { value: as, format: v=>v?"Yes":"No", onchange: v=>as=v @@ -108,14 +108,14 @@ function editAlarm(alarmIndex) { last : day, rp : repeat, as: as }; } - menu["> Save"] = function() { + menu[/*LANG*/"> Save"] = function() { if (newAlarm) alarms.push(getAlarm()); else alarms[alarmIndex] = getAlarm(); require("Storage").write("alarm.json",JSON.stringify(alarms)); showMainMenu(); }; if (!newAlarm) { - menu["> Delete"] = function() { + menu[/*LANG*/"> Delete"] = function() { alarms.splice(alarmIndex,1); require("Storage").write("alarm.json",JSON.stringify(alarms)); showMainMenu(); @@ -136,18 +136,18 @@ function editTimer(alarmIndex) { en = a.on; } const menu = { - '': { 'title': 'Timer' }, - 'Hours': { + '': { 'title': /*LANG*/'Timer' }, + /*LANG*/'Hours': { value: hrs, onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' }, - 'Minutes': { + /*LANG*/'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' }, - 'Enabled': { + /*LANG*/'Enabled': { value: en, - format: v=>v?"On":"Off", + format: v=>v?/*LANG*/"On":/*LANG*/"Off", onchange: v=>en=v } }; diff --git a/apps/alarm/boot.js b/apps/alarm/boot.js index 47dae5361..dffb3a37f 100644 --- a/apps/alarm/boot.js +++ b/apps/alarm/boot.js @@ -7,7 +7,7 @@ active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24); var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); if (!require('Storage').read("alarm.js")) { - console.log("No alarm app!"); + console.log(/*LANG*/"No alarm app!"); require('Storage').write('alarm.json',"[]"); } else { var t = 3600000*(active[0].hr-hr); diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 35fa0e386..c2c4ea6be 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -3,3 +3,4 @@ Fix music control 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 diff --git a/apps/android/boot.js b/apps/android/boot.js index 97e3a5641..59ffe006d 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -65,7 +65,7 @@ // Message response Bangle.messageResponse = (msg,response) => { if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" }); - if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS" }); + if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); // error/warn here? }; })(); diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog index 7b83706bf..e1b8ed5bc 100644 --- a/apps/authentiwatch/ChangeLog +++ b/apps/authentiwatch/ChangeLog @@ -1 +1,4 @@ +0.04: Fix tapping at very bottom of list, exit on inactivity +0.03: Add "Calculating" placeholder, update JSON save format +0.02: Fix JSON save format 0.01: First release diff --git a/apps/authentiwatch/README.md b/apps/authentiwatch/README.md index 403770c2b..8d0e74a0c 100644 --- a/apps/authentiwatch/README.md +++ b/apps/authentiwatch/README.md @@ -1,5 +1,8 @@ # Authentiwatch - 2FA Authenticator +* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here +* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/ + ## Supports * Google Authenticator compatible 2-factor authentication diff --git a/apps/authentiwatch/app-icon.js b/apps/authentiwatch/app-icon.js index 27ced695e..c901fb843 100644 --- a/apps/authentiwatch/app-icon.js +++ b/apps/authentiwatch/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mUywkBiIADCxoTFAAcQGBwY/DDQIKDBiMDDCgGCBI4YMGAIDFDCAFEBQwYLFgIYEGQgYMApoYJGAJjFMogYMSQgCDDBwDCY4oMEDBZgHHQQYQf4oYVBgwYQBogYPPYZpFDBKMEDAbdDCxT9IDYIFFABqSEAogySQYoWNFgrFDJZoQBJggYRBwhLGDBwyFDCZGEDCYAEDGrIMbwhnGDEpLGAwxlLFQgQDJiYoFDDAZDDCpMDMpQOCNxQYNBo4KKBpwYYBYJ8NeJgYkLBQY8UYQXVGQIwN")) +require("heatshrink").decompress(atob("mEwxH+AH4AD64ADFlgAFF04INFz4LUF0QwjEBwv/FzwwgF/4v/F6nMAAWi1AFD5nOeEHPEweoFooAB5/X5wvdFwotG5nN6/WAoQuaEoguHSYPQLwIIDF8uo5ouB6AJEFzuiFwup5/WFwI6GL0esXYKMBHYy9j1WqfBSOhBIYKJF8gAKF/4v6cZAvhGDAuWSDAvXMCwuYF+AwUFzX+0XGGAgxKFrYuBAAQxEeg4tcF4oABBQnGAAgv/F6b5KXsIvIGAqNnF/69fX8ZeSF7btNR8IuOF75ePL8ouOd74NKF8IANF94wEF1QAXA")) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 43eff4709..c0cb608c0 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -6,8 +6,15 @@ const algos = { "SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 }, "SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 }, }; +const calculating = "Calculating"; +const notokens = "No tokens"; +const notsupported = "Not supported"; -var tokens = require("Storage").readJSON("authentiwatch.json", true) || []; +// sample settings: +// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} +var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}}; +if (settings.data ) tokens = settings.data ; /* v0.02 settings */ +if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ // QR Code Text // @@ -66,9 +73,8 @@ function do_hmac(key, message, algo) { var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); return v.getUint32(0) & 0x7FFFFFFF; } -function hotp(token) { +function hotp(d, token, dohmac) { var tick; - var d = new Date(); if (token.period > 0) { // RFC6238 - timed var seconds = Math.floor(d.getTime() / 1000); @@ -81,15 +87,17 @@ function hotp(token) { var v = new DataView(msg.buffer); v.setUint32(0, tick >> 16 >> 16); v.setUint32(4, tick & 0xFFFFFFFF); - var ret = ""; - try { - var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); - ret = "" + hash % Math.pow(10, token.digits); - while (ret.length < token.digits) { - ret = "0" + ret; + var ret = calculating; + if (dohmac) { + try { + var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); + ret = "" + hash % Math.pow(10, token.digits); + while (ret.length < token.digits) { + ret = "0" + ret; + } + } catch(err) { + ret = notsupported; } - } catch(err) { - ret = "Not supported"; } return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; } @@ -109,7 +117,7 @@ function drawToken(id, r) { var y1 = r.y; var x2 = r.x + r.w - 1; var y2 = r.y + r.h - 1; - var adj; + var adj, sz; g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ), Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2)); if (id == state.curtoken) { @@ -129,7 +137,7 @@ function drawToken(id, r) { adj = (y1 + y2) / 2; } g.clearRect(x1, y1, x2, y2); - g.drawString(tokens[id].label, (x1 + x2) / 2, adj, false); + g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false); if (id == state.curtoken) { if (tokens[id].period > 0) { // timed - draw progress bar @@ -140,11 +148,14 @@ function drawToken(id, r) { // counter - draw triangle as swipe hint let yc = (y1 + y2) / 2; g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]); - adj = 5; + adj = 10; } // digits just below label - g.setFont("Vector", (state.otp.length > 8) ? 26 : 30); - g.drawString(state.otp, (x1 + x2) / 2 + adj, y1 + 16, false); + sz = 30; + do { + g.setFont("Vector", sz--); + } while (g.stringWidth(state.otp) > (r.w - adj)); + g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false); } // shaded lines top and bottom g.setColor(0.5, 0.5, 0.5); @@ -154,9 +165,14 @@ function drawToken(id, r) { } function draw() { + var timerfn = exitApp; + var timerdly = 10000; var d = new Date(); if (state.curtoken != -1) { var t = tokens[state.curtoken]; + if (state.otp == calculating) { + state.otp = hotp(d, t, true).hotp; + } if (d.getTime() > state.nextTime) { if (state.hide == 0) { // auto-hide the current token @@ -167,7 +183,7 @@ function draw() { state.nextTime = 0; } else { // time to generate a new token - var r = hotp(t); + var r = hotp(d, t, state.otp != ""); state.nextTime = r.next; state.otp = r.hotp; if (t.period <= 0) { @@ -191,11 +207,13 @@ function draw() { y += tokenentryheight; } if (drewcur) { - // the current token has been drawn - draw it again in 1sec - if (state.drawtimer) { - clearTimeout(state.drawtimer); + // the current token has been drawn - schedule a redraw + if (tokens[state.curtoken].period > 0) { + timerdly = (state.otp == calculating) ? 1 : 1000; // timed + } else { + timerdly = state.nexttime - d.getTime(); // counter } - state.drawtimer = setTimeout(draw, (tokens[state.curtoken].period > 0) ? 1000 : state.nexttime - d.getTime()); + timerfn = draw; if (tokens[state.curtoken].period <= 0) { state.hide = 0; } @@ -210,14 +228,18 @@ function draw() { } else { g.setFont("Vector", 30); g.setFontAlign(0, 0, 0); - g.drawString("No tokens", Bangle.appRect.x + Bangle.appRect.w / 2,Bangle.appRect.y + Bangle.appRect.h / 2, false); + g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } + if (state.drawtimer) { + clearTimeout(state.drawtimer); + } + state.drawtimer = setTimeout(timerfn, timerdly); } function onTouch(zone, e) { if (e) { var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight); - if (id == state.curtoken || tokens.length == 0) { + if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { id = -1; } if (state.curtoken != id) { @@ -231,37 +253,34 @@ function onTouch(zone, e) { if (y > Bangle.appRect.h) { state.listy += (y - Bangle.appRect.h); } + state.otp = ""; } state.nextTime = 0; state.curtoken = id; state.hide = 2; - draw(); } } + draw(); } function onDrag(e) { if (e.x > g.getWidth() || e.y > g.getHeight()) return; if (e.dx == 0 && e.dy == 0) return; var newy = Math.min(state.listy - e.dy, tokens.length * tokenentryheight - Bangle.appRect.h); - newy = Math.max(0, newy); - if (newy != state.listy) { - state.listy = newy; - draw(); - } + state.listy = Math.max(0, newy); + draw(); } function onSwipe(e) { - if (e == 1) { - Bangle.showLauncher(); - } if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { tokens[state.curtoken].period--; - require("Storage").writeJSON("authentiwatch.json", tokens); + let newsettings={tokens:tokens,misc:settings.misc}; + require("Storage").writeJSON("authentiwatch.json", newsettings); state.nextTime = 0; + state.otp = ""; state.hide = 2; - draw(); } + draw(); } function bangle1Btn(e) { @@ -281,16 +300,22 @@ function bangle1Btn(e) { state.curtoken = -1; state.nextTime = 0; onTouch(0, fakee); + } else { + draw(); // resets idle timer } } +function exitApp() { + Bangle.showLauncher(); +} + Bangle.on('touch', onTouch); Bangle.on('drag' , onDrag ); Bangle.on('swipe', onSwipe); if (typeof BTN2 == 'number') { - setWatch(function(){bangle1Btn(-1); }, BTN1, {edge:"rising", debounce:50, repeat:true}); - setWatch(function(){Bangle.showLauncher();}, BTN2, {edge:"rising", debounce:50, repeat:true}); - setWatch(function(){bangle1Btn( 1); }, BTN3, {edge:"rising", debounce:50, repeat:true}); + setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising", debounce:50, repeat:true}); + setWatch(function(){exitApp(); }, BTN2, {edge:"rising", debounce:50, repeat:true}); + setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising", debounce:50, repeat:true}); } Bangle.loadWidgets(); diff --git a/apps/authentiwatch/app.png b/apps/authentiwatch/app.png index 208fb63b3..8775d3e40 100644 Binary files a/apps/authentiwatch/app.png and b/apps/authentiwatch/app.png differ diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html index 12c0c1d8d..26533b17b 100644 --- a/apps/authentiwatch/interface.html +++ b/apps/authentiwatch/interface.html @@ -35,8 +35,9 @@ const otpAuthUrl = 'otpauth://'; const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)']; -/* Array of TOTP tokens */ -var tokens=[]; +/* Settings */ +var settings = {tokens:[], misc:{}}; +var tokens = settings.tokens; /* Remove any non-base-32 characters from the given string and collapses * whitespace to a single space. Optionally removes all whitespace from @@ -261,6 +262,7 @@ qrcode.callback = res => { scanning = false; editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value)); t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')'; + t['label'] = t['label'].substr(0, 10); var fe = document.forms['edittoken'].elements; if (res.startsWith(otpAuthUrl + 'hotp/')) { t['period'] = '30'; @@ -319,21 +321,21 @@ function doScan() { */ function loadTokens() { Util.showModal('Loading...'); - Puck.eval(`require('Storage').read(${JSON.stringify('authentiwatch.json')})`,data=>{ + Puck.eval(`require('Storage').readJSON(${JSON.stringify('authentiwatch.json')})`,data=>{ Util.hideModal(); - try { - tokens = JSON.parse(data); - updateTokens(); - } catch { - tokens = []; - } + if (data.data ) settings.tokens = data.data ; /* v0.02 settings */ + if (data.tokens) settings.tokens = data.tokens; /* v0.03+ settings */ + if (data.misc ) settings.misc = data.misc ; /* v0.03+ settings */ + tokens = settings.tokens; + updateTokens(); }); } /* Save settings as a JSON file on the watch. */ function saveTokens() { Util.showModal('Saving...'); - Puck.write(`\x10require('Storage').write(${JSON.stringify('authentiwatch.json')},${JSON.stringify(tokens)})\n`,()=>{ + let newsettings={tokens:tokens,misc:settings.misc}; + Puck.write(`\x10require('Storage').writeJSON(${JSON.stringify('authentiwatch.json')},${JSON.stringify(newsettings)})\n`,()=>{ Util.hideModal(); }); } diff --git a/apps/awairmonitor/ChangeLog b/apps/awairmonitor/ChangeLog new file mode 100644 index 000000000..0cc9a42b0 --- /dev/null +++ b/apps/awairmonitor/ChangeLog @@ -0,0 +1 @@ +0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11) diff --git a/apps/awairmonitor/README.md b/apps/awairmonitor/README.md new file mode 100644 index 000000000..69894fea2 --- /dev/null +++ b/apps/awairmonitor/README.md @@ -0,0 +1,22 @@ +# Awair Monitor + +Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device. + +* What you need: + * A BangleJS 2 + * An Awair device [with local API enabled](https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature) + * The web app [awair_to_bangle.html](awair_to_bangle.html) that will retrive the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection +* How to get started + * Open awair_to_bangle.html with a text/code editor and input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx") + * Launch the Awair Monitor app on your BangleJS + * Open awair_to_bangle.html on Chrome and click "Connect BangleJS" - it connects to your watch the same way as the Bangle app store + * Once connected to the watch with the app running, the watch app is updated once per second + +![](screenshot.png) + +![](awair-monitor-photo.jpg) + +## Creator +[@alainsaas](https://github.com/alainsaas) + +Contributions are welcome, send me your Pull Requests! diff --git a/apps/awairmonitor/app-icon.js b/apps/awairmonitor/app-icon.js new file mode 100644 index 000000000..9d4dcf4a3 --- /dev/null +++ b/apps/awairmonitor/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AD38g4FD8EAAoeAgE/AoUD/EfAgP+AYMPDgQPBw4FB/F///DAoPwAQPjAQPBAQPxDgJVCAoP4gYaCCwIcBAoM/8P8h0HjEP8f4h0Gp0H4/44lj5+H4/54lzj/jx/5/lyDgIFDh/xAoQRBAoXsuY8Bx4jCAoeEkYFB447CAoRxBOAPxM4RmC8IFD4ZZD/8H/DHDh/+AoaSBUAIABCoYATVwS2Ct4FE84REXQQLCk4RJAo0XGxY=")) diff --git a/apps/awairmonitor/app.js b/apps/awairmonitor/app.js new file mode 100644 index 000000000..a5a1d1a72 --- /dev/null +++ b/apps/awairmonitor/app.js @@ -0,0 +1,98 @@ +Graphics.prototype.setFontMichroma36 = function() { +g.setFontCustom(atob("AAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAAAAAAAAAAAAAAAAAAAGAAAAA+AAAAD+AAAAP+AAAA/8AAAD/wAAAf/AAAB/4AAAH/gAAAf+AAAB/4AAAH/gAAAf+AAAAfwAAAAfAAAAAcAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AP///8APwAD+APAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAeAPAAAeAPwAD+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAEAAAAAOAAAAAfAAAAA+AAAAB8AAAAD8AAAAH4AAAAPwAAAAPgAAAAfAAAAAf///+Af///+Af///+Af///+AAAAAAAAAAAAAAAAAAAAAAAAAA/Af+AD/A/+AH/B/+AP/D/+APwD4eAPADweAfADweAeADweAeADweAeADweAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAPgeAeAPAeAeAPAeAeAPAeAeAPAeAfAPAeAPw/AeAP/+AeAH/+AeAD/8AeAB/wAOAAAAAAAAAAAAAAAAAAAAAAAAAB8APgAD8AP4AH8AP8AP8AP8APgAB+AfAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAeAfAeAeAPx/h+AP///+AH///8AD///4AB/h/gAAAAAAAAAAAAAAAAAAAAAAeAAAAA/AAAAA/AAAAB/AAAAD/AAAAH/AAAAPvAAAAPPAAAAfPAAAA+PAAAB8PAAAD4PAAADwPAAAHwPAAAPgPAAAfAPAAA+APAAA8APAAB8APAAD4APAAHwAPAAPgAPAAPAAPAAfAAPAAf///+Af///+Af///+Af///+AAAAPAAAAAPAAAAAPAAAAAPAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAf/8PgAf/8P4Af/8P8Af/8P8AeB4A+AeB4AeAeDwAeAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAfAeDwAeAeD4A+AeD+D+AeB//8AeB//4AeA//4AAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AH///8AP4fB+APAeAeAfA8AeAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAfA8APAPA+AeAPgeAeAP8fh+AH8f/8AD8P/8AA8H/4AAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAACAeAAAGAeAAAOAeAAAeAeAAA+AeAAD+AeAAH8AeAAP4AeAAfwAeAA/gAeAB/AAeAD+AAeAP4AAeAfwAAeA/gAAeB/AAAeD+AAAeH8AAAefwAAAe/gAAAf/AAAAf+AAAAf8AAAAf4AAAAfgAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAMAAB+B/wAD/j/4AH/3/8AP///+AP//A+AfB+AeAeA+AeAeA+APAeA+APAeA+APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA+APAeA+APAeA+APAeA+AOAeA+AeAPh/A+AP///+AP/3/8AH/3/8AB/D/wAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAD/4HAAH/8HwAP/+H4AP5/H8AfAfA8AeAPAeAeAPAeAeAPAeAeAHgfAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHAPAeAPAOAeAPAeAPAPAeAPwfB+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAAAAAAAB8DwAAB8HwAAB8HwAAB8DwAAAAAAAAAAAAA"), 46, atob("CBIkESMjJCMjIyMjCA=="), 36+(1<<8)+(1<<16)); +}; + +var drawTimeout; + +function queueNextDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 1000 - (Date.now() % 1000)); +} + +var locale = require("locale"); + +var bt_current_co2 = 0; +var bt_current_voc = 0; +var bt_current_pm25 = 0; +var bt_current_humi = 0; +var bt_current_temp = 0; +var bt_last_update = 0; + +var last_update = 0; +var bt_co2_history = new Array(10).fill(0); +var bt_voc_history = new Array(10).fill(0); +var bt_pm25_history = new Array(10).fill(0); +var bt_humi_history = new Array(10).fill(0); +var bt_temp_history = new Array(10).fill(0); + +var internal_last_update = -1; + +function draw() { + g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); + + var date = new Date(); + g.setFontAlign(0,0); + g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 56); + + g.setFont("6x8"); + g.drawString(locale.date(new Date(),1), g.getWidth()/2, 80); + + g.setFont("6x8"); + g.drawString("CO2", 20, 100); + g.drawString("VOC", 55, 100); + g.drawString("PM25", 90, 100); + g.drawString("Humi", 125, 100); + g.drawString("Temp", 160, 100); + + g.setFont("HaxorNarrow7x17"); + g.drawString(""+bt_current_co2, 18, 110); + g.drawString(""+bt_current_voc, 53, 110); + g.drawString(""+bt_current_pm25, 88, 110); + g.drawString(""+bt_current_humi, 123, 110); + g.drawString(""+bt_current_temp, 158, 110); + + if (last_update != bt_last_update) { + last_update = bt_last_update; + internal_last_update = last_update; + if (last_update % 10 == 0) { + bt_co2_history.shift(); bt_co2_history.push(bt_current_co2); + bt_voc_history.shift(); bt_voc_history.push(bt_current_voc); + bt_pm25_history.shift(); bt_pm25_history.push(bt_current_pm25); + bt_humi_history.shift(); bt_humi_history.push(bt_current_humi); + bt_temp_history.shift(); bt_temp_history.push(bt_current_temp); + } + } + + if (internal_last_update == -1) { + g.drawString("Waiting for connection", 88, 164); + } else if (internal_last_update > last_update + 5) { + g.drawString("Trying to reconnect since " + (internal_last_update - last_update), 88, 164); + } + + + for (i = 0; i < 10; i++) { + // max height = 32 + g.drawLine(10+i*2, 150-(Math.min(Math.max(bt_co2_history[i],400), 1200)-400)/25, 10+i*2, 150); + g.drawLine(45+i*2, 150-(Math.min(Math.max(bt_voc_history[i],0), 1440)-0)/45, 45+i*2, 150); + g.drawLine(80+i*2, 150-(Math.min(Math.max(bt_pm25_history[i],0), 32)-0)/1, 80+i*2, 150); + g.drawLine(115+i*2, 150-(Math.min(Math.max(bt_humi_history[i],20), 60)-20)/1.25, 115+i*2, 150); + g.drawLine(150+i*2, 150-(Math.min(Math.max(bt_temp_history[i],19), 27)-19)*4, 150+i*2, 150); + + // target humidity level + g.setColor("#00F").drawLine(115, 150-(40-20)/1.25, 115+18, 150-(40-20)/1.25); + g.reset(); + } + + if (internal_last_update != -1) { internal_last_update++; } + queueNextDraw(); +} + +// init +require("FontHaxorNarrow7x17").add(Graphics); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/awairmonitor/app.png b/apps/awairmonitor/app.png new file mode 100644 index 000000000..26a5d0cff Binary files /dev/null and b/apps/awairmonitor/app.png differ diff --git a/apps/awairmonitor/awair-monitor-photo.jpg b/apps/awairmonitor/awair-monitor-photo.jpg new file mode 100644 index 000000000..8b62faa24 Binary files /dev/null and b/apps/awairmonitor/awair-monitor-photo.jpg differ diff --git a/apps/awairmonitor/awair_to_bangle.html b/apps/awairmonitor/awair_to_bangle.html new file mode 100644 index 000000000..2926cca9e --- /dev/null +++ b/apps/awairmonitor/awair_to_bangle.html @@ -0,0 +1,195 @@ + + + + + + + + + + +

+How to use +

+Step 1: Enable the Local API on your Awair: https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature +

+Step 2: Modify this HTML file to input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx") +

+Step 3: Launch the Awair Monitor app on your BangleJS +

+Step 4: Click "Connect BangleJS" +

+Step 5: Optionally, open the web inspector's console (Right click > Inspector > Console) to read the bluetooth logs +

+ +
+ + +
+ +

+ +
+
+
+
+
+ + diff --git a/apps/awairmonitor/screenshot.png b/apps/awairmonitor/screenshot.png new file mode 100644 index 000000000..51ca0aa44 Binary files /dev/null and b/apps/awairmonitor/screenshot.png differ diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 98f80efd9..5c929421b 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -40,3 +40,6 @@ 0.35: Add Bangle.appRect polyfill Don't set beep vibration up on Bangle.js 2 (built in) 0.36: Add comments to .boot0 to make debugging a bit easier +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) diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index d642426c2..e338d9020 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -23,8 +23,14 @@ if (s.ble!==false) { boot += `bleServiceOptions.hid=Bangle.HID;\n`; } } -if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth - if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal +if (s.log==2) { // logging to file + boot += `_DBGLOG=require("Storage").open("log.txt","a"); +`; +} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth + if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +LoopbackA.setConsole(true);\n`; + else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console /* If not programmable add our own handler for Bluetooth data to allow Gadgetbridge commands to be received*/ @@ -41,7 +47,10 @@ Bluetooth.on('line',function(l) { try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} });\n`; } else { - if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) + if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`; + else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth } // we just reset, so BLE should be on. @@ -78,14 +87,8 @@ boot += `E.on('errorFlag', function(errorFlags) { if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`; // Apply any settings-specific stuff if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; -if (s.quiet && s.qmOptions) boot+=`Bangle.setOptions(${E.toJS(s.qmOptions)});\n`; -if (s.quiet && s.qmBrightness) { - if (s.qmBrightness!=1) boot+=`Bangle.setLCDBrightness(${s.qmBrightness});\n`; -} else { - if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; -} -if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`; -if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`; +if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; +if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; // Pre-2v10 firmwares without a theme/setUI delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 8bafff34a..de887bfa7 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -1,2 +1,3 @@ 0.01: Basic calendar 0.02: Make Bangle 2 compatible +0.03: Add setting to start week on Sunday diff --git a/apps/calendar/README.md b/apps/calendar/README.md index 19a60afc0..e22d06573 100644 --- a/apps/calendar/README.md +++ b/apps/calendar/README.md @@ -6,3 +6,8 @@ Basic calendar - Use `BTN4` (left screen tap) to go to the previous month - Use `BTN5` (right screen tap) to go to the next month + +## Settings + +- Starts on Sunday: whether the calendar should start on Sunday (default is Monday). + diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 6f3c33164..5707bd97a 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -18,6 +18,10 @@ const gray2 = "#888888"; const gray3 = "#bbbbbb"; const red = "#d41706"; +let settings = require('Storage').readJSON("calendar.json", true) || {}; +if (settings.startOnSun === undefined) + settings.startOnSun = false; + function drawCalendar(date) { g.setBgColor(color4); g.clearRect(0, 0, maxX, maxY); @@ -61,13 +65,18 @@ function drawCalendar(date) { ); g.setFont("6x8", fontSize); - const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + let dowLbls; + if (settings.startOnSun) { + dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; + } else { + dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + } dowLbls.forEach((lbl, i) => { g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); }); date.setDate(1); - const dow = date.getDay(); + const dow = date.getDay() + (settings.startOnSun ? 1 : 0); const dowNorm = dow === 0 ? 7 : dow; const monthMaxDayMap = { diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js new file mode 100644 index 000000000..f9c7783a3 --- /dev/null +++ b/apps/calendar/settings.js @@ -0,0 +1,24 @@ +(function(back) { + var FILE = "calendar.json"; + var settings = require('Storage').readJSON(FILE, true) || {}; + if (settings.startOnSun === undefined) + settings.startOnSun = true; + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Calendar" }, + "< Back" : () => back(), + 'Start on Sunday': { + value: settings.startOnSun, + format: v => v?"Yes":"No", + onchange: v => { + settings.startOnSun = v; + writeSettings(); + } + }, + }); +}) + diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog index e173467a1..ded543397 100644 --- a/apps/chronowid/ChangeLog +++ b/apps/chronowid/ChangeLog @@ -1,3 +1,5 @@ 0.01: New widget and app! 0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) -0.03: Display only minutes:seconds when less than 1 hour left \ No newline at end of file +0.03: Display only minutes:seconds when less than 1 hour left +0.04: Change to 7 segment font, move to top widget bar + Better auto-update behaviour, less RAM used diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md index ec1d5dd46..6e0aba681 100644 --- a/apps/chronowid/README.md +++ b/apps/chronowid/README.md @@ -5,14 +5,13 @@ The advantage is, that you can still see your normal watchface and other widgets The widget is always active, but only shown when the timer is on. Hours, minutes, seconds and timer status can be set with an app. -When there is less than one seconds left on the timer it buzzes. +When there is less than one second left on the timer it buzzes. The widget has been tested on Bangle 1 and Bangle 2 ## Screenshots -![](chrono_with_wave.jpg) -![](chrono_with_pastel.jpg) +![](screenshot.png) ## Features @@ -28,15 +27,15 @@ There are no settings section in the settings app, timer can be set using an app * Hours: Set the hours for the timer * Minutes: Set the minutes for the timer * Seconds: Set the seconds for the timer -* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. +* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. ## Releases -* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) +* Official app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) * Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) * Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid ## Requests -If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ \ No newline at end of file +If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js index 0cacdee23..f38105e34 100644 --- a/apps/chronowid/app.js +++ b/apps/chronowid/app.js @@ -3,7 +3,6 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); const storage = require('Storage'); -const boolFormat = v => v ? "On" : "Off"; let settingsChronowid; function updateSettings() { @@ -12,6 +11,7 @@ function updateSettings() { now.getHours() + settingsChronowid.hours, now.getMinutes() + settingsChronowid.minutes, now.getSeconds() + settingsChronowid.seconds); settingsChronowid.goal = goal.getTime(); storage.writeJSON('chronowid.json', settingsChronowid); + if (WIDGETS["chronowid"]) WIDGETS["chronowid"].reload(); } function resetSettings() { @@ -44,6 +44,7 @@ function showMenu() { timerMenu.started.value = settingsChronowid.started; } }, + '< Back' : ()=>{load();}, 'Reset values': function() { settingsChronowid.hours = 0; settingsChronowid.minutes = 0; @@ -84,15 +85,15 @@ function showMenu() { }, 'Timer on': { value: settingsChronowid.started, - format: boolFormat, + format: v => v ? "On" : "Off", onchange: v => { settingsChronowid.started = v; updateSettings(); } }, }; - timerMenu['-Exit-'] = ()=>{load();}; + return E.showMenu(timerMenu); } -showMenu(); \ No newline at end of file +showMenu(); diff --git a/apps/chronowid/chrono_with_pastel.jpg b/apps/chronowid/chrono_with_pastel.jpg deleted file mode 100644 index 2f5993e79..000000000 Binary files a/apps/chronowid/chrono_with_pastel.jpg and /dev/null differ diff --git a/apps/chronowid/chrono_with_wave.jpg b/apps/chronowid/chrono_with_wave.jpg deleted file mode 100644 index 5f35bd28b..000000000 Binary files a/apps/chronowid/chrono_with_wave.jpg and /dev/null differ diff --git a/apps/chronowid/screenshot.png b/apps/chronowid/screenshot.png new file mode 100644 index 000000000..f94eece94 Binary files /dev/null and b/apps/chronowid/screenshot.png differ diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js index f0e785efd..2d1c78941 100644 --- a/apps/chronowid/widget.js +++ b/apps/chronowid/widget.js @@ -1,93 +1,79 @@ (() => { - const storage = require('Storage'); - settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file - var height = 23; - var width = 58; + var settingsChronowid; var interval = 0; //used for the 1 second interval timer - var now = new Date(); + var diff; - var time = 0; - var diff = settingsChronowid.goal - now; - //Convert ms to time function getTime(t) { var milliseconds = parseInt((t % 1000) / 100), seconds = Math.floor((t / 1000) % 60), minutes = Math.floor((t / (1000 * 60)) % 60), hours = Math.floor((t / (1000 * 60 * 60)) % 24); - - hours = (hours < 10) ? "0" + hours : hours; - minutes = (minutes < 10) ? "0" + minutes : minutes; - seconds = (seconds < 10) ? "0" + seconds : seconds; - - return hours + ":" + minutes + ":" + seconds; + return hours.toString().padStart(2,0) + ":" + minutes.toString().padStart(2,0) + ":" + seconds.toString().padStart(2,0); } - function printDebug() { - print ("Nowtime: " + getTime(now)); - print ("Now: " + now); + /*function printDebug() { print ("Goaltime: " + getTime(settingsChronowid.goal)); print ("Goal: " + settingsChronowid.goal); print("Difftime: " + getTime(diff)); print("Diff: " + diff); print ("Started: " + settingsChronowid.started); print ("----"); - } + }*/ //counts down, calculates and displays function countDown() { - now = new Date(); + var now = new Date(); diff = settingsChronowid.goal - now; //calculate difference - WIDGETS["chronowid"].draw(); - //time is up + // time is up if (settingsChronowid.started && diff < 1000) { Bangle.buzz(1500); //write timer off to file settingsChronowid.started = false; - storage.writeJSON('chronowid.json', settingsChronowid); + require('Storage').writeJSON('chronowid.json', settingsChronowid); clearInterval(interval); //stop interval + interval = undefined; } - //printDebug(); + // calculates width and redraws accordingly + WIDGETS["chronowid"].redraw(); } - // draw your widget - function draw() { - if (!settingsChronowid.started) { - width = 0; - return; //do not draw anything if timer is not started - } - g.reset(); - if (diff >= 0) { - if (diff < 3600000) { //less than 1 hour left - width = 58; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 2); - g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 - } - if (diff >= 3600000) { //one hour or more left - width = 48; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 1); - g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 - } - } - // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. - // else { - // width = 58; - // g.clearRect(this.x,this.y,this.x+width,this.y+height); - // g.setFont("6x8", 2); - // g.drawString("END", this.x+15, this.y+5); - // } - } - - if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second - // add the widget - WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { - reload(); - Bangle.drawWidgets(); // relayout all widgets + WIDGETS["chronowid"]={area:"tl",width:0,draw:function() { + if (!this.width) return; + g.reset().setFontAlign(0,0).clearRect(this.x,this.y,this.x+this.width,this.y+23); + //g.drawRect(this.x,this.y,this.x+this.width-1, this.y+23); + var scale; + var timeStr; + if (diff < 3600000) { //less than 1 hour left + width = 58; + scale = 2; + timeStr = getTime(diff).substring(3); // remove hour part 00:00:00 -> 00:00 + } else { //one hour or more left + width = 48; + scale = 1; + timeStr = getTime(diff); //display hour 00:00:00 but small + } + // Font5x9Numeric7Seg - just build this in as it's tiny + g.setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 9 + (scale<<8)); + g.drawString(timeStr, this.x+this.width/2, this.y+12); + }, redraw:function() { + var last = this.width; + if (!settingsChronowid.started) this.width = 0; + else this.width = (diff < 3600000) ? 58 : 48; + if (last != this.width) Bangle.drawWidgets(); + else this.draw(); + }, reload:function() { + settingsChronowid = require('Storage').readJSON("chronowid.json",1)||{}; + if (interval) clearInterval(interval); + interval = undefined; + // start countdown each second + if (settingsChronowid.started) interval = setInterval(countDown, 1000); + // reset everything + countDown(); }}; //printDebug(); - countDown(); -})(); \ No newline at end of file + // set width correctly, start countdown each second + WIDGETS["chronowid"].reload(); +})(); diff --git a/apps/cliclockJS2Enhanced/ChangeLog b/apps/cliclockJS2Enhanced/ChangeLog index c7cb9e2c6..f4d146d5f 100644 --- a/apps/cliclockJS2Enhanced/ChangeLog +++ b/apps/cliclockJS2Enhanced/ChangeLog @@ -1,2 +1,3 @@ 0.01: Submitted to App Loader 0.02: Removed unneded code, added HID controlls thanks to t0m1o1 for his code :p +0.03: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) diff --git a/apps/cliclockJS2Enhanced/app.js b/apps/cliclockJS2Enhanced/app.js index 70e86f3d6..b6172b497 100644 --- a/apps/cliclockJS2Enhanced/app.js +++ b/apps/cliclockJS2Enhanced/app.js @@ -50,7 +50,7 @@ if (next) { setTimeout(drawApp, 1000); Bangle.setLocked(true); }, BTN1, { edge:"falling",repeat:true,debounce:50}); - Bangle.on('drag', function(e) { + Bangle.on('drag', function(e) { if(!e.b){ console.log(lasty); console.log(lastx); @@ -91,7 +91,7 @@ if (next) { lasty = lasty + e.dy; } }); - + } @@ -144,14 +144,15 @@ function writeLine(str,line){ } g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -drawAll(); + Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); var click = setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ - drawAll(); + drawAll(); // why do we redraw here?? }); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(); diff --git a/apps/clicompleteclk/ChangeLog b/apps/clicompleteclk/ChangeLog new file mode 100644 index 000000000..ee05bd582 --- /dev/null +++ b/apps/clicompleteclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New clock! +0.02: Load steps from Health Tracking app (if installed) diff --git a/apps/clicompleteclk/README.md b/apps/clicompleteclk/README.md new file mode 100644 index 000000000..8b8094633 --- /dev/null +++ b/apps/clicompleteclk/README.md @@ -0,0 +1,24 @@ +# Command line complete clock + +Command line styled clock with lots of information: + +It can show the following (depending on availability) information: +* Time data: + * Time + * Day of week + * Date +* Additional information (can be toggled via settings): + * Weather conditions and temperature (requires app [Weather](https://banglejs.com/apps/#weather)) + * Steps (requires app [Health Tracking](https://banglejs.com/apps/#health%20tracking) or a step widget) + * Heart rate (when screen is on and unlocked) + +## TODO +* Make time font bigger +* Show progress of steps (if any goal is set) +* Show trend of HRM out of history data + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icon +Icon taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/clicompleteclk/app-icon.js b/apps/clicompleteclk/app-icon.js new file mode 100644 index 000000000..b874bb6fa --- /dev/null +++ b/apps/clicompleteclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgI/8/4ACAqYv/F/PwAqgA6A==")) diff --git a/apps/clicompleteclk/app.js b/apps/clicompleteclk/app.js new file mode 100644 index 000000000..a39b37e58 --- /dev/null +++ b/apps/clicompleteclk/app.js @@ -0,0 +1,250 @@ +const storage = require('Storage'); +const locale = require("locale"); + +const font12 = g.getFonts().includes("12x20"); +const font = font12 ? "12x20" : "6x8"; +const fontsize = font12 ? 1: 2; +const fontheight = 19; + +const marginTop = 5; +const marginLeftTopic = 3; // margin of topics +const marginLeftData = font12 ? 64 : 75; // margin of data values + +const topicColor = g.theme.dark ? "#fff" : "#000"; +const textColor = g.theme.dark ? "#0f0" : "#080"; +const textColorRed = g.theme.dark ? "#FF0000" : "#FF0000"; + +let hrtValue; +let hrtValueIsOld = false; + +let localTempValue; +let weatherTempString; +let lastHeartRateRowIndex; +let lastStepsRowIndex; +let i = 2; + +let settings; + +function loadSettings() { + settings = storage.readJSON('clicompleteclk.json', 1) || {}; +} + +function setting(key) { + if (!settings) { loadSettings(); } + const DEFAULTS = { + 'battery': true, + 'batteryLvl': 30, + 'weather': true, + 'steps': true, + 'heartrate': true + }; + return (key in settings) ? settings[key] : DEFAULTS[key]; +} + + +let showBattery = setting('battery'); +let batteryWarnLevel = setting('batteryLvl'); +let showWeather = setting('weather'); +let showSteps = setting('steps'); +let showHeartRate = setting('heartrate'); + + +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + drawAll(true); + }, 60000 - (Date.now() % 60000)); +} + +function drawAll(drawInfoToo){ + let now = new Date(); + updateTime(now); + if (drawInfoToo) { + drawInfo(now); + } + queueDraw(); +} + +function updateTime(now){ + if (!Bangle.isLCDOn()) return; + writeLineTopic("TIME", 1); + writeLine(locale.time(now,1),1); +} + +function drawInfo(now) { + if (now == undefined) + now = new Date(); + + i = 2; + + writeLineTopic("DOWK", i); + writeLine(locale.dow(now),i); + i++; + + writeLineTopic("DATE", i); + writeLine(locale.date(now,1),i); + i++; + + if (showBattery) { + writeLineTopic("BATT", i); + const b = E.getBattery(); + writeLine(b + "%", i, b < batteryWarnLevel ? textColorRed : textColor); + i++; + } + + if (showWeather) { + drawWeather(); + } + + if (showSteps) { + drawSteps(i); + i++; + } + + if (showHeartRate) { + drawHeartRate(i); + } +} + +function drawWeather() { + const weatherJson = getWeather(); + if(weatherJson && weatherJson.weather){ + const currentWeather = weatherJson.weather; + + const weatherTempValue = locale.temp(currentWeather.temp-273.15); + weatherTempString = weatherTempValue; + writeLineTopic("WTHR", i); + writeLine(currentWeather.txt,i); + i++; + + writeLineTopic("TEMP", i); + writeLine(weatherTempValue,i); + i++; + } +} + +function drawSteps(i) { + if (!showSteps) return; + if (i == undefined) + i = lastStepsRowIndex; + const steps = getSteps(); + if (steps != undefined) { + writeLineTopic("STEP", i); + writeLine(steps, i); + } + lastStepsRowIndex = i; +} + +function drawHeartRate(i) { + if (!showHeartRate) return; + if (i == undefined) + i = lastHeartRateRowIndex; + writeLineTopic("HRTM", i); + if (hrtValue != undefined) { + if (!hrtValueIsOld) + writeLine(hrtValue,i); + else + writeLine(hrtValue,i, topicColor); + } + lastHeartRateRowIndex = i; +} + + +function writeLineTopic(str, line) { + var y = marginTop+line*fontheight; + g.setFont(font,fontsize); + g.setColor(topicColor).setFontAlign(-1,-1); + + g.clearRect(0,y,g.getWidth(),y+fontheight-1); + g.drawString("[" + str + "]",marginLeftTopic,y); +} + +function writeLine(str,line,pColor){ + if (pColor == undefined) + pColor = textColor; + var y = marginTop+line*fontheight; + g.setFont(font,fontsize); + g.setColor(pColor).setFontAlign(-1,-1); + g.drawString(str,marginLeftData,y); +} + + +function getSteps() { + var steps = 0; + let health; + try { + health = require("health"); + } catch (e) { + // Module health not found + } + if (health != undefined) { + health.readDay(new Date(), h=>steps+=h.steps); + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } else if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom.getSteps(); + } + return steps; +} + +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} + +// EVENTS: + +// turn on HRM when the LCD is unlocked +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + if (showHeartRate) { + Bangle.setHRMPower(1,"clicompleteclk"); + if (hrtValue == undefined) + hrtValue = "..."; + else + hrtValueIsOld = true; + } + } else { + if (showHeartRate) { + hrtValueIsOld = true; + Bangle.setHRMPower(0,"clicompleteclk"); + } + } + // Update steps and heart rate + drawSteps(); + drawHeartRate(); +}); + +Bangle.on('lcdPower',function(on) { + if (on) { + drawAll(true); + } else { + if (showHeartRate) { + hrtValueIsOld = true; + } + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +if (showHeartRate) { + Bangle.on('HRM', function(hrm) { + //if(hrm.confidence > 90){ + hrtValueIsOld = false; + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + //} else { + // hrtValue = undefined; + //} + }); +} + +g.clear(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +loadSettings(); +drawAll(true); diff --git a/apps/clicompleteclk/app.png b/apps/clicompleteclk/app.png new file mode 100644 index 000000000..104e6124a Binary files /dev/null and b/apps/clicompleteclk/app.png differ diff --git a/apps/clicompleteclk/settings.js b/apps/clicompleteclk/settings.js new file mode 100644 index 000000000..2df20ed3e --- /dev/null +++ b/apps/clicompleteclk/settings.js @@ -0,0 +1,54 @@ +(function(back) { + const storage = require('Storage'); + let settings = storage.readJSON('clicompleteclk.json', 1) || {}; + function save(key, value) { + settings[key] = value; + storage.write('clicompleteclk.json', settings); + } + E.showMenu({ + '': { 'title': 'CLI complete clk' }, + 'Show battery': { + value: "battery" in settings ? settings.battery : false, + format: () => (settings.battery ? 'Yes' : 'No'), + onchange: () => { + settings.battery = !settings.battery; + save('battery', settings.battery); + }, + }, + 'Battery warn': { + value: "batteryLvl" in settings ? settings.batteryLvl : 30, + min: 0, + max : 100, + step: 10, + format: x => { + return x + "%"; + }, + onchange: x => save('batteryLvl', x), + }, + 'Show weather': { + value: "weather" in settings ? settings.weather : false, + format: () => (settings.weather ? 'Yes' : 'No'), + onchange: () => { + settings.weather = !settings.weather; + save('weather', settings.weather); + }, + }, + 'Show steps': { + value: "steps" in settings ? settings.steps : false, + format: () => (settings.steps ? 'Yes' : 'No'), + onchange: () => { + settings.steps = !settings.steps; + save('steps', settings.steps); + }, + }, + 'Show heartrate': { + value: "heartrate" in settings ? settings.heartrate : false, + format: () => (settings.heartrate ? 'Yes' : 'No'), + onchange: () => { + settings.heartrate = !settings.heartrate; + save('heartrate', settings.heartrate); + }, + }, + '< Back': back, + }); +}); diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog index 2a93a0d5f..68249b622 100644 --- a/apps/cliock/ChangeLog +++ b/apps/cliock/ChangeLog @@ -7,3 +7,4 @@ 0.13: Use setUI, work with smaller screens and themes 0.14: Fix BTN1 (fix #853) Add light/dark theme support +0.15: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) diff --git a/apps/cliock/app.js b/apps/cliock/app.js index 0fd6ea580..d9271bf15 100644 --- a/apps/cliock/app.js +++ b/apps/cliock/app.js @@ -183,9 +183,6 @@ Bangle.on('HRM', function(hrm) { }); g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -drawAll(); Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); @@ -195,4 +192,7 @@ Bangle.setUI("clockupdown", btn=>{ if (btn<0) changeInfoMode(); if (btn>0) changeFunctionMode(); drawAll(); -}); \ No newline at end of file +}); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(); diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog new file mode 100644 index 000000000..c7b309a74 --- /dev/null +++ b/apps/coretemp/ChangeLog @@ -0,0 +1 @@ +0.1: New app diff --git a/apps/coretemp/README.md b/apps/coretemp/README.md new file mode 100644 index 000000000..fac25df21 --- /dev/null +++ b/apps/coretemp/README.md @@ -0,0 +1,20 @@ +# CoreTemp display + +Basic bare-bones example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current body core temperature readings. + +## Usage + +On startup connects to a CoreTemp device (1809/2A1C) and emits a "Core, temp" value for each reading. +The app simply displays these readings on screen. + +## TODO + +* Integrate with other tracking/sports apps to log data. +* Add device selection +* Provide enable/disable option +* Check status, add Retry/reconnect +* Also provide skin temp reading + +## Creator + +Ivor Hewitt diff --git a/apps/coretemp/boot.js b/apps/coretemp/boot.js new file mode 100644 index 000000000..59e227dad --- /dev/null +++ b/apps/coretemp/boot.js @@ -0,0 +1,23 @@ +(function() { + var gatt; + + //Would it be better to scan by uuid rather than name? + NRF.requestDevice({ timeout: 20000, filters: [{ name: 'CORE [a]' }] }).then(function(device) { + return device.gatt.connect(); + }).then(function(g) { + gatt = g; + return gatt.getPrimaryService("1809"); + }).then(function(service) { + return service.getCharacteristic("2A1C"); + }).then(function(characteristic) { + characteristic.on('characteristicvaluechanged', function(event) { + var dv = event.target.value; + var core = (dv.buffer[2]*256+dv.buffer[1])/100; + Bangle.emit('Core',{ + temp:core + }); + }); + return characteristic.startNotifications(); + }).then(function() { + }); +})(); diff --git a/apps/coretemp/coretemp-icon.js b/apps/coretemp/coretemp-icon.js new file mode 100644 index 000000000..5f36b9090 --- /dev/null +++ b/apps/coretemp/coretemp-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///k0DxUFgsDCY8KwAfJlQLHhWglWq1WgBIcCA4QCB1WoComq0+iBYWqCwl//4OBAAQxChWlv/2BYIlCBYUqv9VvQLBwA9BBYWlqtV/QLBGoRIBgQLBr9aBYQ2BBYMKroLBtQLCgALClIKC1AXG1NVuoFBF4sC09V+woCBAJHCgWXq9oPQZrDgWdq9gBZG9rqgCTwSbCgVVqysDBYkK6tWYoa/DkEJ6vaaIgWBaAILCbQhUCBYXoc4wNBBZWqBfBtB1ALKKZILCR4J3FToQLBU4KPEWoQLNZYILIa4NVcYReEcYOnqtaDAbvDgALBcg4EBlNVqtqDoOgd4YoBBYNWytWCwQdCgQLBAAVaBYkA0oLDuwLFkv1BgZGDAAMJuoKCroWEGAOnDAVftShGr////1tDdG14LB+wiEAAdqHAjTHBYgA==")) diff --git a/apps/coretemp/coretemp.js b/apps/coretemp/coretemp.js new file mode 100644 index 000000000..226508c83 --- /dev/null +++ b/apps/coretemp/coretemp.js @@ -0,0 +1,19 @@ +Bangle.setLCDPower(1); +Bangle.setLCDTimeout(0); +var btm = g.getHeight()-1; + +function onCore(c) { + var px = g.getWidth()/2; + g.setFontAlign(0,0); + g.clearRect(0,24,g.getWidth(),80); + var str = c.temp + "C"; + g.setFontVector(40).drawString(str,px,45); +} +Bangle.on('Core', onCore); + +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); diff --git a/apps/coretemp/coretemp.png b/apps/coretemp/coretemp.png new file mode 100644 index 000000000..a573828f8 Binary files /dev/null and b/apps/coretemp/coretemp.png differ diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index c3102b4b9..c414c1ddc 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -3,3 +3,5 @@ 0.03: cycle thru pages 0.04: reset to clock after 2 mins of inactivity 0.05: add Bangle 2 version +0.06: Adds settings page (hide clocks or launchers) +0.06: Adds setting for directly launching app on touch for Bangle 2 diff --git a/apps/dtlaunch/app-b1.js b/apps/dtlaunch/app-b1.js index 9bbf3e219..ec0569127 100644 --- a/apps/dtlaunch/app-b1.js +++ b/apps/dtlaunch/app-b1.js @@ -2,6 +2,11 @@ * */ +var settings = Object.assign({ + showClocks: true, + showLaunchers: true, +}, require('Storage').readJSON("dtlaunch.json", true) || {}); + function wdog(handle,timeout){ if(handle !== undefined){ wdog.handle = handle; @@ -17,7 +22,13 @@ function wdog(handle,timeout){ wdog(load,120000) var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +var apps = s.list(/\.info$/).map(app=>{ + var a=s.readJSON(app,1); + return a && { + name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src + };}).filter( + app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); + apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index 674fe3677..800ec456c 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -2,8 +2,20 @@ * */ +var settings = Object.assign({ + showClocks: true, + showLaunchers: true, + direct: false, +}, require('Storage').readJSON("dtlaunch.json", true) || {}); + var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +var apps = s.list(/\.info$/).map(app=>{ + var a=s.readJSON(app,1); + return a && { + name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src + };}).filter( + app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); + apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first @@ -28,7 +40,7 @@ const YOFF = 30; function draw_icon(p,n,selected) { var x = (n%2)*72+XOFF; var y = n>1?72+YOFF:YOFF; - (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+10,y+2,x+60,y+52); + (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52); g.clearRect(x+12,y+4,x+59,y+51); g.setColor(g.theme.fg); try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){} @@ -52,7 +64,7 @@ function drawPage(p){ } for (var i=0;i<4;i++) { if (!apps[p*4+i]) return i; - draw_icon(p,i,selected==i); + draw_icon(p,i,selected==i && !settings.direct); } g.flip(); } @@ -81,9 +93,9 @@ Bangle.on("touch",(_,p)=>{ for (i=0;i<4;i++){ if((page*4+i)=0) { - if (selected!=i){ + draw_icon(page,i,true && !settings.direct); + if (selected>=0 || settings.direct) { + if (selected!=i && !settings.direct){ draw_icon(page,selected,false); } else { load(apps[page*4+i].src); diff --git a/apps/dtlaunch/settings-b1.js b/apps/dtlaunch/settings-b1.js new file mode 100644 index 000000000..f3101da16 --- /dev/null +++ b/apps/dtlaunch/settings-b1.js @@ -0,0 +1,33 @@ +(function(back) { + var FILE = "dtlaunch.json"; + + var settings = Object.assign({ + showClocks: true, + showLaunchers: true + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Desktop launcher" }, + "< Back" : () => back(), + 'Show clocks': { + value: settings.showClocks, + format: v => v?"On":"Off", + onchange: v => { + settings.showClocks = v; + writeSettings(); + } + }, + 'Show launchers': { + value: settings.showLaunchers, + format: v => v?"On":"Off", + onchange: v => { + settings.showLaunchers = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js new file mode 100644 index 000000000..7f667d213 --- /dev/null +++ b/apps/dtlaunch/settings-b2.js @@ -0,0 +1,42 @@ +(function(back) { + var FILE = "dtlaunch.json"; + + var settings = Object.assign({ + showClocks: true, + showLaunchers: true, + direct: false + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Desktop launcher" }, + "< Back" : () => back(), + 'Show clocks': { + value: settings.showClocks, + format: v => v?"On":"Off", + onchange: v => { + settings.showClocks = v; + writeSettings(); + } + }, + 'Show launchers': { + value: settings.showLaunchers, + format: v => v?"On":"Off", + onchange: v => { + settings.showLaunchers = v; + writeSettings(); + } + }, + 'Direct launch': { + value: settings.direct, + format: v => v?"On":"Off", + onchange: v => { + settings.direct = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/emojuino/ChangeLog b/apps/emojuino/ChangeLog index 1c99f1970..04367183f 100644 --- a/apps/emojuino/ChangeLog +++ b/apps/emojuino/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Upgraded text to images, added welcome screen and subtitles. +0.03: Advertise app name as Espruino manufacturer data when idle. diff --git a/apps/emojuino/emojuino.js b/apps/emojuino/emojuino.js index 5b7670652..d241063e6 100644 --- a/apps/emojuino/emojuino.js +++ b/apps/emojuino/emojuino.js @@ -32,6 +32,7 @@ const CYCLE_BUZZ_MILLISECONDS = 50; const WELCOME_MESSAGE = 'Emojuino:\r\n\r\n< Swipe >\r\nto select\r\n\r\nTap\r\nto transmit'; // Non-user-configurable constants +const APP_ID = 'emojuino'; const IMAGE_INDEX = 0; const CODE_POINT_INDEX = 1; const EMOJI_PX = 96; @@ -40,12 +41,11 @@ const EMOJI_Y = (g.getHeight() - EMOJI_PX) / 2; const TX_X = 68; const TX_Y = 12; const FONT_SIZE = 24; -const BTN_WATCH_OPTIONS = { repeat: true, debounce: 20, edge: "falling" }; +const ESPRUINO_COMPANY_CODE = 0x0590; const UNICODE_CODE_POINT_ELIDED_UUID = [ 0x49, 0x6f, 0x49, 0x44, 0x55, 0x54, 0x46, 0x2d, 0x33, 0x32 ]; - // Global variables let emojiIndex = 0; let isToggleOn = false; @@ -100,9 +100,22 @@ function transmitEmoji(image, codePoint, duration) { } +// Transmit the app name under the Espruino company code to facilitate discovery +function transmitAppName() { + let options = { + showName: false, + manufacturer: ESPRUINO_COMPANY_CODE, + manufacturerData: JSON.stringify({ name: APP_ID }), + interval: 2000 + } + + NRF.setAdvertising({}, options); +} + + // Terminate the emoji transmission function terminateEmoji(displayIntervalId) { - NRF.setAdvertising({ }); + transmitAppName(); isTransmitting = false; clearInterval(displayIntervalId); drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], false); @@ -169,3 +182,4 @@ g.setFontAlign(0, 0); g.drawString(WELCOME_MESSAGE, g.getWidth() / 2, g.getHeight() / 2); Bangle.on('touch', handleTouch); Bangle.on('drag', handleDrag); +transmitAppName(); diff --git a/apps/flow/README.md b/apps/flow/README.md new file mode 100644 index 000000000..caeaf92d9 --- /dev/null +++ b/apps/flow/README.md @@ -0,0 +1,12 @@ +# FLOW + +This is a game where you have to help a flow avoid white obstacles thing by tapping! +This is a demake of an app which I forgot the name of. +Press BTN(1) to restart. +See if you can get to 2500 score! + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) diff --git a/apps/flow/app-icon.js b/apps/flow/app-icon.js new file mode 100644 index 000000000..969a608f4 --- /dev/null +++ b/apps/flow/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AwX48EHgEC1WgCQkVqoDBBfuqBQcBqoLagEqGAguBqALaGAOoAoQuEBbEAKgIMBBQNUBbgMCyoKHBbBVBBYIKGBbEBtNVrQLfOgNaT4gLagp0CPQOABbcBFwNAgEKBgILbitVqAFClWq0ALZFwTDFGAQLZFwYwDBfg")) diff --git a/apps/flow/app.js b/apps/flow/app.js new file mode 100644 index 000000000..5f4da8f35 --- /dev/null +++ b/apps/flow/app.js @@ -0,0 +1,220 @@ +const isB2 = process.env.HWVERSION === 2; + +// Bangle.js 1 runs just too fast in direct mode??? (also no getPixel) +if (!isB2) Bangle.setLCDMode("120x120"); + +const options = Bangle.getOptions(); + +options.lockTimeout = 0; +options.lcdPowerTimeout = 0; + +Bangle.setOptions(options); + +g.reset(); +g.setBgColor(0, 0, 0); +g.setColor(255, 255, 255); +g.clear(); +const h = g.getHeight(); + +function trigToCoord(ret) { + return ((ret + 1) * h) / 2; +} + +function trigToLen(ret) { + return (ret * h) / 2; +} + +let i = 0.2; +let speedCoef = 0.014; + +let flowFile = require("Storage").readJSON("flow.json"); + +let highestI = (flowFile && flowFile.hiscore) || 0.1; + +let colorA = [255, 255, 0]; +let colorB = [0, 255, 255]; + +let x = 0; +let xt = 0; +let safeMode = false; +let lost = false; + +function offsetRect(g, x, y, w) { + g.fillRect(x, y, x + w, y + w); +} + +function getColor(num) { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 1, 0], + [0, 1, 1], + [1, 0, 1], + [0.5, 0.5, 1], + [1, 0.5, 0], + [0, 1, 0.5], + [0.5, 0.5, 0.5], + ][num]; +} + +function calculateColor(num) { + colorA = getColor(Math.floor((num % 1) * 10)); + colorB = getColor(Math.floor((num % 10) - (num % 1))); +} + +calculateColor(highestI); + +Bangle.on("touch", () => (safeMode = !safeMode)); + +function resetGame() { + x = xt = 0; + safeMode = lost = false; + i = 0.2; + speedCoef = 0.014; + obstaclePeriod = 150; + obstacleMode = 1; + g.clear(); + shownScore = false; + intervalId = setInterval(draw); +} + +function checkCollision() { + lost = g.getPixel(trigToCoord(+x), (h * 2) / 3 - 4) !== 0; + if (lost) { + scoringI = i; + speedCoef = Math.min(speedCoef, 0.02); + g.setFont(isB2 ? "6x15" : "4x6", 3); + g.setColor(colorA[0], colorA[1], colorA[2]) + .drawString( + "Game over", + trigToCoord(0) - g.stringWidth("Game over") / 2, + trigToCoord(0) + ) + .setColor(1, 1, 1); + } +} + +function drawPlayer() { + if (!safeMode) xt = Math.cos(i * Math.PI * 4) / 7.5; + else xt = -Math.cos(i * Math.PI * 2) / 20 + 0.35; + x = x * 0.8 + xt * 0.2; + if (highestI > 250) calculateColor(i); + g.setColor(colorA[0], colorA[1], colorA[2]); + offsetRect(g, trigToCoord(+x), (h * 2) / 3, 3); + g.setColor(colorB[0], colorB[1], colorB[2]); + offsetRect(g, trigToCoord(-x), (h * 2) / 3, 3); +} + +let obstaclePeriod = 150; +let obstacleMode = 1; + +function drawObstracle() { + g.setColor(1, 1, 1); + switch (obstacleMode) { + case 0: + offsetRect(g, trigToCoord(-0.15), 0, trigToLen(0.3)); + break; + case 1: + offsetRect(g, trigToCoord(0.2), 0, trigToLen(0.2)); + offsetRect(g, trigToCoord(-0.4), 0, trigToLen(0.2)); + break; + case 2: + break; + } + obstaclePeriod--; + if (obstaclePeriod <= 0) { + // If we are off cooldown mode, pick a random actual mode + if (obstacleMode === 2) { + obstaclePeriod = Math.random() * 50 + 50; + obstacleMode = Math.round(Math.random()); + } else if (Math.random() > 0.5) { + // Give it a chance to repeat with no cooldown + obstaclePeriod = 25 + 2.5 * speedCoef; + obstacleMode = 2; + } + } +} + +let shownScore = false; +let scoringI = 0; + +function draw() { + if (!lost) { + drawPlayer(); + checkCollision(); + speedCoef *= 1.0005; + drawObstracle(); + } else { + speedCoef /= 1.05; + if (speedCoef <= 0.005) { + clearInterval(intervalId); + i -= speedCoef; + g.setFont(isB2 ? "6x15" : "4x6", 1); + const str = "Hiscore: " + Math.round(highestI * 10); + g.setColor( + scoringI > highestI ? 0 : 255, + 0, + scoringI > highestI ? 255 : 0 + ) + .drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) + ) + .setColor(255, 255, 255); + if (scoringI > highestI) { + highestI = scoringI; + require("Storage").writeJSON("flow.json", { + hiscore: highestI, + }); + calculateColor(highestI); + } + setTimeout(resetGame, 3000); + } else if (speedCoef <= 0.01 && !shownScore) { + shownScore = true; + g.setFont(isB2 ? "6x15" : "4x6", 2); + const str = "Score: " + Math.round(scoringI * 10); + g.setColor(colorB[0], colorB[1], colorB[2]) + .drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) + ) + .setColor(1, 1, 1); + } + } + i += speedCoef; + g.scroll(0, speedCoef * h); + g.flip(); +} + +let intervalId; + +if (BTN.read()) { + for (let i = 0; i < 10; i++) { + color = getColor(i); + g.setColor(color[0], color[1], color[2]); + g.fillRect((i / 10) * h, 0, ((i + 1) / 10) * h, h); + } + g.setColor(0); + g.setFont("Vector", 9); + let str = "Welcome to the debug screen!"; + g.drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) - 9 + ); + str = "Don't hold BTN while opening to play!"; + g.drawString(str, trigToCoord(0) - g.stringWidth(str) / 2, trigToCoord(0)); + g.flip(); + setInterval(() => { + g.scroll(0, 0.014 * h); + i += 0.014; + calculateColor(i); + g.setColor(colorA[0], colorA[1], colorA[2]); + g.fillRect(0, 0, trigToCoord(0), 0.014 * h); + g.setColor(colorB[0], colorB[1], colorB[2]); + g.fillRect(trigToCoord(0), 0, trigToCoord(1), 0.014 * h); + }, 1000 / 30); +} else intervalId = setInterval(draw, 1000 / 30); diff --git a/apps/flow/app.png b/apps/flow/app.png new file mode 100644 index 000000000..b35c3ca77 Binary files /dev/null and b/apps/flow/app.png differ diff --git a/apps/flow/screenshot1.png b/apps/flow/screenshot1.png new file mode 100644 index 000000000..fd5dee427 Binary files /dev/null and b/apps/flow/screenshot1.png differ diff --git a/apps/flow/screenshot2.png b/apps/flow/screenshot2.png new file mode 100644 index 000000000..e29691b69 Binary files /dev/null and b/apps/flow/screenshot2.png differ diff --git a/apps/flow/screenshot3.png b/apps/flow/screenshot3.png new file mode 100644 index 000000000..3e1c80ba7 Binary files /dev/null and b/apps/flow/screenshot3.png differ diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog index ec66c5568..96e7e4e9b 100644 --- a/apps/fwupdate/ChangeLog +++ b/apps/fwupdate/ChangeLog @@ -1 +1,4 @@ 0.01: Initial version +0.02: Add support for ZIPs + Find and download ZIPs direct from the Espruino website + Take 'beta' tag off diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 7230a77a8..8c2008e54 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -4,28 +4,45 @@

THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE - INSTRUCTIONS FOR BANGLE.JS 1 AND BANGLE.JS 2

+ INSTRUCTIONS FOR BANGLE.JS 1 AND BANGLE.JS 2. For usage on Bangle.js 2 you'll likely need to have an updated bootloader.

-

Firmware updates using the App Loader are only possible on +

Firmware updates using the App Loader are only possible on Bangle.js 2. For firmware updates on Bangle.js 1 please - see the Bangle.js 1 instructions

+ see the Bangle.js 1 instructions

+

Your current firmware version is unknown

+

Firmware updates via this tool work differently to the NRF Connect method mentioned on + the Bangle.js page. Firmware + is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies + the new firmware into internal Storage.

+

 
     
+    
+    
 
     
   
diff --git a/apps/gbdebug/ChangeLog b/apps/gbdebug/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/gbdebug/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/gbdebug/README.md b/apps/gbdebug/README.md
new file mode 100644
index 000000000..47b1525b8
--- /dev/null
+++ b/apps/gbdebug/README.md
@@ -0,0 +1,26 @@
+# Gadgetbridge Debug
+
+This is useful if your Bangle isn't responding to the Gadgetbridge
+Android app properly.
+
+This app disables all existing Gadgetbridge handlers and then displays the
+messages that come from Gadgetbridge on the screen
+of the watch. It also saves the last 10 messages in a variable
+called `history`.
+
+More info on Gadgetbridge at http://www.espruino.com/Gadgetbridge
+
+## Usage
+
+* Run the `GB Debug` app on your Bangle
+* Connect your Bangle to Gadgetbridge
+* Do whatever was causing you problems (eg receiving a call)
+* The Gadgetbridge message should now be displayed on-screen
+
+If you want to get the *actual* data rather than copying it from the screen.
+
+* Ensure the `GB Debug` app is kept running after the above steps
+* Disconnect Gadgetbridge from the Bangle
+* Connect the Web IDE on your PC
+* Type `show()` on the left-hand side of the IDE and the
+last 10 messages from Gadgetbridge will be shown.
diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js
new file mode 100644
index 000000000..a701ef3a9
--- /dev/null
+++ b/apps/gbdebug/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))
diff --git a/apps/gbdebug/app.js b/apps/gbdebug/app.js
new file mode 100644
index 000000000..ee5e46999
--- /dev/null
+++ b/apps/gbdebug/app.js
@@ -0,0 +1,21 @@
+E.showMessage("Waiting for message");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var history = [];
+
+GB = function(e) {
+  if (history.length > 10)
+    history = history.slice(history.length-10);
+  history.push(e);
+
+  var s = JSON.stringify(e,null,2);
+
+  g.reset().clear(Bangle.appRect);
+  g.setFont("6x8").setFontAlign(-1,0);
+  g.drawString(s, 10, g.getHeight()/2);
+};
+
+function show() {
+  print(JSON.stringify(history,null,2));
+}
diff --git a/apps/gbdebug/app.png b/apps/gbdebug/app.png
new file mode 100644
index 000000000..f70bce7ad
Binary files /dev/null and b/apps/gbdebug/app.png differ
diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog
index 9cebf0a31..316b98a84 100644
--- a/apps/gbmusic/ChangeLog
+++ b/apps/gbmusic/ChangeLog
@@ -5,3 +5,4 @@
 0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker
 0.06: Bangle.js 2 support
 0.07: Fix "previous" button image
+0.08: Fix scrolling title background color
diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js
index f514dfccd..1bddf70f7 100644
--- a/apps/gbmusic/app.js
+++ b/apps/gbmusic/app.js
@@ -91,7 +91,7 @@ function rScroller(l) {
     y = l.y+l.h/2;
   l.offset = l.offset%w;
   g.setClipRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
-    .setColor(l.col)
+    .setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
     .setFontAlign(-1, 0) // left center
     .clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
     .drawString(l.label, l.x-l.offset+40, y)
diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog
index fddb1eb80..67d421f33 100644
--- a/apps/gbridge/ChangeLog
+++ b/apps/gbridge/ChangeLog
@@ -24,3 +24,5 @@
 0.22: Respect Quiet Mode
 0.23: Allow notification dismiss to remove from phone too
 0.24: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799)
+0.25: workaround call notification
+	  Fix inflated step number
diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js
index 53f832b07..7cb7147ec 100644
--- a/apps/gbridge/widget.js
+++ b/apps/gbridge/widget.js
@@ -184,7 +184,7 @@
       case "call":
         var note = { size: 55, title: event.name, id: "call",
                      body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))}
-        if (event.cmd === "incoming") {
+        if (event.cmd === "incoming" || event.cmd === "") {
           require("notify").show(note);
           if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
             Bangle.buzz();
@@ -262,7 +262,7 @@
   // Send a summary of activity to Gadgetbridge
   function sendActivity(hrm) {
     var steps = currentSteps - lastSentSteps;
-    lastSentSteps = 0;
+    lastSentSteps = currentSteps;
     gbSend({ t: "act", stp: steps, hrm:hrm });
   }
 
diff --git a/apps/golfscore/ChangeLog b/apps/golfscore/ChangeLog
new file mode 100644
index 000000000..4995dd59a
--- /dev/null
+++ b/apps/golfscore/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: multiple player score support
\ No newline at end of file
diff --git a/apps/golfscore/README.md b/apps/golfscore/README.md
new file mode 100644
index 000000000..68552ad4b
--- /dev/null
+++ b/apps/golfscore/README.md
@@ -0,0 +1,37 @@
+# Golf Score
+
+Lets you keep track of strokes during a game of Golf.
+
+![](mainmenu.png)
+![](setupmenu.png)
+![](scorecard.png)
+![](holemenu.png)
+
+## Usage
+
+1. Open the app,
+1. scroll to setup
+2. set the number of holes (18 by default, but can be configured)
+3. set the number of players (4 by default, but can be 1-20)
+4. click back
+5. scroll to a hole (hole 1)
+6. scroll to a player and set the number of strokes they took (repeat as needed)
+7. click next hole and repeat #6 and #7 as needed; or click back
+8. at any time, check the score card for a sum total of all the strokes for each player
+
+## Features
+
+Track strokes for multiple players (1-20)
+Set number of holes on course
+
+## Controls
+
+N/A
+
+## Requests
+
+Michael Salaverry (github.com/barakplasma)
+
+## Creator
+
+Michael Salaverry
diff --git a/apps/golfscore/app-icon.js b/apps/golfscore/app-icon.js
new file mode 100644
index 000000000..238001688
--- /dev/null
+++ b/apps/golfscore/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIEBgOABQcD4AFDg1wAokYDokOAokDDwkBDwkADwn4nAFD/geDgP8gYFEDwn8gFgDocA+AFCkE/A4IABg//Aoc//4RDn/+Goc/8AFJj4FLEQYFGh4FLIAYFGg4FKh5sBApEfnhTEAok+Aol8vihEAon4AocB+F4ZQYFF8AFDg/AAocPAouAKYcfXQQFHjzEEhjvDA"))
diff --git a/apps/golfscore/app.js b/apps/golfscore/app.js
new file mode 100644
index 000000000..7c5c2d0e8
--- /dev/null
+++ b/apps/golfscore/app.js
@@ -0,0 +1,113 @@
+// @ts-check
+// @ts-ignore
+const menu = require("graphical_menu");
+/**
+ * @type {{showMenu: (config) => void}}
+ */
+let E;
+/**
+ * @type {{clear: () => void}}
+ */
+let g;
+
+let holes_count = 18;
+let player_count = 4;
+/**
+ * @type {number[][]}
+ */
+let course = new Array(holes_count).map(() => new Array(player_count).fill(0));
+
+const main_menu = {
+  "": {
+    "title": "-- Golf --"
+  },
+  "Setup": function () { E.showMenu(setup_menu); },
+  "Score Card": function () {
+    calculate_score();
+    E.showMenu(score_card);
+  },
+};
+
+function calculate_score() {
+  let scores = course.reduce((acc, hole) => {
+    hole.forEach((stroke_count, player) => {
+      acc[player] = acc[player]+stroke_count;
+    });
+    return acc;
+  }, new Array(player_count).fill(0));
+
+  score_card = {
+    "": {
+      "title": "score card"
+    },
+    "< Back": function () { E.showMenu(main_menu); },
+  };
+
+  for (let player = 0; player < player_count; player++) {
+    score_card["Player - " + (player + 1)] = {
+      value: scores[player]
+    };
+  }
+}
+
+let score_card = {};
+
+const setup_menu = {
+  "": {
+    "title": "-- Golf Setup --"
+  },
+  "Holes": {
+    value: holes_count,
+    min: 1, max: 20, step: 1, wrap: true,
+    onchange: v => { holes_count = v; add_holes(); }
+  },
+  "Players": {
+    value: player_count,
+    min: 1, max: 10, step: 1, wrap: true,
+    onchange: v => { player_count = v; }
+  },
+  "< Back": function () { E.showMenu(main_menu); },
+};
+
+function inc_hole(i, player) { return function (v) { course[i][player] = v; }; }
+
+function add_holes() {
+  for (let j = 0; j < 20; j++) {
+    delete main_menu["Hole - " + (j + 1)];
+  }
+  for (let i = 0; i < holes_count; i++) {
+    course[i] = new Array(player_count).fill(0);
+    main_menu["Hole - " + (i + 1)] = goto_hole_menu(i);
+  }
+  E.showMenu(main_menu);
+}
+
+function goto_hole_menu(i) {
+  return function () {
+    E.showMenu(hole_menu(i));
+  };
+}
+
+function hole_menu(i) {
+  let menu = {
+    "": {
+      "title": `-- Hole ${i + 1}--`
+    },
+    "Next hole": goto_hole_menu(i + 1),
+    "< Back": function () { E.showMenu(main_menu); },
+  };
+
+  for (let player = 0; player < player_count; player++) {
+    menu[`player - ${player + 1}`] = {
+      value: course[i][player],
+      min: 1, max: 20, step: 1, wrap: true,
+      onchange: inc_hole(i, player)
+    };
+  }
+
+  return menu;
+}
+
+// @ts-ignore
+g.clear();
+add_holes();
\ No newline at end of file
diff --git a/apps/golfscore/app.png b/apps/golfscore/app.png
new file mode 100644
index 000000000..fc5d51557
Binary files /dev/null and b/apps/golfscore/app.png differ
diff --git a/apps/golfscore/holemenu.png b/apps/golfscore/holemenu.png
new file mode 100644
index 000000000..ac214f182
Binary files /dev/null and b/apps/golfscore/holemenu.png differ
diff --git a/apps/golfscore/mainmenu.png b/apps/golfscore/mainmenu.png
new file mode 100644
index 000000000..3ebeb0ca7
Binary files /dev/null and b/apps/golfscore/mainmenu.png differ
diff --git a/apps/golfscore/scorecard.png b/apps/golfscore/scorecard.png
new file mode 100644
index 000000000..9e7ff1130
Binary files /dev/null and b/apps/golfscore/scorecard.png differ
diff --git a/apps/golfscore/setupmenu.png b/apps/golfscore/setupmenu.png
new file mode 100644
index 000000000..13158e2e7
Binary files /dev/null and b/apps/golfscore/setupmenu.png differ
diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog
index cb22dd13f..365405846 100644
--- a/apps/gpsrec/ChangeLog
+++ b/apps/gpsrec/ChangeLog
@@ -28,3 +28,4 @@
 0.24: Better support for Bangle.js 2, avoid widget area for Graphs, smooth graphs more
 0.25: Fix issue where if Bangle.js 2 got a GPS fix but no reported time, errors could be caused by the widget (fix #935)
 0.26: Multiple bugfixes
+0.27: Map drawing with light theme (fix #1023) 
diff --git a/apps/gpsrec/README.md b/apps/gpsrec/README.md
index 72f744452..71b934111 100644
--- a/apps/gpsrec/README.md
+++ b/apps/gpsrec/README.md
@@ -8,3 +8,6 @@ This app allows you to record a GPS track. It can run in background. The data ca
 
 When you turn on recording, a widget badge that looks like a satellite will appear immediately at the top of the screen. However, the recording does not begin immediately. It usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). You will notice a blinking question mark at the lower left of the badge indicating currently getting a fix. The badge will change when a GPS fix is achieved and that is when the app actually starts writing data to the log file. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
 
+## Viewing a track
+
+![](screenshot.png)
diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js
index df3353930..833a816ea 100644
--- a/apps/gpsrec/app.js
+++ b/apps/gpsrec/app.js
@@ -197,15 +197,14 @@ function plotTrack(info) {
   g.setColor(1,0.5,0.5);
   g.setFont("Vector",16);
   g.drawString("Track"+info.fn.toString()+" - Loading",10,220);
-  g.setColor(0,0,0);
+  g.setColor(g.theme.bg);
   g.fillRect(0,220,239,239);
   if (!info.qOSTM) {
     g.setColor(1, 0, 0);
     g.fillRect(9,80,11,120);
     g.fillPoly([9,60,19,80,0,80]);
-    g.setColor(1,1,1);
+    g.setColor(g.theme.fg);
     g.drawString("N",2,40);
-    g.setColor(1,1,1);
   } else {
     osm.lat = info.lat;
     osm.lon = info.lon;
@@ -228,7 +227,7 @@ function plotTrack(info) {
   g.setColor(0,1,0);
   g.fillCircle(mp.x,mp.y,5);
   if (info.qOSTM) g.setColor(1,0,0.55);
-  else g.setColor(1,1,1);
+  else g.setColor(g.theme.fg);
   l = f.readLine(f);
   while(l!==undefined) {
     c = l.split(",");
@@ -248,11 +247,11 @@ function plotTrack(info) {
   g.setColor(1,0,0);
   g.fillCircle(ox,oy,5);
   if (info.qOSTM) g.setColor(0, 0, 0);
-  else g.setColor(1,1,1);
+  else g.setColor(g.theme.fg);
   g.drawString(require("locale").distance(dist),g.getWidth() / 2, g.getHeight() - 20);
   g.setFont("6x8",2);
   g.setFontAlign(0,0,3);
-  g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40);
+  g.drawString("Back",g.getWidth() - 10, g.getHeight()/2);
   setWatch(function() {
     viewTrack(info.fn, info);
   }, global.BTN3||BTN1);
diff --git a/apps/gpsrec/screenshot.png b/apps/gpsrec/screenshot.png
new file mode 100644
index 000000000..f6e001749
Binary files /dev/null and b/apps/gpsrec/screenshot.png differ
diff --git a/apps/gpstouch/README.md b/apps/gpstouch/README.md
index 7329f9833..172b5da57 100644
--- a/apps/gpstouch/README.md
+++ b/apps/gpstouch/README.md
@@ -14,3 +14,5 @@
 ![](screenshot2.png)
 ![](screenshot3.png)
 ![](screenshot4.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/hebrew_calendar/ChangeLog b/apps/hebrew_calendar/ChangeLog
new file mode 100644
index 000000000..fdd29db66
--- /dev/null
+++ b/apps/hebrew_calendar/ChangeLog
@@ -0,0 +1,4 @@
+0.01: New App!
+0.02: using TS and rollup to bundle
+0.03: bug fixes and support bangle 1
+0.04: removing TS
\ No newline at end of file
diff --git a/apps/hebrew_calendar/LICENSE b/apps/hebrew_calendar/LICENSE
new file mode 100644
index 000000000..bdcdec9e4
--- /dev/null
+++ b/apps/hebrew_calendar/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+Copyright (c) 2021 Michael Salaverry
+Copyright (c) 2016-20 Ionică Bizău  (https://ionicabizau.net)
+
+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.
\ No newline at end of file
diff --git a/apps/hebrew_calendar/README.md b/apps/hebrew_calendar/README.md
new file mode 100644
index 000000000..7a96a97db
--- /dev/null
+++ b/apps/hebrew_calendar/README.md
@@ -0,0 +1,26 @@
+# Hebrew Calendar
+
+Displays the current hebrew calendar date
+Add screen shots (if possible) to the app folder and link then into this file with ![](.png)
+
+## Usage
+
+Open the app, and it shows a menu with the date components
+
+## Features
+
+Shows the hebrew date, month, and year; alongside the gregorian date
+
+## Controls
+
+Name the buttons and what they are used for
+
+## Requests
+
+Michael Salaverry (github.com/barakplasma)
+
+## Creator
+
+Michael Salaverry
+with help from https://github.com/IonicaBizau/hebrew-date (MIT license)
+
Icons made by Smashicons from [www.flaticon.com](https://www.flaticon.com/premium-icon/calendar_3130060?term=jewish&page=1&position=10&page=1&position=10&related_id=3130060&origin=tag)
\ No newline at end of file diff --git a/apps/hebrew_calendar/app-icon.js b/apps/hebrew_calendar/app-icon.js new file mode 100644 index 000000000..b6b0a53ae --- /dev/null +++ b/apps/hebrew_calendar/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("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/4ABIn4C/AXWQX/4C/X/6/DghH/AX4C5wC/DgBH/AX4C5Xwi//AX6//gESI/4C/AW8gX4sCI/4C/AW6+FAAMJJX4C/AWtAX48BJX4C/AWq+HAAMEJX4C/AWeAX5MAgRN/AX4CxXpQADn//AH4A/AFn8Xxy//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X/4A/X/6//AH6//X7n7tu/CaH27YnRE3d/EyO3X/4AG/3btu2CZ/t23bt4mS74mRtom1/4mSX+32JQXbCZwRCtu/E34mvX+xcCAQN/CRn7OIe3E34muX+39JSQRDTH4mO2wmSt6//X5JKNX4hxNE34mhX/6Y/E36/29qY/E3NtX/6/W/yY/EyffX/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X/6//X7hKMX4xxME34mgX+RZGARBiDLIwCIE34mkX+xcOAQISB/YROtu3EyV/E34mOX+39JSFv/4RPAQIm/E0S/29pKR/xxSE0vfTCNtX6QmUX/4CJTCYm/E0C/3LiACB+xxSE0vbTCW+E0u/X/4C/AXy//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//AX4C/X/4C/AX6//ARX2L6PfTHO+CiXtX/6/wOKX+E3C//ABaY/E34CHTSS//TH6//X+1vOKQmm7/9TCQmmX+xKRt5xSE34miX/6Y/E36/2/5KQv///ZxP24m/E0S//TH4m/X+/2JR4TCOJ+/E34mjX/4CG75xSE0yYC/yYS9omlX+xKPTAZxPE34mkX+3/JRwSD/pxNt4m/E0i/3/ZKM24TEOJt/E34mlX+3/JRm/CQn2OJgm/E0y/39pKLCQv+OJffE3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")) \ No newline at end of file diff --git a/apps/hebrew_calendar/app.js b/apps/hebrew_calendar/app.js new file mode 100644 index 000000000..399d124f3 --- /dev/null +++ b/apps/hebrew_calendar/app.js @@ -0,0 +1,26 @@ +g.clear(); + +let now = new Date(); + +let today = require('hebrewDate').hebrewDate(now); + +var mainmenu = { + "": { + "title": "Hebrew Date" + }, + greg: { + // @ts-ignore + value: require('locale').date(now, 1), + }, + date: { + value: today.date, + }, + month: { + value: today.month_name, + }, + year: { + value: today.year, + } +}; +// @ts-ignore +E.showMenu(mainmenu); \ No newline at end of file diff --git a/apps/hebrew_calendar/app.png b/apps/hebrew_calendar/app.png new file mode 100644 index 000000000..0dae731cd Binary files /dev/null and b/apps/hebrew_calendar/app.png differ diff --git a/apps/hebrew_calendar/hebrewDate.js b/apps/hebrew_calendar/hebrewDate.js new file mode 100644 index 000000000..da0c9cf50 --- /dev/null +++ b/apps/hebrew_calendar/hebrewDate.js @@ -0,0 +1,311 @@ +/*! + * This script was taked from this page http://www.shamash.org/help/javadate.shtml and ported to Node.js by Ionică Bizău in https://github.com/IonicaBizau/hebrew-date + * + * This script was adapted from C sources written by + * Scott E. Lee, which contain the following copyright notice: + * + * Copyright 1993-1995, Scott E. Lee, all rights reserved. + * Permission granted to use, copy, modify, distribute and sell so long as + * the above copyright and this permission statement are retained in all + * copies. THERE IS NO WARRANTY - USE AT YOUR OWN RISK. + * + * Bill Hastings + * RBI Software Systems + * bhastings@rbi.com + */ +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; +var GREG_SDN_OFFSET = 32045, DAYS_PER_5_MONTHS = 153, DAYS_PER_4_YEARS = 1461, DAYS_PER_400_YEARS = 146097; +var HALAKIM_PER_HOUR = 1080, HALAKIM_PER_DAY = 25920, HALAKIM_PER_LUNAR_CYCLE = 29 * HALAKIM_PER_DAY + 13753, HALAKIM_PER_METONIC_CYCLE = HALAKIM_PER_LUNAR_CYCLE * (12 * 19 + 7); +var HEB_SDN_OFFSET = 347997, NEW_MOON_OF_CREATION = 31524, NOON = 18 * HALAKIM_PER_HOUR, AM3_11_20 = 9 * HALAKIM_PER_HOUR + 204, AM9_32_43 = 15 * HALAKIM_PER_HOUR + 589; +var SUN = 0, MON = 1, TUES = 2, WED = 3, THUR = 4, FRI = 5, SAT = 6; +function weekdayarr(d0, d1, d2, d3, d4, d5, d6) { + this[0] = d0; + this[1] = d1; + this[2] = d2; + this[3] = d3; + this[4] = d4; + this[5] = d5; + this[6] = d6; +} +function gregmontharr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; +} +function hebrewmontharr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; + this[12] = m12; + this[13] = m13; +} +function monthsperyeararr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; + this[12] = m12; + this[13] = m13; + this[14] = m14; + this[15] = m15; + this[16] = m16; + this[17] = m17; + this[18] = m18; +} +var gWeekday = new weekdayarr("Sun", "Mon", "Tues", "Wednes", "Thurs", "Fri", "Satur"), gMonth = new gregmontharr("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"), hMonth = new hebrewmontharr("Tishri", "Heshvan", "Kislev", "Tevet", "Shevat", "AdarI", "AdarII", "Nisan", "Iyyar", "Sivan", "Tammuz", "Av", "Elul"), mpy = new monthsperyeararr(12, 12, 13, 12, 12, 13, 12, 13, 12, 12, 13, 12, 12, 13, 12, 12, 13, 12, 13); +/** + * hebrewDate + * Convert the Gregorian dates into Hebrew calendar dates. + * + * @name hebrewDate + * @function + * @param {Date|Number} inputDate The date object (representing the Gregorian date) or the year. + * @return {Object} An object containing: + * + * - `year`: The Hebrew year. + * - `month`: The Hebrew month. + * - `month_name`: The Hebrew month name. + * - `date`: The Hebrew date. + */ +function hebrewDate(inputDateOrYear) { + var inputMonth, inputDate; + var hebrewMonth = 0, hebrewDate = 0, hebrewYear = 0, metonicCycle = 0, metonicYear = 0, moladDay = 0, moladHalakim = 0; + function GregorianToSdn(inputYear, inputMonth, inputDay) { + var year = 0, month = 0, sdn = void 0; + // Make year a positive number + if (inputYear < 0) { + year = inputYear + 4801; + } + else { + year = inputYear + 4800; + } + // Adjust the start of the year + if (inputMonth > 2) { + month = inputMonth - 3; + } + else { + month = inputMonth + 9; + year--; + } + sdn = Math.floor(Math.floor(year / 100) * DAYS_PER_400_YEARS / 4); + sdn += Math.floor(year % 100 * DAYS_PER_4_YEARS / 4); + sdn += Math.floor((month * DAYS_PER_5_MONTHS + 2) / 5); + sdn += inputDay - GREG_SDN_OFFSET; + return sdn; + } + function SdnToHebrew(sdn) { + var tishri1 = 0, tishri1After = 0, yearLength = 0, inputDay = sdn - HEB_SDN_OFFSET; + FindTishriMolad(inputDay); + tishri1 = Tishri1(metonicYear, moladDay, moladHalakim); + if (inputDay >= tishri1) { + // It found Tishri 1 at the start of the year. + hebrewYear = metonicCycle * 19 + metonicYear + 1; + if (inputDay < tishri1 + 59) { + if (inputDay < tishri1 + 30) { + hebrewMonth = 1; + hebrewDate = inputDay - tishri1 + 1; + } + else { + hebrewMonth = 2; + hebrewDate = inputDay - tishri1 - 29; + } + return; + } + // We need the length of the year to figure this out,so find Tishri 1 of the next year. + moladHalakim += HALAKIM_PER_LUNAR_CYCLE * mpy[metonicYear]; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + tishri1After = Tishri1((metonicYear + 1) % 19, moladDay, moladHalakim); + } + else { + // It found Tishri 1 at the end of the year. + hebrewYear = metonicCycle * 19 + metonicYear; + if (inputDay >= tishri1 - 177) { + // It is one of the last 6 months of the year. + if (inputDay > tishri1 - 30) { + hebrewMonth = 13; + hebrewDate = inputDay - tishri1 + 30; + } + else if (inputDay > tishri1 - 60) { + hebrewMonth = 12; + hebrewDate = inputDay - tishri1 + 60; + } + else if (inputDay > tishri1 - 89) { + hebrewMonth = 11; + hebrewDate = inputDay - tishri1 + 89; + } + else if (inputDay > tishri1 - 119) { + hebrewMonth = 10; + hebrewDate = inputDay - tishri1 + 119; + } + else if (inputDay > tishri1 - 148) { + hebrewMonth = 9; + hebrewDate = inputDay - tishri1 + 148; + } + else { + hebrewMonth = 8; + hebrewDate = inputDay - tishri1 + 178; + } + return; + } + else { + if (mpy[(hebrewYear - 1) % 19] == 13) { + hebrewMonth = 7; + hebrewDate = inputDay - tishri1 + 207; + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 30; + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 30; + } + else { + hebrewMonth = 6; + hebrewDate = inputDay - tishri1 + 207; + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 30; + } + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 29; + if (hebrewDate > 0) + return; + // We need the length of the year to figure this out,so find Tishri 1 of this year. + tishri1After = tishri1; + FindTishriMolad(moladDay - 365); + tishri1 = Tishri1(metonicYear, moladDay, moladHalakim); + } + } + yearLength = tishri1After - tishri1; + moladDay = inputDay - tishri1 - 29; + if (yearLength == 355 || yearLength == 385) { + // Heshvan has 30 days + if (moladDay <= 30) { + hebrewMonth = 2; + hebrewDate = moladDay; + return; + } + moladDay -= 30; + } + else { + // Heshvan has 29 days + if (moladDay <= 29) { + hebrewMonth = 2; + hebrewDate = moladDay; + return; + } + moladDay -= 29; + } + // It has to be Kislev. + hebrewMonth = 3; + hebrewDate = moladDay; + } + function FindTishriMolad(inputDay) { + // Estimate the metonic cycle number. Note that this may be an under + // estimate because there are 6939.6896 days in a metonic cycle not + // 6940,but it will never be an over estimate. The loop below will + // correct for any error in this estimate. + metonicCycle = Math.floor((inputDay + 310) / 6940); + // Calculate the time of the starting molad for this metonic cycle. + MoladOfMetonicCycle(); + // If the above was an under estimate,increment the cycle number until + // the correct one is found. For modern dates this loop is about 98.6% + // likely to not execute,even once,because the above estimate is + // really quite close. + while (moladDay < inputDay - 6940 + 310) { + metonicCycle++; + moladHalakim += HALAKIM_PER_METONIC_CYCLE; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + } + // Find the molad of Tishri closest to this date. + for (metonicYear = 0; metonicYear < 18; metonicYear++) { + if (moladDay > inputDay - 74) + break; + moladHalakim += HALAKIM_PER_LUNAR_CYCLE * mpy[metonicYear]; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + } + } + function MoladOfMetonicCycle() { + var r1 = void 0, r2 = void 0, d1 = void 0, d2 = void 0; + // Start with the time of the first molad after creation. + r1 = NEW_MOON_OF_CREATION; + // Calculate gMetonicCycle * HALAKIM_PER_METONIC_CYCLE. The upper 32 + // bits of the result will be in r2 and the lower 16 bits will be in r1. + r1 += metonicCycle * (HALAKIM_PER_METONIC_CYCLE & 0xFFFF); + r2 = r1 >> 16; + r2 += metonicCycle * (HALAKIM_PER_METONIC_CYCLE >> 16 & 0xFFFF); + // Calculate r2r1 / HALAKIM_PER_DAY. The remainder will be in r1,the + // upper 16 bits of the quotient will be in d2 and the lower 16 bits + // will be in d1. + d2 = Math.floor(r2 / HALAKIM_PER_DAY); + r2 -= d2 * HALAKIM_PER_DAY; + r1 = r2 << 16 | r1 & 0xFFFF; + d1 = Math.floor(r1 / HALAKIM_PER_DAY); + r1 -= d1 * HALAKIM_PER_DAY; + moladDay = d2 << 16 | d1; + moladHalakim = r1; + } + function Tishri1(metonicYear, moladDay, moladHalakim) { + var tishri1 = moladDay, dow = tishri1 % 7, leapYear = metonicYear == 2 || metonicYear == 5 || metonicYear == 7 || metonicYear == 10 || metonicYear == 13 || metonicYear == 16 || metonicYear == 18, lastWasLeapYear = metonicYear == 3 || metonicYear == 6 || metonicYear == 8 || metonicYear == 11 || metonicYear == 14 || metonicYear == 17 || metonicYear == 0; + // Apply rules 2,3 and 4 + if (moladHalakim >= NOON || !leapYear && dow == TUES && moladHalakim >= AM3_11_20 || lastWasLeapYear && dow == MON && moladHalakim >= AM9_32_43) { + tishri1++; + dow++; + if (dow == 7) + dow = 0; + } + // Apply rule 1 after the others because it can cause an additional delay of one day. + if (dow == WED || dow == FRI || dow == SUN) { + tishri1++; + } + return tishri1; + } + var inputYear = inputDateOrYear; + if ((typeof inputYear === "undefined" ? "undefined" : _typeof(inputYear)) === "object") { + inputMonth = inputDateOrYear.getMonth() + 1; + inputDate = inputDateOrYear.getDate(); + inputYear = inputDateOrYear.getFullYear(); + } + SdnToHebrew(GregorianToSdn(inputYear, inputMonth, inputDate)); + return { + year: hebrewYear, + month: hebrewMonth, + date: hebrewDate, + month_name: hMonth[hebrewMonth - 1] + }; +} + +exports.hebrewDate = hebrewDate; diff --git a/apps/impwclock/ChangeLog b/apps/impwclock/ChangeLog index 0592d4d04..7bc119426 100644 --- a/apps/impwclock/ChangeLog +++ b/apps/impwclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Stopped watchface from flashing every interval 0.03: Move to Bangle.setUI to launcher support +0.04: Tweaks for compatibility with BangleJS2 diff --git a/apps/impwclock/README.md b/apps/impwclock/README.md index 30e42c95e..ac1341097 100644 --- a/apps/impwclock/README.md +++ b/apps/impwclock/README.md @@ -1,4 +1,4 @@ # Imprecise Word Clock -This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Press button 1 to see the time in accurate, digital form. But do you really need to know the exact time? +This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Touch the screen to see the time in accurate, digital form. But do you really need to know the exact time? diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js index 5492eac15..8bb5da6ba 100644 --- a/apps/impwclock/clock-impword.js +++ b/apps/impwclock/clock-impword.js @@ -2,7 +2,7 @@ A remix of word clock by Gordon Williams https://github.com/gfwilliams - Changes the representation of time to be more general -- Shows accurate digital time when button 1 is pressed +- Toggles showing of accurate digital time when screen touched. */ /* jshint esversion: 6 */ @@ -34,14 +34,16 @@ const timeOfDay = { }; +var big = g.getWidth()>200; // offsets and increments -const xs = 35; -const ys = 31; -const dy = 22; -const dx = 25; +const xs = big ? 35 : 20; +const ys = big ? 31 : 28; +const dx = big ? 25 : 20; +const dy = big ? 22 : 16; + // font size and color -const fontSize = 3; // "6x8" +const fontSize = big ? 3 : 2; // "6x8" const passivColor = 0x3186 /*grey*/ ; const activeColorNight = 0xF800 /*red*/ ; const activeColorDay = 0xFFFF /* white */; @@ -115,6 +117,8 @@ function drawWordClock() { // check whether we need to redraw the watchface if (hidx !== hidxPrev) { + // Turn off showDigitalTime + showDigitalTime = false; // draw allWords var c; var y = ys; @@ -138,15 +142,14 @@ function drawWordClock() { hidxPrev = hidx; } - // Display digital time while button 1 is pressed - g.clearRect(0, 215, 240, 240); + // Display digital time when button is pressed or screen touched + g.clearRect(0, big ? 215 : 160, big ? 240 : 176, big ? 240 : 176); if (showDigitalTime){ g.setColor(activeColor); - g.drawString(time, 120, 215); + g.drawString(time, big ? 120 : 90, big ? 215 : 160); } } - Bangle.on('lcdPower', function(on) { if (on) drawWordClock(); }); @@ -157,17 +160,14 @@ Bangle.drawWidgets(); setInterval(drawWordClock, 1E4); drawWordClock(); -// Show digital time while top button is pressed (if we have physical buttons) -if (global.BTN3) setWatch(function() { - showDigitalTime = BTN1.read(); - drawWordClock(); -}, BTN1, {repeat:true,edge:"both"}); -// If LCD pressed (on Bangle.js 2) draw digital time -Bangle.on('drag',e=>{ - var pressed = e.b!=0; - if (pressed!=showDigitalTime) { - showDigitalTime = pressed; +// If LCD pressed, toggle drawing digital time +Bangle.on('touch',e=>{ + if (showDigitalTime){ + showDigitalTime = false; + drawWordClock(); + } else { + showDigitalTime = true; drawWordClock(); } }); diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index 895f50e04..5e60068aa 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -1,3 +1,10 @@ 0.01: New App! 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.06: Fix (not) popupping up old messages +0.07: Added more details from music (instead of Undefined) + Added more app identifiers + diff --git a/apps/ios/README.md b/apps/ios/README.md new file mode 100644 index 000000000..b4c2c6ac9 --- /dev/null +++ b/apps/ios/README.md @@ -0,0 +1,31 @@ +# iOS integration app + +This is the iOS integration app for Bangle.js. This app allows you to receive +notifications from your iPhone. The Apple Notification Center Service (ANCS) +sends all the messages to your watch. + +You can allow this if you connect your Bangle to your iPhone. It will be +prompted for immediatly after you connect the Bangle to the iPhone. + +### Connecting your Bangle(2).js to your iPhone +The Bangle watches are Bluetooth Low Energy (BLE) devices. Sometimes they +will not be seen/detected by the Bluetooth scanner in your iPhone settings +menu. + +To resolve this, you can download numerous apps who can actually scan +for BLE devices. There are great ones out there, free and paid. + +We really like WebBLE, which we also recommend to load apps on your +watch with your iOS device, as Safari does not support WebBluetooth +for now. It's just a few bucks/pounds/euro's. + +If you like to try a free app first, you can always use NRF Toolbox or +Bluetooth BLE Device Finder to find and connect your Bangle. + +## Requests + +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=ios%20app + +## Creator + +Gordon Williams diff --git a/apps/ios/boot.js b/apps/ios/boot.js index c3a30170d..8ccfb617d 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -26,6 +26,13 @@ E.on('ANCS',msg=>{ function ancsHandler() { var msg = Bangle.ancsMessageQueue[0]; NRF.ancsGetNotificationInfo( msg.uid ).then( info => { + + if(msg.preExisting === true){ + info.new = false; + } else { + info.new = true; + } + E.emit("notify", Object.assign(msg, info)); Bangle.ancsMessageQueue.shift(); if (Bangle.ancsMessageQueue.length) @@ -49,27 +56,66 @@ E.on('notify',msg=>{ "message" : string, "messageSize" : string, "date" : string, + "new" : boolean, "posAction" : string, "negAction" : string, "name" : string, */ var appNames = { - "com.netflix.Netflix" : "Netflix", - "com.google.ios.youtube" : "YouTube", + "com.apple.facetime": "FaceTime", + "com.apple.mobilecal": "Calendar", + "com.apple.mobilemail": "Mail", + "com.apple.mobilephone": "Phone", + "com.apple.MobileSMS": "SMS Message", + "com.apple.Passbook": "iOS Wallet", + "com.apple.podcasts": "Podcasts", + "com.apple.reminders": "Reminders", + "com.apple.shortcuts": "Shortcuts", + "com.atebits.Tweetie2": "Twitter", + "com.burbn.instagram" : "Instagram", + "com.facebook.Facebook": "Facebook", + "com.facebook.Messenger": "Messenger", + "com.google.Chromecast" : "Google Home", + "com.google.Gmail" : "GMail", "com.google.hangouts" : "Hangouts", + "com.google.ios.youtube" : "YouTube", + "com.hammerandchisel.discord" : "Discord", + "com.ifttt.ifttt" : "IFTTT", + "com.jumbo.app" : "Jumbo", + "com.linkedin.LinkedIn" : "LinkedIn", + "com.microsoft.Office.Outlook" : "Outlook Mail", + "com.nestlabs.jasper.release" : "Nest", + "com.netflix.Netflix" : "Netflix", + "com.reddit.Reddit" : "Reddit", + "com.skype.skype": "Skype", "com.skype.SkypeForiPad": "Skype", - "com.atebits.Tweetie2": "Twitter" + "com.spotify.client": "Spotify", + "com.strava.stravaride": "Strava", + "com.tinyspeck.chatlyio": "Slack", + "com.toyopagroup.picaboo": "Snapchat", + "com.ubercab.UberClient": "Uber", + "com.ubercab.UberEats": "UberEats", + "com.vilcsak.bitcoin2": "Coinbase", + "com.wordfeud.free": "WordFeud", + "com.zhiliaoapp.musically": "TikTok", + "net.whatsapp.WhatsApp": "WhatsApp", + "nl.ah.Appie": "Albert Heijn", + "nl.postnl.TrackNTrace": "PostNL", + "ph.telegra.Telegraph": "Telegram", + "tv.twitch": "Twitch", + // could also use NRF.ancsGetAppInfo(msg.appId) here }; var unicodeRemap = { '2019':"'" }; var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16)); - if (appNames[msg.appId]) msg.a + //if (appNames[msg.appId]) msg.a require("messages").pushMessage({ t : msg.event, id : msg.uid, src : appNames[msg.appId] || msg.appId, + 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) @@ -82,9 +128,10 @@ E.on('AMS',a=>{ function push(m) { var msg = { t : "modify", id : "music", title:"Music" }; if (a.id=="artist") msg.artist = m; - else if (a.id=="album") msg.artist = m; - else if (a.id=="title") msg.tracl = m; - else return; // duration? need to reformat + else if (a.id=="album") msg.album = m; + else if (a.id=="title") msg.track = m; + else if (a.id=="duration") msg.dur = m; + else return; require("messages").pushMessage(msg); } if (a.truncated) NRF.amsGetMusicInfo(a.id).then(push) diff --git a/apps/lapcounter/ChangeLog b/apps/lapcounter/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/lapcounter/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/lapcounter/README.md b/apps/lapcounter/README.md new file mode 100644 index 000000000..8866955e4 --- /dev/null +++ b/apps/lapcounter/README.md @@ -0,0 +1,19 @@ +# Lap Counter + +Click button to count laps (e.g. in a swimming pool). +Also shows total duration snapshot (like a stopwatch, but laid back). + +![Screenshot](screenshot.png) + +## Usage + +* Click BTN1 to start counting. Counter becomes `0`, duration becomes `00:00.0` +* Each time you click BTN1, counter is incremented, and you see duration between first and last clicks. + +## Features + +Disables LCD timeout (so that you can be _sure_ what BTN1 would do). + +## Creator + +[Nimrod Kerrett](https://zzzen.com) diff --git a/apps/lapcounter/app-icon.js b/apps/lapcounter/app-icon.js new file mode 100644 index 000000000..a443b3a41 --- /dev/null +++ b/apps/lapcounter/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap")) diff --git a/apps/lapcounter/app.js b/apps/lapcounter/app.js new file mode 100644 index 000000000..215f6140a --- /dev/null +++ b/apps/lapcounter/app.js @@ -0,0 +1,53 @@ +const w = g.getWidth(); +const h = g.getHeight(); +const wid_h = 24; +let tStart; +let tNow; +let counter=-1; + +const icon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap")); + +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); + //log_debug(text); + return text; +} + +function doCounter() { + if (counter<0) { + tStart = Date.now(); + tNow = tStart; + } else { + tNow = Date.now(); + } + counter++; + let dT = tNow-tStart; + + g.clearRect(0,wid_h,w,h-wid_h); + g.setFontAlign(0,0); + g.setFont("Vector",72); + g.drawString(counter,w/2,h/2); + g.setFont("Vector",24); + g.drawString(timeToText(dT),w/2,h/2+50); +} + +setWatch(doCounter, BTN1, true); + +g.clear(true); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setLCDTimeout(0); +g.drawImage(icon,w/2-24,h/2-24); +g.setFontAlign(0,0); +require("Font8x12").add(Graphics); +g.setFont("8x12"); +g.drawString("Click button to count.", w/2, h/2+22); diff --git a/apps/lapcounter/app.png b/apps/lapcounter/app.png new file mode 100644 index 000000000..7d6ca8317 Binary files /dev/null and b/apps/lapcounter/app.png differ diff --git a/apps/lapcounter/screenshot.png b/apps/lapcounter/screenshot.png new file mode 100644 index 000000000..f3113d86e Binary files /dev/null and b/apps/lapcounter/screenshot.png differ diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index bd8a9bd03..0b2f134ad 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -6,3 +6,6 @@ 0.06: Use Bangle.setUI for buttons 0.07: Theme colours fix 0.08: Merge Bangle.js 1 and 2 launchers +0.09: Bangle.js 2 - pressing the button goes back to clock (fix #971) + After 10s of being locked, the launcher goes back to the clock screen +0.10: added in selectable font in settings including scalable vector font diff --git a/apps/launch/app-bangle1.js b/apps/launch/app-bangle1.js index 3d4682e55..f779f5de4 100644 --- a/apps/launch/app-bangle1.js +++ b/apps/launch/app-bangle1.js @@ -64,3 +64,12 @@ Bangle.setUI("updown",dir=>{ }); Bangle.loadWidgets(); Bangle.drawWidgets(); +// 10s of inactivity goes back to clock +if (Bangle.setLocked) Bangle.setLocked(false); // unlock initially +var lockTimeout; +Bangle.on('lock', locked => { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) + lockTimeout = setTimeout(_=>load(), 10000); +}); diff --git a/apps/launch/app-bangle2.js b/apps/launch/app-bangle2.js index 8b66247c5..156eecdf4 100644 --- a/apps/launch/app-bangle2.js +++ b/apps/launch/app-bangle2.js @@ -1,4 +1,22 @@ var s = require("Storage"); +let fonts = g.getFonts(); +var scaleval = 1; +var vectorval = 20; +var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; +let settings = require('Storage').readJSON("launch.json", true) || {}; +if ("vectorsize" in settings) { + vectorval = parseInt(settings.vectorsize); +} +if ("font" in settings){ + if(settings.font == "Vector"){ + scaleval = vectorval/20; + font = "Vector"+(vectorval).toString(); + } + else{ + font = settings.font; + scaleval = (font.split('x')[1])/20; + } +} var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); @@ -11,8 +29,6 @@ apps.forEach(app=>{ if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area }); -// FIXME: not needed after 2v11 -var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; // FIXME: check not needed after 2v11 if (g.wrapString) { g.setFont(font); @@ -22,9 +38,9 @@ if (g.wrapString) { function drawApp(i, r) { var app = apps[i]; if (!app) return; - g.clearRect(r.x,r.y,r.x+r.w-1, r.y+r.h-1); - g.setFont(font).setFontAlign(-1,0).drawString(app.name,64,r.y+32); - if (app.icon) try {g.drawImage(app.icon,8,r.y+8);} catch(e){} + g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); + g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval)); + if (app.icon) try {g.drawImage(app.icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} } g.clear(); @@ -32,7 +48,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); E.showScroller({ - h : 64, c : apps.length, + h : 64*scaleval, c : apps.length, draw : drawApp, select : i => { var app = apps[i]; @@ -46,3 +62,16 @@ E.showScroller({ } } }); + +// pressing button goes back +setWatch(_=>load(), BTN1, {edge:"falling"}); + +// 10s of inactivity goes back to clock +Bangle.setLocked(false); // unlock initially +var lockTimeout; +Bangle.on('lock', locked => { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) + lockTimeout = setTimeout(_=>load(), 10000); +}); diff --git a/apps/launch/settings.js b/apps/launch/settings.js new file mode 100644 index 000000000..8be1adb36 --- /dev/null +++ b/apps/launch/settings.js @@ -0,0 +1,25 @@ +// make sure to enclose the function in parentheses +(function(back) { + let settings = require('Storage').readJSON('launch.json',1)||{}; + let fonts = g.getFonts(); + function save(key, value) { + settings[key] = value; + require('Storage').write('launch.json',settings); + } + const appMenu = { + '': {'title': 'Launcher Settings'}, + '< Back': back, + 'Font': { + value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"), + min:0, max:fonts.length-1, step:1,wrap:true, + onchange: (m) => {save('font', fonts[m])}, + format: v => fonts[v] + }, + 'Vector font size': { + value: settings.vectorsize || 10, + min:10, max: 20,step:1,wrap:true, + onchange: (m) => {save('vectorsize', m)} + } + }; + E.showMenu(appMenu); +}); diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 288dc6dde..448f8119a 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -10,3 +10,7 @@ 0.08: Added Mavigation units and en_NAV 0.09: Added New Zealand en_NZ 0.10: Apply 12hour setting to time +0.11: Added translations for nl_NL and changes one formatting +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 diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 9e2624b77..2e3fa8713 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -37,11 +37,30 @@ const codePages = { /* When it's not in the codepage, try and use these conversions */ const charFallbacks = { + "ą":"a", + "ā":"a", "č":"c", - "ř":"r", + "ć":"c", + "ě":"e", + "ę":"e", + "ē":"e", + "ģ":"g", + "i":"ī", + "ķ":"k", + "ļ":"l", + "ł":"l", + "ń":"n", + "ņ":"n", "ő":"o", - "ě":"e" -}; + "ó":"o", + "ř":"r", + "ś":"s", + "š":"s", + "ū":"u", + "ż":"z", + "ź":"z", + "ž":"z", + }; /* timePattern / datePattern: @@ -130,7 +149,7 @@ var locales = { temperature: "°C", ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, - datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%Y" }, // Sonntag, 1. März 2020 // 01.01.20 + datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mär 2020 // 01.03.20 abmonth: "Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", @@ -184,12 +203,30 @@ var locales = { temperature: "°C", ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, - datePattern: { 0: "%A %B %d %Y", 1: "%d.%m.%y" }, // zondag 1 maart 2020 // 01.01.20 + datePattern: { 0: "%d %b %Y", 1: "%d-%m-%Y" }, // 28 feb 2020 // 28-02-2020 abday: "zo,ma,di,wo,do,vr,za", day: "zondag,maandag,dinsdag,woensdag,donderdag,vrijdag,zaterdag", abmonth: "jan,feb,mrt,apr,mei,jun,jul,aug,sep,okt,nov,dec", month: "januari,februari,maart,april,mei,juni,juli,augustus,september,oktober,november,december", - // No translation for english... + trans: { yes: "ja", Yes: "Ja", no: "nee", No: "Nee", ok: "ok", on: "aan", off: "uit", + "< Back": "< Terug", "Delete": "Verwijderen", "Mark Unread": "Markeer als ongelezen" } + }, + "en_NL": { // English date units with Dutch number, currency and navigation units. + lang: "en_NL", + decimal_point: ",", + thousands_sep: ".", + currency_symbol: "€", + int_curr_symbol: "EUR", + speed: "km/h", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "am", 1: "pm" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%b %d %Y", 1: "%d/%m/%Y" }, // Feb 28 2020" // "01/03/2020"(short) + 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", }, "en_CA": { lang: "en_CA", @@ -586,6 +623,42 @@ var locales = { day: "Domingo,Segunda-feira,Terça-feira,Quarta-feira,Quinta-feira,Sexta-feira,Sábado", trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "ok", on: "on", off: "off" } }, + "pl_PL": { + lang: "pl_PL", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "zł", + int_curr_symbol: "PLN", + speed: "kmh", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, + datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2021 // 01.03.2021 + abmonth: "Sty,Lut,Mar,Kwi,Maj,Cze,Lip,Sie,Wrz,Paź,Lis,Gru", + month: "Styczeń,Luty,Marzec,Kwiecień,Maj,Czerwiec,Lipiec,Sierpień,Wrzesień,Październik,Listopad,Grudzień", + abday: "Ndz,Pon,Wt,Śr,Czw,Pt,Sob", + day: "Niedziela,Poniedziałek,Wtorek,Środa,Czwartek,Piątek,Sobota", + trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "on", off: "off", "< Back": "< Wstecz" } + }, + "lv_LV": { // Using charfallbacks + lang: "lv_LV", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "€", + int_curr_symbol: "EUR", + speed: "kmh", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, + datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2020 // 01.03.20 + abmonth: "Jan,Feb,Mar,Apr,Mai,Jūn,Jūl,Aug,Sep,Okt,Nov,Dec", + month: "Janvāris,Februāris,Marts,Aprīlis,Maijs,Jūnijs,Jūlijs,Augusts,Septemberis,Oktobris,Novembris,Decembris", + abday: "Pr,Ot,Tr,Ce,Pk,Se,Sv", + day: "Pirmdiena,Otrdiena,Trešdiena,Ceturtdiena,Piektdiena,Sestdiena,Svētdiena", + trans: { yes: "jā", Yes: "Jā", no: "nē", No: "Nē", ok: "labi", on: "Ieslēgt", off: "Izslēgt", "< Back": "< Atpakaļ" } + }, /*, "he_IL": { // This won't work until we get a font - see https://github.com/espruino/BangleApps/issues/399 codePage : "ISO8859-8", diff --git a/apps/mandlebrotclock/ChangeLog b/apps/mandelbrotclock/ChangeLog similarity index 100% rename from apps/mandlebrotclock/ChangeLog rename to apps/mandelbrotclock/ChangeLog diff --git a/apps/mandlebrotclock/README.md b/apps/mandelbrotclock/README.md similarity index 55% rename from apps/mandlebrotclock/README.md rename to apps/mandelbrotclock/README.md index 8628a61d0..387343a9e 100644 --- a/apps/mandlebrotclock/README.md +++ b/apps/mandelbrotclock/README.md @@ -1,6 +1,6 @@ -# Mandlebrot Clock +# Mandelbrot Clock -A simple clock themed on the mandlebrot set. +A simple clock themed on the mandelbrot set. Written by [James Milner](https://www.github.com/jameslmilner) diff --git a/apps/mandlebrotclock/app.png b/apps/mandelbrotclock/app.png similarity index 100% rename from apps/mandlebrotclock/app.png rename to apps/mandelbrotclock/app.png diff --git a/apps/mandlebrotclock/mandlebrotclock-icon.js b/apps/mandelbrotclock/mandelbrotclock-icon.js similarity index 100% rename from apps/mandlebrotclock/mandlebrotclock-icon.js rename to apps/mandelbrotclock/mandelbrotclock-icon.js diff --git a/apps/mandlebrotclock/mandlebrotclock.js b/apps/mandelbrotclock/mandelbrotclock.js similarity index 99% rename from apps/mandlebrotclock/mandlebrotclock.js rename to apps/mandelbrotclock/mandelbrotclock.js index 16cc8dfb8..94636056e 100644 --- a/apps/mandlebrotclock/mandlebrotclock.js +++ b/apps/mandelbrotclock/mandelbrotclock.js @@ -1,6 +1,6 @@ // MIT License - James Milner 2021 -const mandlebrotBmp = { +const mandelbrotBmp = { width: 176, height: 176, bpp: 8, @@ -13,7 +13,7 @@ const mandlebrotBmp = { }; function draw() { - g.drawImage(mandlebrotBmp); + g.drawImage(mandelbrotBmp); // work out how to display the current time const d = new Date(); const h = d.getHours(), diff --git a/apps/mandlebrotclock/mandlebrotclock.png b/apps/mandelbrotclock/mandelbrotclock.png similarity index 100% rename from apps/mandlebrotclock/mandlebrotclock.png rename to apps/mandelbrotclock/mandelbrotclock.png diff --git a/apps/mandlebrotclock/screenshot_mandlebrotclock.png b/apps/mandelbrotclock/screenshot_mandelbrotclock.png similarity index 100% rename from apps/mandlebrotclock/screenshot_mandlebrotclock.png rename to apps/mandelbrotclock/screenshot_mandelbrotclock.png diff --git a/apps/menuwheel/ChangeLog b/apps/menuwheel/ChangeLog new file mode 100644 index 000000000..defdb5049 --- /dev/null +++ b/apps/menuwheel/ChangeLog @@ -0,0 +1 @@ +0.01: New menu! diff --git a/apps/menuwheel/README.md b/apps/menuwheel/README.md new file mode 100644 index 000000000..22cb49466 --- /dev/null +++ b/apps/menuwheel/README.md @@ -0,0 +1,25 @@ +# Wheel Menu + +Replace Bangle.js 2's menus with a version that contains variable-size text and a back button. + +Bangle.js 1: +![Dark Mode Screenshot](screenshot_b1_dark.png) +![Light Mode Screenshot](screenshot_b1_light.png) + +Bangle.js 2: +![Dark Mode Screenshot](screenshot_b2_dark.png) +![Editing Screenshot](screenshot_b2_edit.png) +![Light Mode Screenshot](screenshot_b2_light.png) + + +## Features + +If the menu contains "Back" or "Exit", it is shown as a button instead. +The menu wraps around, with a divider between the last and first items. + +## Controls + +Bangle.js 1: Use BTN1/BTN3 to scroll through items, BTN2 to open/edit the selected item. +Bangle.js 2: Swipe up/down to scroll through items, tap/BTN to open/edit the selected item. + +Press the back button (if present) to go back. \ No newline at end of file diff --git a/apps/menuwheel/boot.js b/apps/menuwheel/boot.js new file mode 100644 index 000000000..3e708e9a8 --- /dev/null +++ b/apps/menuwheel/boot.js @@ -0,0 +1,213 @@ +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; + } + + var B2 = process.env.HWVERSION===2, + loc = require("locale"), + menuItems = Object.keys(items), + options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + + // show "< Back" item (or similar) as button instead (i.e. remove from the menu) + var back,backLbl; + for (var b of ['Back', 'Exit', 'Cancel']) { + if (!items[b] && items['< '+b]) b = '< '+b; + back = items[b]; + if (typeof back === "function") { + backLbl = loc.translate(b); + menuItems.splice(menuItems.indexOf(b),1); + break; + } + else back = undefined; + } + // font sizes + var small = B2?15:22, + large = B2?30:45; + if (options.selected === undefined) options.selected = 0; + var ar = Bangle.appRect, + x = ar.x, + x2 = ar.x2, + w = ar.w, + y = ar.y, + y2 = ar.y2; + if (options.title) y += 22; + var wrap = menuItems.length>3; // don't wrap if all items are always in view anyway + + var vc=Math.round((y+y2)/2), // vertical center + hc = Math.round((x+x2)/2), // horizontal center + ih = large+small*2; // active item height + + var getItem = idx => { + // we wrap out-of-range indexes + while (idx<0) idx+=menuItems.length; + idx = idx%menuItems.length; + var name = menuItems[idx]; + var item = items[name]; + var v; + if ("object"== typeof item) { + v = item.value; + if (item.format) v = item.format(v); + v = loc.translate(""+v); + } + return {lbl: loc.translate(name), v: v}; + }; + var l = { + lastIdx : null, // we want a complete redraw on first run + draw : function() { + var idx = options.selected, + edit = l.selectEdit; + g.reset(); + + // don't highlight whole item when editing + g.setColor(edit?g.theme.fg:g.theme.fgH) + .setBgColor(edit?g.theme.bg:g.theme.bgH) + .setFont('Vector', large); + var item = getItem(idx), + lw = g.stringWidth(item.lbl)+2; + if (lw+2 >= w) { // label width doesn't fit at large size: scale it down + g.setFont('Vector', Math.floor(large*ar.w/lw)); + } + g.clearRect(x,vc-ih/2,x2,vc+ih/2) + .setFontAlign(0,0,0).drawString(item.lbl,hc,vc); + + if (item.v !== undefined) { + g.setColor(g.theme.fgH).setBgColor(g.theme.bgH) // always highlighted: either as part of item, or while editing + .setFontAlign(0,1,0) + .setFont('Vector', small) + .clearRect(x,vc+ih/2-small-2,x2,vc+ih/2) + .drawString(item.v,hc,vc+ih/2-1); + if (edit) { + g.drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",x2-23,vc+ih/2-small+(B2?1:5),{scale:2}); + } + } + if (l.lastIdx !== idx) { + // we scrolled: redraw all + l.lastIdx=idx; + g.reset(); + + if (options.title) { + if (B2) g.setFont('12x20'); + else g.setFont('6x8',2); + g.drawLine(x, y-2, x2, y-2) + .setFontAlign(0,1,0) + .drawString(options.title, (x+x2)/2, y-2); + } + + // clear prev/next items area + g.clearRect(x,y,x2,vc-ih/2-1) + .clearRect(x,vc+ih/2+1,x2,y2); + + // get display label by index + var lbl = idx => { + var item = getItem(idx); + if (item.v !== undefined) item.lbl+=': '+item.v; + return item.lbl; + } + // previous two items + g.setFontAlign(0, 1) + if (wrap||idx>0) g.setFont('Vector', small).drawString(lbl(idx-1), hc, vc-ih/2-5); + if (wrap||idx>1) g.setFont('Vector', small/2).drawString(lbl(idx-2), hc, vc-ih/2-small-10); + // next two items + g.setFontAlign(0, -1); + if (wrap||idx g.drawLine(x, y, x2, y); + if (idx===0) div(vc-ih/2-1); + if (idx===1) div(vc-ih/2-small-8); + // if (s === 2) div(vc-ih/2-small*1.5-13); + if (idx===menuItems.length-1) div(vc+ih/2+1); + if (idx===menuItems.length-2) div(vc+ih/2+small+6); + // if (s === 2) div(vc+ih/2+small*1.5+13); + } + + if (back) { + g.setBgColor(g.theme.bg2) + .setFont('Vector', small); + var bw=g.stringWidth(backLbl); + g.clearRect(x,y, x+bw+2, y+small+2); + var bx1=x, by1=y, bx2=x+bw+2, by2=y+small+2; + // g.drawRect(x,y, x+bw+2, y+small+2); + var poly = [ // button outline + bx1+2,by1, + bx2-2,by1, + bx2, by1+2, + bx2, by2-2, + bx2-2,by2, + bx1+2,by2, + bx1, by2-2, + bx1, by1+2, + ] + g.setColor(g.theme.bg2).fillPoly(poly, true) + .setColor(g.theme.fg2).drawPoly(poly, true) + .setFontAlign(-1,-1,0).drawString(backLbl, x+2,y+2); + } + } + g.flip(); + }, + select : function() { // same as default menu + var item = items[menuItems[options.selected]]; + if ("function" == typeof item) {l.lastIdx=null; item(l);} // force a redraw after callback + else if ("object" == typeof item) { + // if a number, go into 'edit mode' + if ("number" == typeof item.value) + l.selectEdit = l.selectEdit?undefined:item; + else { // else just toggle bools + if ("boolean" == typeof item.value) item.value=!item.value; + if (item.onchange) {l.lastIdx=null; item.onchange(item.value);} // force a redraw after callback + } + l.draw(); + } + }, + move : function(dir) { + if (l.selectEdit) { // same as default menu + var item = l.selectEdit; + item.value -= (dir||1)*(item.step||1); + if (item.min!==undefined && item.valueitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) {l.lastIdx=null; item.onchange(item.value);} // force a redraw after callback + } else { + if (B2) dir=-dir; // swipe vs button scrolling + if (!wrap && (options.selected+dir<0 || options.selected+dir>=menuItems.length)) { + return; + } + options.selected = (options.selected+dir+menuItems.length)%menuItems.length; + } + l.draw(); + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + if (back) { + // we have a back button: check touches before passing them to setUI's touchHandler + if (B2) { + Bangle.removeListener('touch', Bangle.touchHandler); + Bangle.backHandler = (b, xy) => { + // anywhere top-left (but above the active item) = back button + if (xy.x { + // left side = back button + if (b===1) back(); + } + } + // note: backHandler is cleaned up at the top of this file + Bangle.on('touch', Bangle.backHandler); + } + return l; +}; diff --git a/apps/menuwheel/icon.png b/apps/menuwheel/icon.png new file mode 100644 index 000000000..61f94a035 Binary files /dev/null and b/apps/menuwheel/icon.png differ diff --git a/apps/menuwheel/screenshot_b1_dark.png b/apps/menuwheel/screenshot_b1_dark.png new file mode 100644 index 000000000..c6dfb802b Binary files /dev/null and b/apps/menuwheel/screenshot_b1_dark.png differ diff --git a/apps/menuwheel/screenshot_b1_edit.png b/apps/menuwheel/screenshot_b1_edit.png new file mode 100644 index 000000000..a39b0a832 Binary files /dev/null and b/apps/menuwheel/screenshot_b1_edit.png differ diff --git a/apps/menuwheel/screenshot_b1_light.png b/apps/menuwheel/screenshot_b1_light.png new file mode 100644 index 000000000..35ac01fe9 Binary files /dev/null and b/apps/menuwheel/screenshot_b1_light.png differ diff --git a/apps/menuwheel/screenshot_b2_dark.png b/apps/menuwheel/screenshot_b2_dark.png new file mode 100644 index 000000000..1393838a3 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_dark.png differ diff --git a/apps/menuwheel/screenshot_b2_edit.png b/apps/menuwheel/screenshot_b2_edit.png new file mode 100644 index 000000000..bca98a9a5 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_edit.png differ diff --git a/apps/menuwheel/screenshot_b2_light.png b/apps/menuwheel/screenshot_b2_light.png new file mode 100644 index 000000000..4ffe08fe3 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_light.png differ diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 87094a091..16d0010cc 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -8,3 +8,15 @@ Back now marks a message as read Clicking top-left opens a menu which allows you to delete a message or mark unread 0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909) +0.08: Fix rendering of long messages (fix #969) + buzz on new message (fix #999) +0.09: Message now disappears after 60s if no action taken and clock loads (fix 922) + Fix phone icon (#1014) +0.10: Respect the 'new' attribute if it was set from iOS integrations +0.11: Open app when touching the widget (Bangle.js 2 only) +0.12: Extra app-specific notification icons + New animated notifcationicon (instead of large blinking 'MESSAGES') + Added screenshots +0.13: Add /*LANG*/ comments for internationalisation + Add 'Delete All' option to message options + Now update correctly when 'require("messages").clearAll()' is called diff --git a/apps/messages/README.md b/apps/messages/README.md index c243ec06a..4952b1877 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -1,16 +1,32 @@ # Messages app -**THIS APP IS CURRENTLY BETA** - This app handles the display of messages and message notifications. It stores a list of currently received messages and allows them to be listed, viewed, and responded to. It is a replacement for the old `notify`/`gadgetbridge` apps. -## Usage +## Settings + +You can change settings by going to the global `Settings` app, then `App Settings` +and `Messages`: + +* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received +* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds +* `Unread Timer` - when a new message is received we go into the Messages app. +If there is no user input for this amount of time then the app will exit and return +to the clock where a ringing bell will be shown in the Widget bar. + +## Images +_1. Screenshot of a notification_ + +![](screenshot.png) + +_2. What the notify icon looks like (it's touchable on Bangle.js2!)_ + +![](screenshot-notify.gif) + -... ## Requests @@ -19,3 +35,11 @@ Please file any issues on https://github.com/espruino/BangleApps/issues/new?titl ## Creator Gordon Williams + +## Contributors + +[Jeroen Peters](https://github.com/jeroenpeters1986) + +## Attributions + +Icons used in this app are from https://icons8.com diff --git a/apps/messages/app.js b/apps/messages/app.js index cb2b5c2cd..79009e77e 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -21,6 +21,7 @@ */ var Layout = require("Layout"); +var fontSmall = "6x8"; var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; @@ -41,16 +42,21 @@ try { }; } - +/** this is a timeout if the app has started and is showing a single message +but the user hasn't seen it (eg no user input) - in which case +we should start a timeout for settings.unreadTimeout to return +to the clock. */ +var unreadTimeout; +/// List of all our messages var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; if (!Array.isArray(MESSAGES)) MESSAGES=[]; var onMessagesModified = function(msg) { // TODO: if new, show this new one - if (msg.new) { + if (msg && msg.new) { if (WIDGETS["messages"]) WIDGETS["messages"].buzz(); else Bangle.buzz(); } - showMessage(msg.id); + showMessage(msg&&msg.id); }; function saveMessages() { require("Storage").writeJSON("messages.json",MESSAGES) @@ -59,6 +65,12 @@ function saveMessages() { function getBackImage() { return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); } +function getNotificationImage() { + return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); +} +function getFBIcon() { + return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); +} function getPosImage() { return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); } @@ -68,18 +80,28 @@ function getNegImage() { function getMessageImage(msg) { if (msg.img) return atob(msg.img); var s = (msg.src||"").toLowerCase(); - if (s=="Phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); - if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); + 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=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); - if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); + 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=="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(); + if (s=="messenger") return getFBIcon(); + if (s=="outlook mail") return getNotificationImage(); + if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); + if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); + if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); + if (s=="sms message") return getNotificationImage(); if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); + if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); + if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); + if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); if (msg.id=="back") return getBackImage(); - return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); + return getNotificationImage(); } - function showMapMessage(msg) { var m; var distance, street, target, eta; @@ -121,7 +143,7 @@ function showMapMessage(msg) { function showMusicMessage(msg) { function fmtTime(s) { var m = Math.floor(s/60); - s = (s%60).toString().padStart(2,0); + s = (parseInt(s%60)).toString().padStart(2,0); return m+":"+s; } @@ -135,7 +157,7 @@ function showMusicMessage(msg) { {type:"h", fillx:1, bgCol:colBg, c: [ { type:"btn", src:getBackImage, cb:back }, { type:"v", fillx:1, c: [ - { type:"txt", font:fontLarge, label:msg.artist, pad:2 }, + { type:"txt", font:fontMedium, label:msg.artist, pad:2 }, { type:"txt", font:fontMedium, label:msg.album, pad:2 } ]} ]}, @@ -152,66 +174,89 @@ function showMusicMessage(msg) { } function showMessageSettings(msg) { - E.showMenu({"":{"title":"Message"}, + E.showMenu({"":{"title":/*LANG*/"Message"}, "< Back" : () => showMessage(msg.id), - "Delete" : () => { + /*LANG*/"Delete" : () => { MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); saveMessages(); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); }, - "Mark Unread" : () => { + /*LANG*/"Mark Unread" : () => { msg.new = true; saveMessages(); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); }, + /*LANG*/"Delete all messages" : () => { + E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { + if (isYes) { + MESSAGES = []; + saveMessages(); + } + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + }); + }, }); } function showMessage(msgid) { var msg = MESSAGES.find(m=>m.id==msgid); - if (!msg) return checkMessages(); // go home if no message found - if (msg.src=="Maps") return showMapMessage(msg); - if (msg.id=="music") return showMusicMessage(msg); + if (!msg) return checkMessages({clockIfNoMsg:0,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); + } + if (msg.id=="music") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMusicMessage(msg); + } // Normal text message display - var title=msg.title, titleFont = fontLarge; + var title=msg.title, titleFont = fontLarge, lines; if (title) { - var w = g.getWidth()-40; + var w = g.getWidth()-48; if (g.setFont(titleFont).stringWidth(title) > w) titleFont = fontMedium; - if (g.setFont(titleFont).stringWidth(title) > w) - title = g.wrapString(title, w).join("\n"); + if (g.setFont(titleFont).stringWidth(title) > w) { + lines = g.wrapString(title, w); + title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n"); + } } var buttons = [ {type:"btn", src:getBackImage(), cb:()=>{ - msg.new = false; // read mail - saveMessages(); + msg.new = false; saveMessages(); // read mail + cancelReloadTimeout(); // don't auto-reload to clock now checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:1}); }} // back ]; if (msg.positive) { buttons.push({type:"btn", src:getPosImage(), cb:()=>{ msg.new = false; saveMessages(); + cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); }}); } if (msg.negative) { buttons.push({type:"btn", src:getNegImage(), cb:()=>{ - console.log("Response"); msg.new = false; saveMessages(); + cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,false); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); }}); } + lines = g.wrapString(msg.body, g.getWidth()-10); + 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), cb:()=>showMessageSettings(msg) }, + { type:"btn", src:getMessageImage(msg), pad: 3, cb:()=>{ + cancelReloadTimeout(); // don't auto-reload to clock now + showMessageSettings(msg); + }}, { type:"v", fillx:1, c: [ - {type:"txt", font:fontMedium, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2 }, + {type:"txt", font:fontSmall, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2, halign:1 }, title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{}, ]}, ]}, - {type:"txt", font:fontMedium, label:msg.body||"", wrap:true, fillx:1, filly:1, pad:2 }, + {type:"txt", font:fontMedium, label:body, fillx:1, filly:1, pad:2 }, {type:"h",fillx:1, c: buttons} ]}); g.clearRect(Bangle.appRect); @@ -229,10 +274,10 @@ function checkMessages(options) { options=options||{}; // If no messages, just show 'no messages' and return if (!MESSAGES.length) { - if (!options.clockIfNoMsg) return E.showPrompt("No Messages",{ - title:"Messages", + if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{ + title:/*LANG*/"Messages", img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), - buttons : {"Ok":1} + buttons : {/*LANG*/"Ok":1} }).then(() => { load() }); return load(); } @@ -244,7 +289,8 @@ function checkMessages(options) { // no new messages - go to clock? if (options.clockIfAllRead && newMessages.length==0) return load(); - + // we don't have to time out of this screen... + cancelReloadTimeout(); // Otherwise show a menu E.showScroller({ h : 48, @@ -259,7 +305,7 @@ function checkMessages(options) { var x = r.x+2, title = msg.title, body = msg.body; var img = getMessageImage(msg); if (msg.id=="music") { - title = msg.artist || "Music"; + title = msg.artist || /*LANG*/"Music"; body = msg.track; } if (img) { @@ -286,9 +332,23 @@ function checkMessages(options) { }); } +function cancelReloadTimeout() { + if (!unreadTimeout) return; + clearTimeout(unreadTimeout); + unreadTimeout = undefined; +} + + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setTimeout(() => { + var unreadTimeoutSecs = (require('Storage').readJSON("messages.settings.json", true) || {}).unreadTimeout; + if (unreadTimeoutSecs===undefined) unreadTimeoutSecs=60; + if (unreadTimeoutSecs) + unreadTimeout = setTimeout(function() { + print("Message not seen - reloading"); + load(); + }, unreadTimeoutSecs*1000); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1}); },10); // if checkMessages wants to 'load', do that diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 3094b34e1..63f55dd03 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -1,10 +1,10 @@ +/* Push a new message onto messages queue, event is: + {t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool} + {t:"add",id:int, id:"music", state, artist, track, etc} // add new + {t:"remove-",id:int} // remove + {t:"modify",id:int, title:string} // modified +*/ exports.pushMessage = function(event) { - /* event is: - {t:"add",id:int, src,title,subject,body,sender,tel, important:bool} // add new - {t:"add",id:int, id:"music", state, artist, track, etc} // add new - {t:"remove-",id:int} // remove - {t:"modify",id:int, title:string} // modified - */ var messages, inApp = "undefined"!=typeof MESSAGES; if (inApp) messages = MESSAGES; // we're in an app that has already loaded messages @@ -16,7 +16,11 @@ exports.pushMessage = function(event) { if (mIdx>=0) messages.splice(mIdx, 1); // remove item mIdx=-1; } else { // add/modify - if (event.t=="add") event.new=true; // new message + if (event.t=="add"){ + if(event.new === undefined ) { // If 'new' has not been set yet, set it + event.new=true; // Assume it should be new + } + } if (mIdx<0) { mIdx=0; messages.unshift(event); // add new messages to the beginning @@ -27,17 +31,27 @@ exports.pushMessage = function(event) { // if in app, process immediately if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); // ok, saved now - we only care if it's new - if (event.t!="add") return; - // otherwise load after a delay, to ensure we have all the messages + if (event.t!="add") { + return; + } else if(event.new == false) { + return; + } + // otherwise load messages/show widget + var loadMessages = Bangle.CLOCK || event.important; + // first, buzz + if (loadMessages && global.WIDGETS && WIDGETS.messages) + WIDGETS.messages.buzz(); + // after a delay load the app, to ensure we have all the messages if (exports.messageTimeout) clearTimeout(exports.messageTimeout); exports.messageTimeout = setTimeout(function() { exports.messageTimeout = undefined; // if we're in a clock or it's important, go straight to messages app - if (Bangle.CLOCK || event.important) return load("messages.app.js"); + if (loadMessages) return load("messages.app.js"); if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know WIDGETS.messages.show(); }, 500); } +/// Remove all messages exports.clearAll = function(event) { var messages, inApp = "undefined"!=typeof MESSAGES; if (inApp) { diff --git a/apps/messages/screenshot-notify.gif b/apps/messages/screenshot-notify.gif new file mode 100644 index 000000000..3d0ed0b32 Binary files /dev/null and b/apps/messages/screenshot-notify.gif differ diff --git a/apps/messages/screenshot.png b/apps/messages/screenshot.png new file mode 100644 index 000000000..a95045400 Binary files /dev/null and b/apps/messages/screenshot.png differ diff --git a/apps/messages/settings.js b/apps/messages/settings.js index ef6266cf6..fd8ce8f39 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -3,6 +3,7 @@ let settings = require('Storage').readJSON("messages.settings.json", true) || {}; if (settings.vibrate===undefined) settings.vibrate="."; if (settings.repeat===undefined) settings.repeat=4; + if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; return settings; } function updateSetting(setting, value) { @@ -30,6 +31,12 @@ format: v => v+"s", onchange: v => updateSetting("repeat", v) }, + 'Unread timer': { + value: settings().unreadTimeout, + min: 0, max: 240, step : 10, + format: v => v?v+"s":"Off", + onchange: v => updateSetting("unreadTimeout", v) + }, }; E.showMenu(mainmenu); }) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index 3a22b40fd..f01d22ec7 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,10 +1,9 @@ - WIDGETS["messages"]={area:"tl",width:0,draw:function() { + Bangle.removeListener('touch', this.touch); if (!this.width) return; var c = (Date.now()-this.t)/1000; - g.reset().setBgColor((c&1) ? "#0f0" : "#030").setColor((c&1) ? "#000" : "#fff"); - g.clearRect(this.x,this.y,this.x+this.width,this.y+23); - g.setFont("6x8:1x2").setFontAlign(0,0).drawString("MESSAGES", this.x+this.width/2, this.y+12); + g.reset().clearRect(this.x,this.y,this.x+this.width,this.y+23); + 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) || {}; if (settings.repeat===undefined) settings.repeat = 4; @@ -13,9 +12,11 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { WIDGETS["messages"].buzz(); // buzz every 4 seconds } setTimeout(()=>WIDGETS["messages"].draw(), 1000); -},show:function() { + if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); +},show:function(quiet) { 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; Bangle.drawWidgets(); Bangle.setLCDPower(1);// turns screen on @@ -33,4 +34,15 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { if (c=="-") Bangle.buzz(500).then(()=>setTimeout(b,100)); } b(); +},touch:function(b,c) { + var w=WIDGETS["messages"]; + if (!w||!w.width||c.xw.x+w.width||c.yw.y+23) return; + load("messages.app.js"); }}; +/* We might have returned here if we were in the Messages app for a +message but then the watch was never viewed. In that case we don't +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); +})(); diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/mylocation/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md new file mode 100644 index 000000000..fd597397a --- /dev/null +++ b/apps/mylocation/README.md @@ -0,0 +1,41 @@ +# My Location + + *Sets and stores GPS lat and lon of your preferred city* + +* Select one of the preset Cities or setup through the GPS +* Other Apps can read this information to do calculations based on location +* When the City shows ??? it means the location has been set through the GPS + +## Example Code + + const LOCATION_FILE = "mylocation.json"; + let location; + + // requires the myLocation app + function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + } + +## Screenshots + +### Select one of the Preset Cities + +* The presets are London, Newcastle, Edinburgh, Paris, New York, Tokyo + +![](screenshot_1.png) + +### Or select 'Set By GPS' to start the GPS + +![](screenshot_2.png) + +### While the GPS is running you will see: + +![](screenshot_3.png) + +### When a GPS fix is received you will see: + +![](screenshot_4.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/mylocation/mylocation.app.js b/apps/mylocation/mylocation.app.js new file mode 100644 index 000000000..fb2f73fa7 --- /dev/null +++ b/apps/mylocation/mylocation.app.js @@ -0,0 +1,75 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const SETTINGS_FILE = "mylocation.json"; +let settings; + +// initialize with default settings... +let s = { + 'lat': 51.5072, + 'lon': 0.1276, + 'location': "London" +} + +function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || s; +} + +function save() { + settings = s + require('Storage').write(SETTINGS_FILE, settings) +} + +const locations = ["London", "Newcastle", "Edinburgh", "Paris", "New York", "Tokyo","???"]; +const lats = [51.5072 ,54.9783 ,55.9533 ,48.8566 ,40.7128 ,35.6762, 0.0]; +const lons = [-0.1276 ,-1.6178 ,-3.1883 ,2.3522 , -74.0060 ,139.6503, 0.0]; + +function setFromGPS() { + Bangle.on('GPS', (gps) => { + //console.log("."); + if (gps.fix === 0) return; + //console.log("fix from GPS"); + s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' } + Bangle.buzz(1500); // buzz on first position + Bangle.setGPSPower(0); + save(); + + Bangle.setUI("updown", ()=>{ load() }); + E.showPrompt("Location has been saved from the GPS fix",{ + title:"Location Saved", + buttons : {"OK":1} + }).then(function(v) { + load(); // load default clock + }); + }); + + Bangle.setGPSPower(1); + E.showMessage("Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running"); + Bangle.setUI("updown", undefined); +} + +function showMainMenu() { + console.log("showMainMenu"); + const mainmenu = { + '': { 'title': 'My Location' }, + '{ load(); }, + 'City': { + value: 0 | locations.indexOf(s.location), + min: 0, max: 6, + format: v => locations[v], + onchange: v => { + if (v != 6) { + s.location = locations[v]; + s.lat = lats[v]; + s.lon = lons[v]; + save(); + } + } + }, + 'Set From GPS': ()=>{ setFromGPS(); } + } + return E.showMenu(mainmenu); +} + +loadSettings(); +showMainMenu(); diff --git a/apps/mylocation/mylocation.icon.js b/apps/mylocation/mylocation.icon.js new file mode 100644 index 000000000..bfb38d5ac --- /dev/null +++ b/apps/mylocation/mylocation.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///t/7j/P3/vB4cBqtVoAbHBQIABBQ0FBYdQBYsVBYdUERIkGHIQADHoguEGAwuEGAwKFBZg8DHQw8EBYNf/1Vq3/8oLDIwNf/Wpv//0oLG9Wq3/qBYJUCBYuqBaBqBBYW+BepHEBbybCBYP+BYSnErYLDyoLFAANq/r8Ga5T7MBZZUBAAhSCfhA6DBZhIGBQg8FHQg8GHQgwGFwowFBQwwDFwwLMlS7Bqta1AKEn2q1K1C1WgBYf/1WqBYIDB1QKCgYLC0taBYoXB/QICBY0//7vBAAQ8EEgIABCwwME9QVEA")) diff --git a/apps/mylocation/mylocation.png b/apps/mylocation/mylocation.png new file mode 100644 index 000000000..7148990a4 Binary files /dev/null and b/apps/mylocation/mylocation.png differ diff --git a/apps/mylocation/screenshot_1.png b/apps/mylocation/screenshot_1.png new file mode 100644 index 000000000..a9c61b6b3 Binary files /dev/null and b/apps/mylocation/screenshot_1.png differ diff --git a/apps/mylocation/screenshot_2.png b/apps/mylocation/screenshot_2.png new file mode 100644 index 000000000..4c4404540 Binary files /dev/null and b/apps/mylocation/screenshot_2.png differ diff --git a/apps/mylocation/screenshot_3.png b/apps/mylocation/screenshot_3.png new file mode 100644 index 000000000..81570670b Binary files /dev/null and b/apps/mylocation/screenshot_3.png differ diff --git a/apps/mylocation/screenshot_4.png b/apps/mylocation/screenshot_4.png new file mode 100644 index 000000000..ffae679c9 Binary files /dev/null and b/apps/mylocation/screenshot_4.png differ diff --git a/apps/mywelcome/ChangeLog b/apps/mywelcome/ChangeLog index b012da933..f2b54e42c 100644 --- a/apps/mywelcome/ChangeLog +++ b/apps/mywelcome/ChangeLog @@ -14,3 +14,4 @@ 0.10: Add birthday style 0.11: Skip double buffering, use 240x240 size 0.12: Fix swipe direction (#800) +0.13: Bangle.js 2 support diff --git a/apps/mywelcome/app.js b/apps/mywelcome/app-bangle1.js similarity index 100% rename from apps/mywelcome/app.js rename to apps/mywelcome/app-bangle1.js diff --git a/apps/mywelcome/app-bangle2.js b/apps/mywelcome/app-bangle2.js new file mode 100644 index 000000000..aeee6918d --- /dev/null +++ b/apps/mywelcome/app-bangle2.js @@ -0,0 +1,254 @@ +// exec each function from seq one after the other +function animate(seq,period) { + var c = g.getColor(); + var i = setInterval(function() { + if (seq.length) { + var f = seq.shift(); + g.setColor(c); + if (f) f(); + } else clearInterval(i); + },period); +} + +// Fade in to FG color with angled lines +function fade(col, callback) { + var n = 0; + function f() {"ram" + g.setColor(col); + for (var i=n;i<240;i+=10) g.drawLine(i,0,0,i).drawLine(i,240,240,i); + g.flip(); + n++; + if (n<10) setTimeout(f,0); + else callback(); + } + f(); +} + + +var SCENE_COUNT=11; +function getScene(n) { + if (n==0) return function() { + console.log("Start app"); + g.clear(1); + eval(require("Storage").read("mywelcome.custom.js")); + } + if (n==1) return function() { + g.reset().setBgColor(0).clearRect(0,0,176,176); + g.setFont("6x15"); + var n=0; + var l = Bangle.getLogo(); + var im = g.imageMetrics(l); + var i = setInterval(function() { + n+=0.1; + g.setColor(n,n,n); + g.drawImage(l,(176-im.width)/2,(176-im.height)/2); + if (n>=1) { + clearInterval(i); + setTimeout(()=>g.drawString("Open",44,104), 500); + setTimeout(()=>g.drawString("Hackable",44,116), 1000); + setTimeout(()=>g.drawString("Smart Watch",44,128), 1500); + } + },50); + }; + if (n==2) return function() { + var img = require("heatshrink").decompress(atob("ptR4n/j/4gH+8H5wl+jOukVVoHZ8dt/n//n37OtgH9sHhwHp4H5xmkGiH72MRje/LL/7iIAEE7sPEgoAC+AlagIlIiMQErPxDwUYxAABwIHCj8N7nOl3uEqa6BEggnFjfM5nCkUil3gEq5KDAAQmC6QmBE4JxSEhIABiQmB8QmSXoQlCYRMdEwIlCAAIlNhYlOiO85nNEyMPEoZwIAAcsYIYmPXoYlMiKaFExX/u9VEqLBBOYrCH+czmtVqJyDEpiaCOYsgSYszmc3qtTEqMR7hzG8AlGmd1OQglOOY6aEgYlCmmZoJMCTBrnD6SaIEoU/zOUuolSjbnBJgqaCEoU5zOXX4RyQYBBzCS4X5zNDqqZCJiERJg5zBEoVJEoM1JgYlQjhMHc4JLEmZMEEp6ZIJgPzS4WTmZMVTILmFYAK+BmglCmd1JgUYJiPNEorABEIOZygDBm5MCiJMQlhMH8ByBXwIlBJgUxJiMd5nOTIzlBTAK+BAANVq4jPAAS/HJgJyCTATAEACC/B4S/IJgIlCYAgAPiS/Kn5yEYANTEyPc5niOQxMB/LlCOapyJJgbpBYAZzROQK/Gl0ATIWfEoZzBc6IlB6SYGgBJBJgpzSlhyH8EAh5MBTIjnCuIlOjjlHTAJzC/LmDTSSYIEoTABOYIlETSKYHXwIABOYM0yYmETSCYHEobnDOYqaBExu8TAwlEc4U5EoiaCmK+NTAolFEwX0TQzBMXwXiEpTBCAAomNEoS+EEo4mIYIImKEoS+EEpDoBEyUbEo3gEo4mJdAImIJY4lJEycdEoPOOBYmPuIlE+HcJYhKKTZ1fhYkB2EAhnNcYMuEhomMr8A3YABEoJyB5gjOAAYmHm9VgELEoJMBEoXAEyXzE45YBJgXwEqx1I+ByDOYJyVJw5yCgEB3cQGgJMWJwQnCu6/CgFBigDB13S/glVAAf1qomCglEoADB1QDBADEPEoNVqEAolEgEKolKErJMDYAJMD0lE0AmaEoNaAgJMCFIYAahV/IgIiDOTgABNYJMEOToiCIoJMCOTzfCN4RMBOTxsDJIRyfIwZMBKQZzfJgRyfOYZMBOUBzCJgNKOT5zDJgLoCADxKBOAIABOT6aCAARyfOYRyjOYRyjOYlKEsBzEEsBzEOUJzDOUIABOUiaDOURzCOUZzCEscKCiY")); + var im = g.imageMetrics(img); + g.reset(); + g.setBgColor("#ff00ff"); + var y = 176, speed = 5; + function balloon(callback) { + y-=speed; + var x = (176-im.width)/2; + g.drawImage(img,x,y); + g.clearRect(x,y+81,x+77,y+81+speed); + if (y>30) setTimeout(balloon,0,callback); + else callback(); + } + fade("#ff00ff", function() { + balloon(function() { + g.setColor(-1).setFont("6x15:2").setFontAlign(0,0); + g.drawString("Welcome.",88,130); + }); + }); + setTimeout(function() { + var n=0; + var i = setInterval(function() { + n+=4; + g.scroll(0,-4); + if (n>150) + clearInterval(i); + },20); + },3500); + + }; + if (n==3) return function() { + g.reset(); + g.setBgColor("#ffff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 70, y = 25, h=25; + animate([ + ()=>g.drawString("Your",x,y+=h), + ()=>g.drawString("Bangle.js",x,y+=h), + ()=>g.drawString("has one",x,y+=h), + ()=>g.drawString("button",x,y+=h), + ()=>{g.setFont("12x20:2").setFontAlign(0,0,1).drawString("HERE!",150,88);} + ],200); + }; + if (n==4) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To wake the\nscreen up, or to\nselect", 88,60); + }; + if (n==5) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Long Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To go back to\nthe clock", 88,60); + }; + if (n==6) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFontAlign(0,0).setFont("12x20"); + g.drawString("If Bangle.js ever\nstops, hold the\nbutton for\nten seconds.\n\nBangle.js will\nthen reboot.", 88,78); + }; + if (n==7) return function() { + g.reset(); + g.setBgColor("#0000ff").setColor(-1).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -20, h=60; + animate([ + ()=>{g.drawString("Bangle.js has a\nfull touchscreen",x,y+=h);}, + 0,0, + ()=>{g.drawString("Drag up and down\nto scroll and\ntap to select",x,y+=h);}, + ],300); + }; + if (n==8) return function() { + g.reset(); + g.setBgColor("#00ff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -35, h=80; + animate([ + ()=>{g.drawString("Bangle.js comes\nwith a few\napps installed",x,y+=h);}, + 0,0, + ()=>{g.drawString("To add more, visit\nbanglejs.com/apps",x,y+=h);}, + ],400); + }; + if (n==9) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88; + g.drawString("You can also make\nyour own apps!",x,30); + g.drawString("Check out\nbanglejs.com",x,130); + + var rx = 0, ry = 0; + // draw a cube + function draw() { + // rotate + rx += 0.1; + ry += 0.11; + var rcx=Math.cos(rx), + rsx=Math.sin(rx), + rcy=Math.cos(ry), + rsy=Math.sin(ry); + // Project 3D coordinates into 2D + function p(x,y,z) { + var t; + t = x*rcy + z*rsy; + z = z*rcy - x*rsy; + x=t; + t = y*rcx + z*rsx; + z = z*rcx - y*rsx; + y=t; + z += 4; + return [88 + 60*x/z, 78+ 60*y/z]; + } + + var a; + // draw a series of lines to make up our cube + var s = 30; + g.clearRect(88-s,78-s,88+s,78+s); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,-1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.moveTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.moveTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + } + + setInterval(draw,50); + }; + if (n==10) return function() { + g.reset(); + g.setBgColor("#ffffff");g.clear(); + g.setFontAlign(0,0); + g.setFont("12x20"); + + var x = 88, y = 10, h=21; + animate([ + ()=>g.drawString("That's it!",x,y+=h), + ()=>{g.drawString("Press",x,y+=h*2); + g.drawString("the button",x,y+=h); + g.drawString("to start",x,y+=h); + g.drawString("Bangle.js",x,y+=h);} + ],400); + } +} + +var sceneNumber = 0; + +function move(dir) { + if (dir>0 && sceneNumber+1 == SCENE_COUNT) return; // at the end + sceneNumber = (sceneNumber+dir)%SCENE_COUNT; + if (sceneNumber<0) sceneNumber=0; + clearInterval(); + getScene(sceneNumber)(); + if (sceneNumber>1) { + var l = SCENE_COUNT; + for (var i=0;i move(dir)); +setWatch(()=>{ + if (sceneNumber == SCENE_COUNT-1) + load(); + else + move(1); +}, BTN1, {repeat:true}); + +Bangle.setLCDTimeout(0); +Bangle.setLocked(0); +Bangle.setLCDPower(1); +move(0); diff --git a/apps/mywelcome/custom.html b/apps/mywelcome/custom.html index c4c721765..340f178e8 100644 --- a/apps/mywelcome/custom.html +++ b/apps/mywelcome/custom.html @@ -28,13 +28,15 @@ function getApp() { var line3 = document.getElementById("line3").value; var line4 = document.getElementById("line4").value; var style = document.getElementById("style").value; + // build the app's text using a templated String if (style=="Birthday") return `(function() { var ib = require("heatshrink").decompress(atob("jk0ggGDhOZAAWQCYwMEBxAMFAAIaHyc/+c5DgwMC/84Dg4aCBgwcDBoOf+Y4GBoQEBn4zCI44DBDQ4NEyf4BpgoIBoefxINMBhApEBrQAKBrrrGWpANZHBT7FBpYqIFAYcJBggNOFQwoFDgwMHBwoMIBwYMKBrkykANLmcwBu0zBrMDBv4AFN5gA/ADY")); var ir = require("heatshrink").decompress(atob("jk0ggGDhvdAAXQCYwMEBxAMFAAIaH6c/+c9DgwMC/8zDg4aC/4YCHIwNB7/zHAwNCAgM/DQwqDAYIaHBonT/oNMFBAND74NNBhApEBrQAKBrrrGWpANZHBT7FBpYqIFAYcJBgkA5oMF7gNFFQwoFDgwMHHIoMIAAPM5gMKBrk0oANLmcwBu0zBrMDBv4AFN5gA/ADYA=")); var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/+c3DgwMC/8yDg4aC/4YCHIwNBv/zHAwNCAgM/DQwqDAYIaHBolz+4NMFBANDv8nBpgMIFIgNaABQNddYy1IBrI4KfYoNLFRAoDDhIMEgHnBgt+BooqGFAoqGBg4OFBhAODBhQNcmUgBpczmAN2mYNZgYN/AApvMAH4Ab")); var igift = require("heatshrink").decompress(atob("q1QxH+ADOi0QbZ5nMHDQAbKgIACKa4ACKnJWVKghW0KgxWTKgxWyKhBWRKhBWwKhRWPKhRWuKhhWNKhhWtKpxWKKhys8KxBU8Ky5U+KypU/KyhU/KyhU/KynGKn5WTKn5WUKmHCADpJJE7uYABZUfKuuYKv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/AAv+Kv5VT/wADyIAaKpIlbABZSEKv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/ADNtKv6rdKzZVwKhAABy5V/Khw")); - var W=240,H=240; + var W=g.getWidth(),H=g.getHeight(); + var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var blns = []; function updateFlake(f) { f.im = [ir,ig,ib][Math.round(Math.random()*100)%3]; @@ -60,7 +62,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ }); var x = W/2, y = H/2; g.drawImage(igift,x-43,y-80); - g.setFont("6x8",2).setFontAlign(0,0); + g.setFont(titleFont).setFontAlign(0,0); g.drawString(${JSON.stringify(line1)},x,y+=20); g.drawString(${JSON.stringify(line2)},x,y+=20); g.setFont("6x8"); @@ -68,7 +70,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ g.drawString(${JSON.stringify(line4)},x,y+=10); g.flip(); } - g.clear(); + g.clear(1).setBgColor(0).setColor(-1).clearRect(0,0,W,H); setInterval(draw,50); })()`; // if (style=="Christmas") @@ -76,6 +78,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ var isnow = require("heatshrink").decompress(atob("jEagQWTgfAAocf+gFDh4FDiARBggVB3AFBl3Agf8jfkn/AgX/v/9/+Agfv/2//YrBgfwh4wCgfghYFJCIYdFFIw1EIIpNFL44FFOIoAP")); var itree = require("heatshrink").decompress(atob("mtWxH+ADHHDTI0aGuXH5vNGmhqvTYIzBGtoxF6fTG4g4oGgQyBAAZssGoI0Ga1g1FGdo01ZgIAEGmHHNoLSuAAN/rdb0YFBGlgCBGYIABA4YArGYY1CGn4znAAM6GeVd5PQ5Iyurc/vQ0oGZFAn+d4XC3d5GddiGYIEBy+7zoEBGlFhoEcsQ9GT08+oFk1mkGdaVBMgNArnJ6/KzswGs/J6GlrlbqtbvPC5PCy8wGohniMIPJvIpCqmX3e7vI0BqhqlMIY0DqhtBqoEBa0xgBMIIoEqoABGQwzfsIhBv4qHABM50vQGjg1CGaN66DoBGt1ioGd5LoBGjo1PGYNhvLoCa7wnBqgvGA4YzCAgN5GUAsCqoDBmAHCAYU/wPQ0oSDGcBiDqkwAYcxoFd5PX6GdGjrIIqtUAAc3jk5vPC4fCy5pef5I2BTQMcnAHBy+7y95T0oADnFk1ekBpI2aGRUin7NGAA9hsIzVsIgHTAKZBZoPJ5LNDGhBpXGolcwOsrtcA4TNB3bNDGb/+sVin9AoGe6HX5InEvN/TkP+5XQwM/sRsBzqWB4QuKGjvC6HQ4QdDvKWBZYMwmAuHmFUCYNbqibX3fD5O7qolEZQQ0FBwgKDqgJBGiphEDwNUEgJbBFIQqCAgYOCB4IzCnE6GyhYFGoQnDABYzGAAQ1UAAo2NBoQSBnOB0t/Gjo2EABIPCoGe6HX4QzTGRIAEqtVF4QEBBQc4oE4y/J5PCvIxeABk/oADBvO73eXTyAyZMwM/Awd5vIOFGslAr2Av4PLNcU/jmA6HX5I1KasFcn8dTIOd5PJ4SZGGiNhAAIyNn0ckU+ZYe7AAJpJEYJnNGZk+n9kw9cBAcwGoN5aZg1JJJQABm8/oEjoDKC5ALCrUwqh/NrvQ6HDGp04n9doEdoE/sQJBZQZhCqgABGZk6zw0K/1dnVAoNAFwOlCYL1FubJBy4GCGh1AnOX4XC3YzHFYOeCgdV5PQ5OdD4rKBqqYNGYlbv+X3edGY3CGgKMDAAO7JAJgDAClcr2BEYgADaIZ0DL4uXGbDuB6HX5I1GsP+sNhOgWXIhBmWd4Od5PK4TwFGIJoBAYI2BAD0/jlcQoO7AAJaEGQQADGr0/sjNEvOdAoZmDGgw2ZsVAkeAZpQACGZI2VsU/kVGn1bZoPJZogpGGhA4GfRYwBoGC1mlBQbNFFoo0JNxAGCEod/wM6oFAn9iv/J6/Kzo1Ey9/MZQAKCg4GCFgTDEvPCSwI0BC5I0RN4ocEYYPQ5OdHgeXSwTFKGaJyKFYPC3f+MIdbpzFLAD4zB/1OqtbqtOGgYArGAIADGl9UAAI0wGQN5GoQ0vvIABGoI0uGYQABqo0zNOg0uaQY0/GllOGn40//w=")); var W=g.getWidth(),H=g.getHeight(); + var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var flakes = []; for (var i=0;i<10;i++) { var f = { @@ -97,7 +100,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ }); var x = W/2, y = H/2; g.drawImage(itree,x-27,y-80); - g.setFont("6x8",2).setFontAlign(0,0); + g.setFont(titleFont).setFontAlign(0,0); g.drawString(${JSON.stringify(line1)},x,y+=20); g.drawString(${JSON.stringify(line2)},x,y+=20); g.setFont("6x8"); @@ -105,7 +108,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ g.drawString(${JSON.stringify(line4)},x,y+=10); g.flip(); } - g.clear(); + g.clear(1).setBgColor(0).setColor(-1).clearRect(0,0,W,H); setInterval(draw,50); })(); `; diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index f94d719f4..57818c180 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -7,3 +7,4 @@ 0.07: Add date on touch and some improvements (see settings and readme) 0.08: Add new draw styles, tidy up draw functionality 0.09: Tweak for faster rendering +0.10: Enhance for use with Bangle2, insert new draw mode 'thickfill' \ No newline at end of file diff --git a/apps/numerals/README.md b/apps/numerals/README.md index ebf4c10fe..7a8c25212 100644 --- a/apps/numerals/README.md +++ b/apps/numerals/README.md @@ -7,14 +7,20 @@ Settings can be accessed through the app/widget settings menu of the Bangle.js ### Color: * rnd - shows numerals in different color combinations every time the watches wakes -* r/g - red/green -* y/w - yellow/white -* o/c - orange/cyan -* b/y - blue/yellow'ish +* r/g - red/green (Bangle1/Bangle2) +* y/w - yellow/white (Bangle1 only) +* o/c - orange/cyan (Bangle1 only) +* b/y - blue/yellow'ish (Bangle1 only) +* r/g - red/green (Bangle2 only) +* g/b - green/blue (Bangle2 only) +* r/c - red/cyan (Bangle2 only) +* m/g - magenta/green (Bangle2 only) ### Draw mode * fill - fill numerals * frame - only shows outline of numerals +* framefill - frame with lighter color fill +* thickfill - thick frame in theme foreground color ### Menu button * choose button to start launcher menu with diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js index 3c7607eb1..baf859915 100644 --- a/apps/numerals/numerals.app.js +++ b/apps/numerals/numerals.app.js @@ -6,7 +6,7 @@ * + see README.md for details */ -var numerals = { + var numerals = { 0:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[30,25,61,25,69,33,69,67,61,75,30,75,22,67,22,33]], 1:[[50,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,50,27,42,19,42,9]], 2:[[9,1,82,1,90,9,90,53,82,61,21,61,21,74,82,74,90,82,90,92,82,100,9,100,1,92,1,48,9,40,70,40,70,27,9,27,1,19,1,9]], @@ -19,8 +19,8 @@ var numerals = { 9:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,69,74,69,61,9,61,1,53,1,9],[22,27,69,27,69,41,22,41]], }; var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; -var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; -var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; +var _hCol = []; +var _mCol = []; var _rCol = 0; var scale = g.getWidth()/240; var interval = 0; @@ -42,15 +42,23 @@ var drawFuncs = { }, thickframe : function(poly,isHole){ g.drawPoly(poly,true); - g.drawPoly(translate(1,0,poly),true); - g.drawPoly(translate(1,1,poly),true); - g.drawPoly(translate(0,1,poly),true); + g.drawPoly(translate(1,0,poly,1),true); + g.drawPoly(translate(1,1,poly,1),true); + g.drawPoly(translate(0,1,poly,1),true); + }, + thickfill : function(poly,isHole){ + if (isHole) g.setColor(g.theme.bg); + g.fillPoly(poly,true); + g.setColor(g.theme.fg); + g.drawPoly(translate(1,0,poly,1),true); + g.drawPoly(translate(1,1,poly,1),true); + g.drawPoly(translate(0,1,poly,1),true); } }; -function translate(tx, ty, p){ +function translate(tx, ty, p, ascale){ //return p.map((x, i)=> x+((i&1)?ty:tx)); - return g.transformVertices(p, {x:tx,y:ty,scale:scale}); + return g.transformVertices(p, {x:tx,y:ty,scale:ascale==undefined?scale:ascale}); } @@ -99,6 +107,18 @@ function setUpdateInt(set){ if (set) interval=setInterval(draw, REFRESH_RATE); } +function setUp(){ + if (process.env.HWVERSION==1){ + _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; + _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; + } else { + _hCol = ["#ff0000","#00ff00","#ff0000","#ff00ff"]; + _mCol = ["#00ff00","#0000ff","#00ffff","#00ff00"]; + } + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); +} + +setUp(); g.clear(1); // Show launcher when button pressed Bangle.setUI("clock"); @@ -111,11 +131,12 @@ if (settings.showDate) { } Bangle.on('lcdPower', function(on){ if (on){ - if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + setUp(); draw(); setUpdateInt(1); } else setUpdateInt(0); }); +Bangle.on('lock', () => setUp()); Bangle.loadWidgets(); -Bangle.drawWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js index 70f6e0d98..ae321322a 100644 --- a/apps/numerals/numerals.settings.js +++ b/apps/numerals/numerals.settings.js @@ -12,8 +12,8 @@ } let numeralsSettings = storage.readJSON('numerals.json',1); if (!numeralsSettings) resetSettings(); - let dm = ["fill","frame","framefill","thickframe"]; - let col = ["rnd","r/g","y/w","o/c","b/y"]; + let dm = ["fill","frame","framefill","thickframe","thickfill"]; + let col = process.env.HWVERSION==1?["rnd","r/g","y/w","o/c","b/y"]:["rnd","r/g","g/b","r/c","m/g"]; let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]]; var menu={ "" : { "title":"Numerals"}, diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 60b9d9ae3..6cb9d061e 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -7,3 +7,6 @@ 0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering 0.08: Update for drag event refactor 0.09: Use current theme cols when drawing GPS info +0.10: Improve scale factor calculation to fix scaling issues (#984) +0.11: Add slight offset to OSM data to align it properly (fix #984) + Fix alignment of satellite info text diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index c33acd8ad..62597ca20 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -25,11 +25,11 @@ function drawMarker() { var fix; Bangle.on('GPS',function(f) { fix=f; - g.reset().clearRect(0,y1,240,y1+8).setFont("6x8").setFontAlign(0,0); + g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0); var txt = fix.satellites+" satellites"; if (!fix.fix) txt += " - NO FIX"; - g.drawString(txt,120,y1 + 4); + g.drawString(txt,g.getWidth()/2,y1 + 4); drawMarker(); }); Bangle.setGPSPower(1, "app"); diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 88d94ed37..56dea1188 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -63,10 +63,17 @@ TODO: /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast - //var TILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; //var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white + var map = L.map('map').locate({setView: true, maxZoom: 16}); - var tileLayer = L.tileLayer(TILELAYER, { + // Tiles used for Bangle.js itself + var bangleTileLayer = L.tileLayer(TILELAYER, { + maxZoom: 18, + attribution: 'Map data © OpenStreetMap contributors' + }); + // Tiles used for the may the user sees (faster) + var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, { maxZoom: 18, attribution: 'Map data © OpenStreetMap contributors' }); @@ -83,7 +90,7 @@ TODO: } var mapFiles = []; - tileLayer.addTo(map); + previewTileLayer.addTo(map); function tilesLoaded(ctx, width, height) { var options = { @@ -122,16 +129,44 @@ TODO: } document.getElementById("getmap").addEventListener("click", function() { - var bounds = map.getBounds(); var zoom = map.getZoom(); - var centerlatlon = bounds.getCenter(); - var center = map.project(centerlatlon, zoom).divideBy(256); - var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE); - var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE); - center = center.floor(); - var tileGetters = []; + var centerlatlon = map.getBounds().getCenter(); + var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); + // Reason for 16px adjustment below not 100% known, but it seems to + // align everything perfectly: https://github.com/espruino/BangleApps/issues/984 + var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE) + 16; + var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE) + 16; + center = center.floor(); // make sure we're in the middle of a tile + // JS version of Bangle.js's projection + function bproject(lat, lon) { + const degToRad = Math.PI / 180; // degree to radian conversion + const latMax = 85.0511287798; // clip latitude to sane values + const R = 6378137; // earth radius in m + if (lat > latMax) lat=latMax; + if (lat < -latMax) lat=-latMax; + var s = Math.sin(lat * degToRad); + return new L.Point( + (R * lon * degToRad), + (R * Math.log((1 + s) / (1 - s)) / 2) + ); + } + // Work out scale factors (how much from Bangle.project does one pixel equate to?) + var pc = map.unproject(center.multiplyBy(OSMTILESIZE), zoom); + var pd = map.unproject(center.multiplyBy(OSMTILESIZE).add({x:1,y:0}), zoom); + var bc = bproject(pc.lat, pc.lng) + var bd = bproject(pd.lat, pd.lng) + var scale = bc.distanceTo(bd); - // Render everything to a canvas - 512 x 512 px + // test + /*var p = bproject(centerlatlon.lat, centerlatlon.lng); + var q = bproject(mylat, mylon); + var testPt = { + x : (q.x-p.x)/scale + (MAPSIZE/2), + y : (MAPSIZE/2) - (q.y-p.y)/scale + };*/ + + var tileGetters = []; + // Render everything to a canvas... var canvas = document.getElementById("maptiles"); canvas.style.display=""; var ctx = canvas.getContext('2d'); @@ -147,10 +182,16 @@ TODO: tileGetters.push(new Promise(function(resolve,reject) { img.onload = function(){ ctx.drawImage(img,i*OSMTILESIZE - ox, j*OSMTILESIZE - oy); + /*if (testPt) { + ctx.fillStyle="green"; + ctx.fillRect(testPt.x-1, testPt.y-5, 3,10); + ctx.fillRect(testPt.x-5, testPt.y-1, 10,3); + }*/ resolve(); }; })); - img.src = tileLayer.getTileUrl(coords); + bangleTileLayer._tileZoom = previewTileLayer._tileZoom; + img.src = bangleTileLayer.getTileUrl(coords); })(i,j); } } @@ -163,7 +204,7 @@ TODO: imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, - scale : 10000*Math.pow(2,16-zoom), // FIXME - this is probably wrong + scale : scale, // how much of Bangle.project(latlon) does one pixel equate to? lat : centerlatlon.lat, lon : centerlatlon.lng })}); diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 554a71ca3..d995aca25 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -34,8 +34,8 @@ exports.draw = function() { var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); - var ix = (p.x-map.center.x)*4096/map.scale + (map.imgx/2) - cx; - var iy = (map.center.y-p.y)*4096/map.scale + (map.imgy/2) - cy; + var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx; + var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy; //console.log(ix,iy); var tx = 0|(ix/map.tilesize); var ty = 0|(iy/map.tilesize); @@ -57,8 +57,8 @@ exports.latLonToXY = function(lat, lon) { var cx = g.getWidth()/2; var cy = g.getHeight()/2; return { - x : (q.x-p.x)*4096/map.scale + cx, - y : cy - (q.y-p.y)*4096/map.scale + x : (q.x-p.x)/map.scale + cx, + y : cy - (q.y-p.y)/map.scale }; }; @@ -66,6 +66,6 @@ exports.latLonToXY = function(lat, lon) { exports.scroll = function(x,y) { var a = Bangle.project({lat:this.lat,lon:this.lon}); var b = Bangle.project({lat:this.lat+1,lon:this.lon+1}); - this.lon += x * this.map.scale / ((a.x-b.x) * 4096); - this.lat -= y * this.map.scale / ((a.y-b.y) * 4096); + this.lon += x * this.map.scale / (a.x-b.x); + this.lat -= y * this.map.scale / (a.y-b.y); }; diff --git a/apps/openstmap/screenshot.png b/apps/openstmap/screenshot.png new file mode 100644 index 000000000..2895b562e Binary files /dev/null and b/apps/openstmap/screenshot.png differ diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index 6bfd2ce59..2ede0e161 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -3,5 +3,6 @@ 0.03: Make it work with Gadgetbridge, Notifications fullscreen on a Bangle 2 0.04: Leave space at the bottom for Chrono widget, set back option at first option 0.05: Added 2 new fonts -0.06: COnverted fonts to font modules +0.06: Converted fonts to font modules 0.07: Added info line that cycles on BTN1/BTN3 (or vitual buttons on a bangle 2) +0.08: Added dependancy on MyLocation diff --git a/apps/pastel/README.md b/apps/pastel/README.md index f183005a9..66ae0e189 100644 --- a/apps/pastel/README.md +++ b/apps/pastel/README.md @@ -1,6 +1,6 @@ # Pastel Clock - *a configurable clock with custom fonts and background* + *a configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times* * Designed specifically for Bangle 1 and Bangle 2 * A choice of 7 different custom fonts @@ -8,12 +8,13 @@ * Has a settings menu, change font, enable/disable the grid * On Bangle 1 use BTN1,BTN3 to cycle through the info display (Date, ID, Batt %, Ram % etc) * On Bangle 2 touch the top right/top left to cycle through the info display (Date, ID, Batt %, Ram % etc) - +* Uses mylocation.json from MyLocation app to calculate sunrise and sunset times for your location +* Uses pedometer widget to get latest step count +* Dependant apps are installed when Pastel installs I came up with the name Pastel due to the shade of the grid background. -## Creator -[Hugh Barney](https://github.com/hughbarney) +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/) ## Lato ![](screenshot_lato.png) diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js index 013a010cf..aa4f6abf8 100644 --- a/apps/pastel/pastel.app.js +++ b/apps/pastel/pastel.app.js @@ -1,6 +1,9 @@ +var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); require("f_latosmall").add(Graphics); const SETTINGS_FILE = "pastel.json"; +const LOCATION_FILE = "mylocation.json"; let settings; +let location; function loadSettings() { settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; @@ -8,6 +11,29 @@ function loadSettings() { settings.font = settings.font||"Lato"; } +// requires the myLocation app +function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; +} + +function extractTime(d){ + var h = d.getHours(), m = d.getMinutes(); + return(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2)); +} + +var sunRise = "00:00"; +var sunSet = "00:00"; +var drawCount = 0; + +function updateSunRiseSunSet(now, lat, lon, line){ + // get today's sunlight times for lat/lon + var times = SunCalc.getTimes(new Date(), lat, lon); + + // format sunrise time from the Date object + sunRise = extractTime(times.sunrise); + sunSet = extractTime(times.sunset); +} + function loadFonts() { // load font files based on settings.font if (settings.font == "Architect") @@ -39,6 +65,8 @@ const infoData = { ID_BLANK: { calc: () => '' }, ID_DATE: { calc: () => {var d = (new Date).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, ID_DAY: { calc: () => {var d = require("locale").dow(new Date).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} }, + ID_SR: { calc: () => 'Sunrise: ' + sunRise }, + ID_SS: { calc: () => 'Sunset: ' + sunSet }, ID_STEP: { calc: () => 'Steps: ' + stepsWidget().getSteps() }, ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' }, ID_MEM: { calc: () => {var val = process.memory(); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} }, @@ -148,6 +176,10 @@ function draw() { g.setFontLatoSmall(); g.setFontAlign(0, -1); g.drawString((infoData[infoMode].calc()), w/2, h - 24 - 24); + + if (drawCount % 3600 == 0) + updateSunRiseSunSet(new Date(), location.lat, location.lon); + drawCount++; } // Only update when display turns on @@ -169,6 +201,8 @@ Bangle.setUI("clockupdown", btn=> { loadSettings(); loadFonts(); +loadLocation(); + g.clear(); var secondInterval = setInterval(draw, 1000); draw(); diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog new file mode 100644 index 000000000..b3d37f841 --- /dev/null +++ b/apps/pebble/ChangeLog @@ -0,0 +1,5 @@ +0.01: first release +0.02: included deployment of pebble.settings.js in apps.json +0.03: Changed time+calendar font to LECO1976Regular, changed to slanting boot +0.04: Fix widget hiding code (fix #1046) +0.05: Fix typo in settings - Purple diff --git a/apps/pebble/LECO 1976-Regular.otf b/apps/pebble/LECO 1976-Regular.otf new file mode 100644 index 000000000..05a318224 Binary files /dev/null and b/apps/pebble/LECO 1976-Regular.otf differ diff --git a/apps/pebble/README.md b/apps/pebble/README.md new file mode 100644 index 000000000..4b0233781 --- /dev/null +++ b/apps/pebble/README.md @@ -0,0 +1,17 @@ +# Pebble + + *a Pebble style clock with configurable background color, to keep the revolution going* + +* Designed specifically for Bangle 2 +* A choice of 6 different background colous through its setting menu. Goto Settings, App/Widget settings, Pebble. +* Supports the Light and Dark themes +* Uses pedometer widget to get latest step count +* Dependant apps are installed when Pebble installs +* Uses the whole screen, widgets are made invisible but still run in the background +* When battery is less than 30% main screen goes Red + +![](pebble_screenshot.png) +![](pebble_screenshot2.png) +![](pebble_screenshot3.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/pebble/pebble.app.js b/apps/pebble/pebble.app.js new file mode 100644 index 000000000..106e09b82 --- /dev/null +++ b/apps/pebble/pebble.app.js @@ -0,0 +1,121 @@ +Graphics.prototype.setFontLECO1976Regular42 = function(scale) { + // Actual height 42 (41 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAA/AAAAAAAAH/AAAAAAAA//AAAAAAAP//AAAAAAB///AAAAAAP///AAAAAB////AAAAAf////AAAAD////4AAAAf////AAAAH////4AAAA////+AAAAA////wAAAAA///+AAAAAA///gAAAAAA//8AAAAAAA//gAAAAAAA/4AAAAAAAA/AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////gD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4B/gH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/wB////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/wB////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAH+AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("ERkmHyYmJiYmJCYmEQ=="), 60+(scale<<8)+(1<<16)); +} + +Graphics.prototype.setFontLECO1976Regular22 = function(scale) { + // Actual height 22 (21 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/nA/+cD/5wP/nAAAAAAAAPwAA/gAD+AAPwAAAAAD+AAP4AA/gAAAAAAAAAAAAAcOAP//A//8D//wP//AHDgAcOAP//A//8D//wP//AHDgAAAAAAAAH/jgf+OB/44H/jj8OP/w4//Dj/8OPxw/4HD/gcP+Bw/4AAAAAAAP+AA/8AD/wQOHHA4c8D//wP/8A//gAD4AAfAAH/8A//wP//A84cDjhwIP/AA/8AB/wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8ABwAAAAAAAAD8AAP4AA/gAD8AAAAAAAAAAAEAAD+AB//A///v/D//gB/wABwAAAAAADgAA/wAf/4P8///wf/4AP8AAOAAAAAAAAAyAAHcAAPwAD/gAP/AA/8AA/AAH8AAMwAAAAAAAAAAAAADgAAOAAA4AAf8AD/wAP/AA/8AAOAAA4AADgAAAAAAAAAAD8AAfwAB/AAD8AAAAAAAADgAAOAAA4AADgAAOAAA4AADgAAAAAAAAAADgAAOAAA4AADgAAAAAAAAABwAB/AA/8A//gP/gA/wADwAAIAAAAAAD//wP//A//8D//wOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA4AcDgBwOAHA//8D//wP//A//8AABwAAHAAAcAAAAAAAA+f8D5/wPn/A+f8DhxwOHHA4ccDhxwP/HA/8cD/xwP/HAAAAAAAAOAHA4AcDhxwOHHA4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/wAP/AA/8AD/wAAHAAAcAABwAAHAA//8D//wP//A//8AAAAAAAA/98D/3wP/fA/98DhxwOHHA4ccDhxwOH/A4f8Dh/wOH/AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccDh/wOH/A4f8Dh/wAAAAAAAD4AAPgAA+AADgAAOAAA4AADgAAP//A//8D//wP//AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA//8D//wP//A//8AAAAAAAAOA4A4DgDgOAOA4AAAAAAAAOA/A4H8DgfwOA/AAAAAAAAB4AAPwAA/AAD8AAf4ABzgAPPAA8cAHh4AAAAAAAAAAAAHHAAccABxwAHHAAccABxwAHHAAccABxwAHHAAAAAAAAAOHAA4cADzwAPPAAf4AB/gAD8AAPwAAeAAB4AAAAAAAAA+AAD4AAPgAA+ecDh9wOH3A4fcDhwAP/AA/8AD/wAP/AAAAAAAAAP//4///j//+P//44ADjn/OOf845/zjnHOP8c4//zj//OP/84AAAAAAAP//A//8D//wP//A4cADhwAOHAA4cAD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA//8D//wP9/A/j8AAAAAAAA//8D//wP//A//8DgBwOAHA4AcDgBwOAHA4AcDgBwOAHAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA8A8D//wH/+AP/wAf+AAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4ccDhxwOAHA4AcAAAAAAAA//8D//wP//A//8DhwAOHAA4cADhwAOHAA4cADgAAOAAAAAAD//wP//A//8D//wOAHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA//8D//wP//A//8ABwAAHAAAcAABwAP//A//8D//wP//AAAAAAAAP//A//8D//wP//AAAAAAAAOAHA4AcDgBwOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA//8D//wP//A//8AHwAA/AAP8AB/wAPn/A8f8DB/wIH/AAAAAAAAP//A//8D//wP//AAAcAABwAAHAAAcAABwAAHAAAAAAAAP//A//8D//wP//Af8AAP+AAH/AAD8AAHwAD/AB/wAf8AP+AA//8D//wP//AAAAAAAAP//A//8D//wP//AfwAAfwAAfwAAfwAAfwP//A//8D//wAAAAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHAA4cADhwAOHAA/8AD/wAP/AA/8AAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//+P//4///j//+AAA4AADgAAAP//A//8D//wP//A4eADh+AOH8A4f4D/3wP/HA/8MD/wQAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA4AADgAAOAAA//8D//wP//A//8DgAAOAAA4AADgAAAAAA//8D//wP//A//8AABwAAHAAAcAABwP//A//8D//wP//AAAADAAAPgAA/wAD/4AB/8AA/8AAfwAB/AA/8Af+AP/AA/wAD4AAMAAA4AAD+AAP/gA//8AH/wAB/AAf8Af/wP/4A/4AD/gAP/4AH/8AB/wAB/AB/8D//wP/gA/gADgAAIABA4AcDwDwPw/Afn4Af+AA/wAD/AA//AH5+A/D8DwDwOAHAgAEAAAAP/AA/8AD/wAP/AAAf8AB/wAH/AAf8D/wAP/AA/8AD/wAAAAAAAADh/wOH/A4f8Dh/wOHHA4ccDhxwOHHA/8cD/xwP/HA/8cAAAAAAAAf//9///3///f//9wAA3AADcAAMAAAOAAA/gAD/wAH/8AB/8AA/wAAPAAAEAAAAHAADcAANwAB3///f//9///wAA"), 32, atob("BwYLDg4UDwYJCQwMBgkGCQ4MDg4ODg4NDg4GBgwMDA4PDg4ODg4NDg4GDQ4MEg8ODQ8ODgwODhQODg4ICQg="), 22+(scale<<8)+(1<<16)); +} + +const SETTINGS_FILE = "pebble.json"; +let settings; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green'}; +} + +var img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA==")); + +const h = g.getHeight(); +const w = g.getWidth(); +const ha = 2*h/5 - 4; +const h2 = 3*h/5 - 10; +const h3 = 7*h/8; + +let batteryWarning = false; + +function draw() { + let date = new Date(); + let da = date.toString().split(" "); + let timeStr = da[4].substr(0,5); + const t = 6; + + // turn the warning on once we have dipped below 30% + if (E.getBattery() < 30) + batteryWarning = true; + + // turn the warning off once we have dipped above 40% + if (E.getBattery() > 40) + batteryWarning = false; + + g.reset(); + g.setColor(settings.bg); + g.fillRect(0, 0, w, h2 - t); + + // contrast bar + g.setColor(g.theme.fg); + g.fillRect(0, h2 - t, w, h2); + + // day and steps + if (settings.color == 'Blue' || settings.color == 'Red') + g.setColor('#fff'); // white on blue or red best contrast + else + g.setColor('#000'); // otherwise black regardless of theme + + g.setFontLECO1976Regular22(); + g.setFontAlign(0, -1); + g.drawString(da[0].toUpperCase(), w/4, ha); // day of week + g.drawString(getSteps(), 3*w/4, ha); + + // time + // white on red for battery warning + g.setColor(!batteryWarning ? g.theme.bg : '#f00'); + g.fillRect(0, h2, w, h3); + + g.setFontLECO1976Regular42(); + g.setFontAlign(0, -1); + g.setColor(!batteryWarning ? g.theme.fg : '#fff'); + g.drawString(timeStr, w/2, h2 + 8); + + // contrast bar + g.setColor(g.theme.fg); + g.fillRect(0, h3, w, h3 + t); + + // the bottom + g.setColor(settings.bg); + g.fillRect(0, h3 + t, w, h); + + g.setColor(settings.bg); + g.drawImage(img, w/2 + ((w/2) - 64)/2, 1, { scale: 1 }); + drawCalendar(((w/2) - 42)/2, 14, 42, 4, da[2]); +} + +// at x,y width:wi thicknes:th +function drawCalendar(x,y,wi,th,str) { + g.setColor(g.theme.fg); + g.fillRect(x, y, x + wi, y + wi); + g.setColor(g.theme.bg); + g.fillRect(x + th, y + th, x + wi - th, y + wi - th); + g.setColor(g.theme.fg); + + let hook_t = 6; + // first calendar hook, one third in + g.fillRect(x + (wi/3) - (th/2), y - hook_t, x + wi/3 + th - (th/2), y + hook_t); + // second calendar hook, two thirds in + g.fillRect(x + (2*wi/3) -(th/2), y - hook_t, x + 2*wi/3 + th - (th/2), y + hook_t); + + g.setFontLECO1976Regular22(); + g.setFontAlign(0, 0); + g.drawString(str, x + wi/2, y + wi/2 + th); +} + +function getSteps() { + if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } + return '????'; +} + +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. + */ +for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +loadSettings(); +setInterval(draw, 15000); // refresh every 15s +draw(); +Bangle.setUI("clock"); diff --git a/apps/pebble/pebble.icon.js b/apps/pebble/pebble.icon.js new file mode 100644 index 000000000..ecd7feb7f --- /dev/null +++ b/apps/pebble/pebble.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("oFAwgNKiIAIFqofegIf/DAUzAAMyAwUQD60T/4ACD7Q/cPxIf/YCofcDhYiSXYYfuUZgf/D/4f/D6USkUgD/4fuogAID6vtDw/UD6vu6geF73kb6vuEAtN9wfYMIneD7JADDwIfaIAJdBD7YgBHwQfbAAgfkf6Qf/D/4feogAID6oAND/4f/iAdJD/4f/D/4fUDxYABD74iODiAftTZgfnYYczAAMyD7UT/4ACH/S+bD8DAKD9Y=")) diff --git a/apps/pebble/pebble.png b/apps/pebble/pebble.png new file mode 100644 index 000000000..10f5adb56 Binary files /dev/null and b/apps/pebble/pebble.png differ diff --git a/apps/pebble/pebble.settings.js b/apps/pebble/pebble.settings.js new file mode 100644 index 000000000..ff408907d --- /dev/null +++ b/apps/pebble/pebble.settings.js @@ -0,0 +1,38 @@ +(function(back) { + const SETTINGS_FILE = "pebble.json"; + + // initialize with default settings... + let s = {'bg': '#0f0', 'color': 'Green'} + + // ...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) || s; + const saved = settings || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; + var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; + + E.showMenu({ + '': { 'title': 'Pebble Clock' }, + '< Back': back, + 'Colour': { + value: 0 | color_options.indexOf(s.color), + min: 0, max: 5, + format: v => color_options[v], + onchange: v => { + s.color = color_options[v]; + s.bg = bg_code[v]; + save(); + }, + } + }); +}) diff --git a/apps/pebble/pebble_screenshot.png b/apps/pebble/pebble_screenshot.png new file mode 100644 index 000000000..169df2d22 Binary files /dev/null and b/apps/pebble/pebble_screenshot.png differ diff --git a/apps/pebble/pebble_screenshot2.png b/apps/pebble/pebble_screenshot2.png new file mode 100644 index 000000000..cd09e1c2f Binary files /dev/null and b/apps/pebble/pebble_screenshot2.png differ diff --git a/apps/pebble/pebble_screenshot3.png b/apps/pebble/pebble_screenshot3.png new file mode 100644 index 000000000..0de8df516 Binary files /dev/null and b/apps/pebble/pebble_screenshot3.png differ diff --git a/apps/pooqroman/ChangeLog b/apps/pooqroman/ChangeLog new file mode 100644 index 000000000..c4f3171d3 --- /dev/null +++ b/apps/pooqroman/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial check-in. +0.02: Make internal menu time out + small fixes. +0.03: Autolight feature. diff --git a/apps/pooqroman/README.md b/apps/pooqroman/README.md new file mode 100644 index 000000000..87acea9ca --- /dev/null +++ b/apps/pooqroman/README.md @@ -0,0 +1,45 @@ +# pooq Roman: a classic watch face with amusing dynamicity + +This is a normal watch face for telling the time. +It is unusual in that it supports the 24 hour clock by dynamically updating the labels on the face +(so, if you enable 24 hour mode, you will get to see a hand pointing to XXIII o'clock each evening). + +The date and day of the week can also be displayed, and they choose their own spelling depending on the available screen space. It's fun! + +## Options + +Because sometimes I don't want to burn what I'm cooking and other times I'm lazy and just want to know if it's afternoon yet, +you can alter the number of hands on the display. When the watch is unlocked, slide up to add minute and second hands, or down to remove the distraction. +There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky, in case you want +the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails. + +Although we generally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face. +You can also override the system 12/24 hour setting just for this face here, since it's, well, a rather different experience than with numeric displays. + +By default, there is a backlight that comes on when you twist your wrist. This, of course, somewhat increases power draw and could be +annoying in an intentionally dark environment, so there is an option to disable it. + +One other thing: there's some integration with system timers and alarms; they will show as small pips at the appropriate places +in the day around the display. When they come within an hour, the pips turn to crosses relating to the minute hand, and the minute +hand turns itself on. When timers are mere seconds away, the display changes again and the second hand activates itself, so you +can watch as your doom approaches. + +## Limitations + +Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets. +Sorry about that, but control of all the pixels was just too important to me. + +There's also no support for internationalisation at present. This irks me, but... well, talk to me about it if there's a language you'd like. + +## The future + +The design is begging for integration with host-device calendars, and proper time zone/DST support. We'll see what the future holds. + +## Feedback + +[I'd be happy to hear your feedback](https://www.github.com/stephenPspackman) if you have comments or find any bugs, or (most especially) +if you find this work interesting. + +## By + +Made by [Stephen P Spackman](https://www.github.com/stephenPspackman). diff --git a/apps/pooqroman/app-icon.js b/apps/pooqroman/app-icon.js new file mode 100644 index 000000000..20a9c8b0a --- /dev/null +++ b/apps/pooqroman/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIAWiEAgIpKEwgrFgAaBgIcBAAwREC4oVBBoQoCAQoXJBogXqI653DC6SnEC9RHXX/6/kSgIAGU5wAICQhfGACAX/C/4AOXIIX/C/4X/C/4XUgEBF6wYHI6AYGL6MACIgXRCIISDR6QYEU6YYDX6gYCAAKxHDB4XTDAYXUL6oAgA==")) diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js new file mode 100644 index 000000000..bed8ef3d2 --- /dev/null +++ b/apps/pooqroman/app.js @@ -0,0 +1,782 @@ +/* -*- mode: Javascript; c-basic-offset: 2; indent-tabs-mode: nil; coding: latin-1 -*- */ +// pooqRoman +// +// Copyright (c) 2021 Stephen P Spackman +// +// 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. +// +// Notes: +// +// This only works for Bangle 2. + +////////////////////////////////////////////////////////////////////////////// +/* System integration */ + +const storage = require('Storage'); + +const settings = storage.readJSON("setting.json", true) || {}; + +const alarms = storage.readJSON('alarm.json', true) || []; + +/* + { on : true, + hr : 6.5, // hours + minutes/60 + msg : "Eat chocolate", + last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! + rp : true, // repeat + as : false, // auto snooze + timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes + } +*/ + +////////////////////////////////////////////////////////////////////////////// +/* Face-specific options */ + +class Options { + // Protocol: subclasses must have static id and defaults fields. + // Only fields named in the defaults will be saved. + constructor() { + this.id = this.constructor.id; + this.file = `${this.id}.json`; + this.backing = storage.readJSON(this.file, true) || {}; + Object.setPrototypeOf(this.backing, this.constructor.defaults); + this.reactivator = _ => this.active(); + Object.keys(this.constructor.defaults).forEach(k => this.bless(k)); + } + + writeBack(delay) { + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout( + () => { + this.timeout = null; + storage.writeJSON(this.file, this.backing); + }, + delay + ); + } + + bless(k) { + Object.defineProperty(this, k, { + get: () => this.backing[k], + set: v => { + this.backing[k] = v; + // Ten second writeback delay, since the user will roll values up and down. + this.writeBack(10000); + } + }); + } + + showMenu(m) { + if (m instanceof Function) m = m(); + if (m) { + for (const k in m) if ('init' in m[k]) m[k].value = m[k].init(); + m[''].selected = -1; // Workaround for self-selection bug. + Bangle.on('drag', this.reactivator); + this.active(); + } else { + if (this.bored) clearTimeout(this.bored); + this.bored = null; + Bangle.removeListener('drag', this.reactivator); + this.emit('done'); + } + g.clear(true); + E.showMenu(m); + } + + active() { + if (this.bored) clearTimeout(this.bored); + this.bored = setTimeout(_ => this.showMenu(), 15000); + } + + reset() { + this.backing = {__proto__: this.constructor.defaults}; + this.writeBack(0); + } +} + +class RomanOptions extends Options { + constructor() { + super(); + this.menu = { + '': {title: '* face options *'}, + '< Back': _ => this.showMenu(), + Ticks: { + init: _ => this.resolution, + min: 0, max: 3, + onchange: x => this.resolution = x, + format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x] + }, + 'Display': { + init: _ => this.o24h == null ? 0 : 1 + this.o24h, + min: 0, max: 2, + onchange: x => this.o24h = [null, 0, 1][x], + format: x => ['system', '12h', '24h'][x] + }, + 'Day of Week': { + init: _ => this.dow, + onchange: x => this.dow = x + }, + Calendar: { + init: _ => this.calendric, + min: 0, max: 2, + onchange: x => this.calendric = x, + format: x => ['none', 'day', 'date'][x] + }, + 'Auto-Illum.': { + init: _ => this.autolight, + onchange: x => this.autolight = x + }, + Defaults: _ => {this.reset(); this.interact();} + }; + } + + interact() {this.showMenu(this.menu);} +} + +RomanOptions.id = 'pooqroman'; + +RomanOptions.defaults = { + resolution: 1, + dow: true, + calendric: 2, + o24h: !settings["12hour"], + bg: g.theme.bg, + fg: g.theme.fg, + barBg: g.theme.fg, + barFg: g.theme.bg, + hourFg: g.theme.fg, + minuteFg: g.theme.fg, + secondFg: g.theme.fg2, + rectFg: g.theme.fg, + hubFg: g.theme.fg, + alarmFg: '#f00', + timerFg: '#0f0', + activeFg: g.theme.fg2, + autolight: true, +}; + +////////////////////////////////////////////////////////////////////////////// +/* Assets (generated by resourcer.js, in this directory) */ + +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +const romanPartsF = [ + dec( + 'wEBsEB3//7//9//+0AjUAguAg3AgYQJjfAgv+gH/8Fg/0gh/AgP4gf2h/j/+BCAP' + + 'wgFggEggEQgEMgEHwEDEIIyDuED3kD7+H9vn2k/hEPgMP4Xevd+j4QB7kA9kAmkA' + + 'hUGgOH8Hn3le4+GgH32PuvfGj+CCAMDgXD4dz+evt9DgcL7fXn87h8NCAMP+Ef/0' + + 'eg+egPugF2j0bCAPAh3wh88h8P/8BNwI' + ), 97, dec('gUDgUGgUJgYFBhsBhMJhgA=='), 17 +];const fontF = [ + dec( + 'AAUwAIM/4F/8HguHAmABBAoIJBBoIUBkEwsEw//wAIIdDBoUQBoIfC+HB+Hj2F/m' + + 'E+CIXAoHEsHMuHcmH8mHuuHH8GBGIUAwEBwEHwH/wH5+EBAIILCCAP8oH8EYXMmA' + + 'BB5wjCgYjCAYMP8E+uF8mHsCIWHCIgCBAIXw4fw54tBgBsBGgUAnKLC99w40wAII' + + 'FBBIINBCIM8gF+iHnmHDuHD8HnDYMAjizEMYJJBn+A+OAAYIHBBYKjDXYKvDYZYP' + + 'D40AAIYMBZYgkC4Hg4DnDuH/8H/BYIVCv/wnEAjwBCAoIJBEIYRFh0Ag8AgPAEYQ' + + 'RCJIJNBfYRXKnFAvlg9ihE8dwsfgkLFHMYgJF8DNCh+AUYWAA4ILBAAJGB/4PB+D' + + '9CgADCEoIPCJobbBB4IBBAoJdDEgXggvwhuwAIcH8EDRIh/BhkwAIMOuAPCMYQDB' + + 'A4ILBCIcGsECoAPLU4oPDH42ggeAB4XEg/mh1zhkzh03g/+h/4J4nwg0AhjbDRII' + + 'vCt/wAIIVFAoKTBCYIXBDIYHHEIYVFGJJxHSI8P/8H/6hLF44BBM4IABg8gh6NEh' + + 'vwgngBoITBv/Av7PBV4kAsArCfYIVBuEABYNwA4I3BD4cPL4UAM4IXBBYQfC4kP8' + + '0AucAmcAu8PXogA=' + ), 32, dec('gINMgUAhMHhIAGCQ0KAQIKBgwEBgcIBAQVEhIJBhAeIBQIADAoUDEQULBQcHg4FD' + + 'CII='), 16 +]; +const lockI = dec('iMSwMAgfwgf8geHgeB4PA8HguFwnH//9//+4gPf//v//3gE7//9//+8EHCAO///A'); +const batteryI = dec('h8SwMAgPggfAv/4//x//j//H/+P/8f/0//gOOA=='); +const chargeI = dec('h0MwIEBkEBwEMgFwgeAj/w/+AjkA8EDgEYgFAA=='); +const GPSI = dec('iUQwMAhEAgsAgUggFEgEKvEBn0Aj+AgfgglygsJosgxNGiNIgWJ4FBEoM4gA'); +const HRMI = dec('iMRwMAnken8fzfd7v+/3/v9/38/z+b5tiiM3/eP/+D/+AAIM/wEPwEDwEAAIIA=='); +const compassI = dec('iMRwMAgfgg/8g8ng0Q40ImcOjcHg+DwfB4Ph2Hw7FsolmkUxwEwuFwj/wEIMAA=='); + +////////////////////////////////////////////////////////////////////////////// +/* Squeezable strings */ + +class Formattable { + width(g) {return this.w != null ? this.w : (this.w = g.stringWidth(this.text));} + print(g, x, y) {g.drawString(this.text, x, y); return this.width();} +} + +class Fixed extends Formattable { + constructor(text) { + super(); + this.text = text; + } + squeeze() {return false;} +} + +class Squeezable extends Formattable { + constructor(named, index) { + super(); + this.named = named; + this.index = index; + this.end = index + named.forms; + } + squeeze() { + if (this.index >= this.end) return false; + this.index++; + this.w = null; + return true; + } + get text() {return this.named.table[this.index];} +} + +class Named { + constructor(forms, table) { + this.forms = forms; + this.table = table; + } + on(index) {return new Squeezable(this, this.forms * index);} +} + +////////////////////////////////////////////////////////////////////////////// +/* Face */ + +// Static geometry +const barW = 26, barH = g.getHeight(), barX = g.getWidth() - barW, barY = 0; +const faceW = g.getWidth() - barW, faceH = g.getHeight(); +const faceX = 0, faceY = 0, faceCX = faceW / 2, faceCY = faceH / 2; +const rectX = faceX + 35, rectY = faceY + 24, rectW = 80, rectH = 128; + +// Extended-Roman-numeral labels +const layout = E.toUint8Array( + 75, 23, // XII + 132, 24, // I + 132, 61, // II + 132, 97, // III + 132, 133, // IV + 132, 170, // V + 75, 171, // VI + 18, 170, // VII + 18, 133, // VIII + 18, 97, // IX + 18, 61, // X + 18, 24 // XI +); + +const numeral = (n, options) => [ + 'n', // 0 + 'abc', // I + 'abdc', // II + 'abddc', // III + 'abefg', // IV + 'hfg', // V + 'hfibc', // VI + 'hfibdc', // VII + 'hfibddc', // VIII + 'abjk', // IX + 'kjk', // X + 'kjbc', // XI + 'kjbdc', // XII + 'kjbddc', // XIII + 'kjbefg', // XIV + 'kjefg', // XV + 'labc', // XVI + 'labdc', // XVII + 'labddc', // XVIII + 'kjbjk', // XIX + 'kjjk', // XX + 'mabc', // XXI + 'mabdc', // XXII + 'mabddc', // XXIII +][options.o24h ? n % 24 : (n + 11) % 12 + 1]; + +const formatMonth = new Named(4, [ + 'January', 'Jan.', 'Jan', 'I', + 'February', 'Feb.', 'Feb', 'II', + 'March', 'Mar.', 'Mar', 'III', + 'April', 'Apr.', 'Apr', 'IV', + 'May', 'May', 'May', 'V', + 'June', 'June', 'Jun', 'VI', + 'July', 'July', 'Jul', 'VII', + 'August', 'Aug.', 'Aug', 'VIII', // VIII *is* narrower than Aug, our I is thin. + 'September', 'Sept.', 'Sep', 'IX', + 'October', 'Oct.', 'Oct', 'X', + 'November', 'Nov.', 'Nov', 'XI', + 'December', 'Dec.', 'Dec', 'XII' +]); +const formatDom = { + on: d => new Fixed(d.toString()) +}; +const formatDow = new Named(4, [ + 'Sunday', 'Sun.', 'Sun', 'Su', + 'Monday', 'Mon.', 'Mon', 'M', + 'Tuesday', 'Tues.', 'Tue', 'Tu', + 'Wednesday', 'Weds.', 'Wed', 'W', + 'Thursday', 'Thurs.', 'Thu', 'Th', + 'Friday', 'Fri.', 'Fri', 'F', + 'Saturday', 'Sat.', 'Sat', 'Sa' +]); + +const hceil = x => Math.ceil(x / 3600000) * 3600000; +const hfloor = x => Math.floor(x / 3600000) * 3600000; +const isString = x => typeof x == 'string'; +const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; +const imageHeight = i => isString(i) ? i.charCodeAt(1) : i.height; + +const events = { + // Items are {time: number, wall: boolean, priority: number, + // past: bool, future: bool, precision: number, + // colour: colour, dramatic?: bool, event?: any} + fixed: [{time: Number.POSITIVE_INFINITY}], // indexed by ms absolute + wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms + + clean: function(now, l) { + let o = now.getTimezoneOffset() * 60000; + let tf = now.getTime() + l, tw = tf - o; + // Discard stale events: + while (this.wall[0].time <= tw) this.wall.shift(); + while (this.fixed[0].time <= tf) this.fixed.shift(); + }, + + scan: function(now, from, to, f) { + result = Infinity; + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let c, p, i, l = from - o, h = to - o; + for (i = 0; (c = this.wall[i]).time < l; i++) ; + for (; (c = this.wall[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(result, f(c, new Date(c.time + o), p)); + } + l += o; h += o; t += o; + for (i = 0; (c = this.fixed[i]).time < l; i++) ; + for (; (c = this.fixed[i]).time < h; i++) { + if ((p = c.time < t) ? c.past : c.future) + result = Math.min(f(c, new Date(c.time), p)); + } + return result; + }, + + span: function(now, from, to, width) { + let o = now.getTimezoneOffset() * 60000; + let t = now.getTime() - o; + let lfence = [], rfence = []; + this.scan(now, from, to, (e, d, p) => { + if (p) { + for (let j = 0; j <= e.priority; j++) { + if (d < (lfence[e.priority] || t)) lfence[e.priority] = d; + } + } else { + for (let j = 0; j <= e.priority; j++) { + if (d > (rfence[e.priority] || t)) rfence[e.priority] = d; + } + } + }); + for (let j = 0; ; j += 0.5) { + if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) { + return [lfence[Math.floor(j)] || now, rfence[Math.ceil(j)] || now]; + } + } + }, + + insert: function(t, wall, e) { + let v = wall ? this.wall : this.fixed; + e.time = t = t - (wall ? t.getTimezoneOffset() * 60000 : 0); + v.splice(v.findIndex(x => x.time > t), 0, e); + }, + + loadFromSystem: function(options) { + alarms.forEach(x => { + if (x.on) { + const t = new Date(); + let h = x.hr; + let m = h % 1 * 60; + let s = m % 1 * 60; + let ms = s % 1 * 1000; + t.setHours(h - h % 1, m - m % 1, s - s % 1, ms); + // There's a race condition here, but I'm not sure what we can do about it. + if (t < Date.now() || x.last === t.getDate()) t.setDate(t.getDate() + 1); + this.insert(t, true, { + priority: 0, + past: false, // System alarms seem uninteresting if past? + future: true, + precision: x.timer ? 1000 : 60000, + colour: x.timer ? options.timerFg : options.alarmFg, + event: x + }); + } + }); + return this; + }, +}; + +////////////////////////////////////////////////////////////////////////////// +/* The main face logic */ + +class Sidebar { + constructor(g, x, y, w, h, options) { + this.g = g; + this.options = options; + this.x = x; + this.y = this.initY = y; + this.h = h; + this.rate = Infinity; + this.doLocked = Sidebar.status(_ => Bangle.isLocked(), lockI); + this.doHRM = Sidebar.status(_ => Bangle.isHRMOn(), HRMI); + this.doGPS = Sidebar.status(_ => Bangle.isGPSOn(), GPSI, Sidebar.gpsColour(options)); + } + reset(rate) {this.y = this.initY; this.rate = rate; return this;} + print(t) { + this.y += 4 + t.print( + this.g.setColor(this.options.barFg).setFontAlign(-1, 1, 1), + this.x + 3, this.y + 4 + ); + return this; + } + pad(n) {this.y += n; return this;} + free() {return this.h - this.y;} + static status(p, i, c) { + return function() { + if (p()) { + this.g.setColor(c ? c() : this.options.barFg) + .drawImage(i, this.x + 4, this.y += 4); + this.y += imageHeight(i); + } + return this; + }; + } + static gpsColour(o) { + const fix = Bangle.getGPSFix(); + return fix && fix.fix ? o.activeFg : o.barFg; + } + doPower() { + const c = Bangle.isCharging(); + const b = E.getBattery(); + if (c || b < 50) { + let g = this.g, x = this.x, y = this.y, options = this.options; + g.setColor(options.barFg).drawImage(batteryI, x + 4, y + 4); + g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0'); + const h = 13 * (100 - b) / 100; + g.fillRect(x + 8, y + 7 + h, x + 17, y + 20); + // Espruino disallows blank leading rows in icons, for some reason. + if (c) g.setColor(options.barBg).drawImage(chargeI, x + 4, y + 8); + this.y = y + imageHeight(batteryI) + 4; + } + return this; + } + doCompass() { + if (Bangle.isCompassOn()) { + const c = Bangle.getCompass(); + const a = c && this.rate <= 1000; + this.g.setColor(a ? this.options.activeFg : this.options.barFg).drawImage( + compassI, + this.x + 4 + imageWidth(compassI) / 2, + this.y + 4 + imageHeight(compassI) / 2, + a ? {rotate: c.heading / 180 * Math.PI} : undefined + ); + this.y += 4 + imageHeight(compassI); + } + return this; + } +} + +class Roman { + constructor(g, events) { + this.g = g; + this.state = null; + const options = this.options = new RomanOptions(); + this.events = events.loadFromSystem(this.options); + this.timescales = [1000, [1000, 60000], 60000, 3600000]; + this.sidebar = new Sidebar(g, barX, barY, barW, barH, options); + this.hours = Roman.hand(g, 3, 0.5, 12, _ => options.hourFg); + this.minutes = Roman.hand(g, 2, 0.9, 60, _ => options.minuteFg); + this.seconds = Roman.hand(g, 1, 0.9, 60, _ => options.secondFg); + } + + reset() {this.state = null;} + + doIcons(which) {this.state.iconsOk = null;} + + // Watch hands. These could be improved, graphically. + // If we restricted them to 60 positions, we could feasibly hand-draw them? + static hand(g, w, l, d, c) { + return p => { + g.setColor(c()); + p = ((12 * p / d) + 1) % 12; + let h = l * rectW / 2; + let v = l * rectH / 2; + let poly = + p <= 2 ? [faceCX + w, faceCY, faceCX - w, faceCY, + faceCX + h * (p - 1), faceCY - v, + faceCX + h * (p - 1) + 1, faceCY - v] + : p < 6 ? [faceCX + 1, faceCY + w, faceCX + 1, faceCY - w, + faceCX + h, faceCY + v / 2 * (p - 4), + faceCX + h, faceCY + v / 2 * (p - 4) + 1] + : p <= 8 ? [faceCX - w, faceCY + 1, faceCX + w, faceCY + 1, + faceCX - h * (p - 7), faceCY + v, + faceCX - h * (p - 7) - 1, faceCY + v] + : [faceCX, faceCY - w, faceCX, faceCY + w, + faceCX - h, faceCY - v / 2 * (p - 10), + faceCX - h, faceCY - v / 2 * (p - 10) - 1]; + g.fillPoly(poly); + }; + } + + static pos(p, r) { + let h = r * rectW / 2; + let v = r * rectH / 2; + p = (p + 1) % 12; + return p <= 2 ? [faceCX + h * (p - 1), faceCY - v] + : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)] + : p <= 8 ? [faceCX - h * (p - 7), faceCY + v] + : [faceCX - h, faceCY - v / 2 * (p - 10)]; + } + + alert(e, date, now, past) { + const g = this.g; + g.setColor(e.colour); + const dt = date - now; + if (e.precision < 60000 && dt >= 0 && e.future && dt <= 59000) { // Seconds away + const p = Roman.pos(date.getSeconds() / 5, 0.95); + g.drawLine(faceCX, faceCY, p[0], p[1]); + return 1000; + } else if (e.precision < 3600000 && dt >= 0 && e.future && dt <= 3540000) { // Minutes away + const p = Roman.pos(date.getMinutes() / 5 + date.getSeconds() / 300, 0.8); + g.drawLine(p[0] - 5, p[1], p[0] + 5, p[1]); + g.drawLine(p[0], p[1] - 5, p[0], p[1] + 5); + return dt < 119000 ? 1000 : 60000; // Turn on second hand two minutes up. + } else if (e.precision < 43200000 && dt >= 0 ? e.future : e.past) { // Hours away + const p = Roman.pos(date.getHours() + date.getMinutes() / 60, 0.6); + const poly = [p[0] - 4, p[1], p[0], p[1] - 4, p[0] + 4, p[1], p[0], p[1] + 4]; + if (date >= now) g.fillPoly(poly); + else g.drawPoly(poly, true); + return 3600000; + } + return Infinity; + } + + render(d, rate) { + const g = this.g; + const state = this.state || (g.clear(true), this.state = {}); + const options = this.options; + const events = this.events; + events.clean(d, -39600000); // 11h + + // Sidebar: icons and date + if (d.getDate() !== state.date || !state.iconsOk) { + const sidebar = this.sidebar; + state.date = d.getDate(); + state.iconsOk = true; + g.setColor(options.barBg).fillRect(barX, barY, barX + barW, barY + barH); + + sidebar.reset(rate).doLocked().doPower().doGPS().doHRM().doCompass(); + g.setFontCustom.apply(g, fontF); + let formatters = []; + let month, dom, dow; + if (options.calendric > 1) { + formatters.push(month = formatMonth.on(d.getMonth())); + } + if (options.calendric > 0) { + formatters.push(dom = formatDom.on(d.getDate())); + } + if (options.dow) { + formatters.push(dow = formatDow.on(d.getDay())); + } + // Obnoxiously inefficient iterative method :( + let ava = sidebar.free() - 3, use, i = 0, j = 0; + while ((use = formatters.reduce((l, f) => l + f.width(g) + 4, 0)) > ava && + j < formatters.length) + for (j = 0; + !formatters[i++ % formatters.length].squeeze() && + j < formatters.length; + j++) ; + if (dow) sidebar.print(dow); + sidebar.pad(ava - use); + if (month) sidebar.print(month); + if (dom) sidebar.print(dom); + } + + // Hour labels and (purely aesthetic) box; clear inner face. + let keyHour = d.getHours() < 12 ? 1 : 13; + let alertSpan = events.span(d, hceil(d) - 39600000, hfloor(d) + 39600000, 39600000); + let l = alertSpan[0].getHours(), h = alertSpan[1].getHours(); + if ((l - keyHour + 24) % 24 >= 12 || (h - keyHour + 24) % 24 >= 12) keyHour = l; + if (keyHour !== state.keyHour) { + state.keyHour = keyHour; + g.setColor(options.bg) + .fillRect(faceX, faceY, faceX + faceW, faceY + faceH) + .setFontCustom.apply(g, romanPartsF) + .setFontAlign(0, 1) + .setColor(options.fg); + // In order to deal with timezone changes more logic will be required, + // since the labels may be in unusual locations (even offset when + // a non-integral zone is involved). The value of keyHour can be + // anything in [hr-12, hr] mod 24. + for (let h = keyHour; h < keyHour + 12; h++) { + g.drawString( + numeral(h % 24, options), + faceX + layout[h % 12 * 2], + faceY + layout[h % 12 * 2 + 1] + ); + } + g.setColor(options.rectFg) + .drawRect(rectX, rectY, rectX + rectW - 1, rectY + rectH - 1); + } else { + g.setColor(options.bg) + .fillRect(rectX + 1, rectY + 1, rectX + rectW - 2, rectY + rectH - 2) + .setColor(options.fg); + } + + // Alerts + let requestedRate = events.scan( + d, hfloor(alertSpan[0] + 0), hceil(alertSpan[1] + 0) + 1, + (e, t, p) => this.alert(e, t, d, p) + ); + if (rate > requestedRate) rate = requestedRate; + + // Hands + // Here we are using incremental hands for hours and minutes. + // If we quantised, we could use hand-crafted bitmaps, though. + this.hours(d.getHours() + d.getMinutes() / 60); + if (rate < 3600000) { + this.minutes(d.getMinutes() + d.getSeconds() / 60); + } + if (rate < 60000) this.seconds(d.getSeconds()); + g.setColor(options.hubFg).fillCircle(faceCX, faceCY, 3); + return requestedRate; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Master clock */ + +class Clock { + constructor(face) { + this.face = face; + this.timescales = face.timescales; + this.options = face.options; + this.rates = {}; + + this.options.on('done', () => this.start()); + + this.listeners = { + charging: _ => {face.doIcons('charging'); this.active();}, + lock: _ => {face.doIcons('locked'); this.active();}, + faceUp: up => {this.conservative = !up; this.active();}, + twist: _ => this.options.autolight && Bangle.setLCDPower(true), + drag: e => { + if (this.t0) { + if (e.b) { + e.x < this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x); + e.y < this.yN && (this.yN = e.y) || e.y > this.yX && (this.yX = e.y); + } else if (this.xX - this.xN < 20) { + if (e.y - this.e0.y < -50) { + this.options.resolution > 0 && this.options.resolution--; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (e.y - this.e0.y > 50) { + this.options.resolution < this.timescales.length - 1 && + this.options.resolution++; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) { + this.stop(); + this.options.interact(); + } + this.t0 = null; + } + } else if (e.b) { + this.t0 = Date.now(); this.e0 = e; + this.xN = this.xX = e.x; this.yN = this.yX = e.y; + } + } + }; + } + + redraw(rate) { + const now = this.updated = new Date(); + if (this.refresh) this.face.reset(); + this.refresh = false; + rate = this.face.render(now, rate); + if (rate !== this.rates.face) { + this.rates.face = rate; + this.active(); + } + return this; + } + + inactive() { + this.timeout && clearTimeout(this.timeout); + this.exception && clearTimeout(this.exception); + this.interval && clearInterval(this.interval); + this.timeout = this.exception = this.interval = this.rate = null; + this.face.reset(); // Cancel any ongoing background rendering + return this; + } + + active() { + const prev = this.rate; + const now = Date.now(); + let rate = Infinity; + for (const k in this.rates) { + let r = this.rates[k]; + r === +r || (r = r[+this.conservative]) + r < rate && (rate = r); + } + const delay = rate - now % rate + 1; + this.refresh = true; + if (rate !== prev) { + this.inactive(); + this.redraw(rate); + if (rate < 31622400000) { // A year! + this.timeout = setTimeout( + () => { + this.inactive(); + this.interval = setInterval(() => this.redraw(rate), rate); + if (delay > 1000) this.redraw(rate); + this.rate = rate; + }, delay + ); + } + } else if (rate > 1000) { + if (!this.exception) this.exception = setTimeout(() => { + this.redraw(rate); + this.exception = null; + }, this.updated + 1000 - Date.now()); + } + return this; + } + + stop() { + this.inactive(); + for (const l in this.listeners) { + Bangle.removeListener(l, this.listeners[l]); + } + return this; + } + + start() { + this.inactive(); // Reset to known state. + this.conservative = false; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + for (const l in this.listeners) { + Bangle.on(l, this.listeners[l]); + } + Bangle.setUI('clock'); + return this; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Main */ + +const clock = new Clock(new Roman(g, events)).start(); diff --git a/apps/pooqroman/app.png b/apps/pooqroman/app.png new file mode 100644 index 000000000..bd27186e0 Binary files /dev/null and b/apps/pooqroman/app.png differ diff --git a/apps/pooqroman/resourcer.js b/apps/pooqroman/resourcer.js new file mode 100644 index 000000000..69365018e --- /dev/null +++ b/apps/pooqroman/resourcer.js @@ -0,0 +1,721 @@ +// pooqRoman resource maker +// +// Copyright (c) 2021 Stephen P Spackman +// +// 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. +// +// Notes: +// +////////////////////////////////////////////////////////////////////////////// +/* ==ASSETS== */ + +const heatshrink = require('heatshrink'); + +const enc = x => { + const d = btoa(require("heatshrink").compress(x)); + var r = "'" + d.substr(0, 64); + for (let i = 64; i < d.length; i += 64) r += "' +\n '" + d.substr(i, 64); + return r + "'"; +}; + +const prepBitmap = (name, data) => { + const image = Graphics.createImage(data); + const raw = String.fromCharCode(image.width, image.height, 0x81, 0) + image.buffer; + const x = ` +const ${name}I = dec(${enc(raw)}); +`; + return x; +}; + +const prepFont = (name, data) => { + const image = Graphics.createImage(data); + const lengths = Uint8Array(256); + const offsets = Uint16Array(256); + const adjustments = Uint16Array(256); + let min = Infinity, max = -Infinity; + const lines = data.split('\n'); + let m; + // This regexp is clearly suboptimal, but Espruino's regexp engine is really wonky + // and doesn't process nested parentheses or alternation correctly. + for (let i = 0; i < 5 && !(m = /^(<*)=([*\d]+)(=*)(>*)$/.exec(lines[i])); i++); + if (!m) throw new Error('Missing or incorrect header'); + const desc = m[1].length, body = 1 + m[2].length + m[3].length, asc = m[4].length; + const h = desc + body + asc; + let width = m[2] == '*' ? null : +m[2]; + let c = null, o = 0; + lines.forEach((line, l) => { + if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { + const h = m[2] == '='; + if (m[1].length > desc || h && m[1].length != desc) + throw new Error('Invalid descender height at ' + l); + if (m[2].length + m[3].length + m[4].length != body) + throw new Error('Invalid body height at ' + l); + if (m[5].length > asc || h && m[5].length != asc) + throw new Error('Invalid ascender height at ' + l); + if (c != null) { + lengths[c] = l - o; + if (width !== null && width !== lengths[c]) + throw new Error( + `Character has width ${lengths[c]} != ${width} at ${offsets[c]}` + ); + c = null + } + if (!h) { + c = m[3].charCodeAt(0); + if (c < min) min = c; + if (c > max) max = c; + o = l + 1; + offsets[c] = l; + adjustments[c] = m[1].length + } + } + }); + const xoffs = Uint8Array(lines.length); + const ypos = Uint16Array(lines.length); + ypos.fill(0xffff); + const w0 = lengths[min]; + let widths = ''; + for (c = min, o = 0; c <= max; c++) { + for (i = 0, j = offsets[c]; i < lengths[c]; i++) { + xoffs[j] = asc + body + adjustments[c] - 1; + ypos[j++] = o++; + } + widths += String.fromCharCode(lengths[c]); + } + const raster = Graphics.createArrayBuffer(h, o, 1, {msb: true}); + const writer = Graphics.createCallback( + image.width, image.height, 1, + (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col) + ); + writer.drawImage(image); + if (width === null) width = `dec(${enc(widths)})`; + const x = `const ${name}F = [ + dec( + ${enc(raster.buffer)} + ), ${min}, ${width}, ${h} +];`; + return x; +}; + +res = ` +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +`; + +res += prepFont('romanParts', ` +<=*============== +-a-------------- +x x +xx xx +-b-------------- +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +-c-------------- +xx xx +x x +-d-------------- +xx xx +xx xx +xx xx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxx +-e-------------- +xx xx +x xxxx +<-f-------------- + xxxxxxxx + xxxxxxxxxxx + xxxxxxx xx + xxxxxx x +xxxxx + xxxxxx x + xxxxxxx xx + xxxxxxxxxxx + xxxxxxxx +-g-------------- + xxxx + xx + x +-h-------------- + x + xx + xxxx +-i-------------- +x xxxx +xx xx +-j-------------- +xx xx +xxx xxx +xxxx xxxx +xxxxxx xxxxxx +xx xxxx xxxx xx +x xxxxxx x + xxxx +x xxxxxx x +xx xxxx xxxx xx +xxxxxx xxxxxx +xxxx xxxx +xxx xxx +xx xx +-k-------------- +x x +<-l-------------- + xx x + xxxxxx xx + xxxx xxxx xxx + xxxx xx xxxx x +xxx xx + xxxx xx xxxx x + xxxx xxxx xxx + xxxxxx xx + xx x +-m-------------- +x xx x +xx xxxx xx +xxx xxxxxx xxx +x xxxx xx xxxx x + xx xx +x xxxx xx xxxx x +xxx xxxxxx xxx +xx xxxx xx +x xx x +-n-------------- + xxxxxxxx + xxxxxxxxxxxx + xxxx xxxx +xxxx xxxx +xxx xxx +xx xxxx xx +xx xxxx xx +xxx xxx +xxxx xxxx + xxxx xxxx + xxxxxxxxxxxx + xxxxxxxx +<=*============== +`); + +res += prepFont('font', ` +<<<<=*======>>>> +- ------ + +-.------ +xx +xx +-0------>>>> + xxxxxxxx + xxxxxxxxxx +xxx xxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-1------>>>> +xx x +xx xx +xxxxxxxxxxxx +xxxxxxxxxxxx +xx +xx + +-2------>>>> +x x +xx xx +xxx xxx +xxxx xx +xxxxx xx +xx xxx xxx +xx xxxxxxx +xx xxxxx + +-3------>>>> + x xx + xx x xx +xxx xx xx +xx xxx xx +xx xxxxxx +xxx xxx xxx + xxxxxx xx + xxx x + +-4------>>>> + x + xx + xxxx + xxxxxxxxx +xxxxx xxxxx +xxxxx + xx + xx + +-5------>>>> + x xxxxxx + xx xxxxxx +xxx xx xx +xx xx xx +xx xx xx +xxx xxx xx + xxxxxx xx + xxxx + +-6------>>>> + xxxx + xxxxxxx +xxx xxxxx +xx xxxxx +xx xx xxx +xxx xxx xx + xxxxxx x + xxxx + +-7------>>>> + xx + xx +xxxx xx +xxxxxx xx + xxxx xx + xxxxxx + xxxx + x + +-8------>>>> + xxx xxx + xxxxxxxxxx +xxx xxxx xxx +xx xx xx +xx xx xx +xxx xxxx xxx + xxxxxxxxxx + xxx xxx + +-9------>>>> + xxxx +x xxxxxx +xx xxx xxx +xxx xx xx + xxxxx xx + xxxxx xxx + xxxxxxx + xxx + +-A------>>>> +xx +xxxxx + xxxxxxx + xxxxxxx + xx xxxx + xxxxxxx + xxxxxxx +xxxxx +xx + +-D------>>>> +xx xx +xxxxxxxxxxxx +xxxxxxxxxxxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-F------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + xx xx + xx xx + xx xx + xx +-I------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + +-J------>>>> + xx + xxx xx +xxx xx +xx xx +xxx xx + xxxxxxxxxxx + xxxxxxxxxx + xx +-M------>>>> +xxxxxxxxxxxx +xxxxxxxxxxx + xxx + xxxx + xxxx + xxx +xxxxxxxxxxx +xxxxxxxxxxxx + +-N------>>>> +xxxxxxxxxxxx +xxxxxxxxxxx + xxx + xxx + xxx + xxx + xxxxxxxxxxx +xxxxxxxxxxxx + +-O------>>>> + xxxxxxxx + xxxxxxxxxx +xxx xxx +xx xx +xx xx +xxx xxx + xxxxxxxxxx + xxxxxxxx + +-S------>>>> + x xxx + xx xxxxx +xxx xx xxx +xx xx xx +xx xx xx +xxx xx xxx + xxxxx xx + xxx x + +-T------>>>> + xx + xx + xx +xxxxxxxxxxxx +xxxxxxxxxxxx + xx + xx + xx +-V------>>>> + xxx + xxxxxx + xxxxx +xxxxx + xxxxx + xxxxxx + xxx + +-W------>>>> + xxxx + xxxxxxxx +xxxxxxxx + xxxx + xxxx + xxxx +xxxxxxxx + xxxxxxxx + xxxx +-X------>>>> +xx xx +xxx xxx + xxx xxx + xxxx + xxxx + xxx xxx +xxx xxx +xx xx + +-a------ + xxx +xxxxx x +xx xx xx +xx xx xx +xx xx xx + xxxxxx +xxxxxx + +-b------>>>> +xxxxxxxxxxxx + xxxxxxxxxxx +xx xx +xx xx +xxx xxx + xxxxxx + xxxx + +-c------ + xxxx + xxxxxx +xxx xxx +xx xx +xx xx + xx xx + x x + +-d------>>>> + xxxx + xxxxxx +xxx xxx +xx xx +xx xx + xxxxxxxxxxx +xxxxxxxxxxxx + +-e------ + xxxx + xxxxxx +xx xx xx +xx xx xx +xx xx xx + x xxxx + xxx + +<<<<-g------ + x xxxx + xx xxxxxx +xx xxx xxx +xx xx xx +xxx xx xxx + xxxxxxxxxx + xxxxxxxxx + +-h------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + xx + xx + xxx +xxxxxxx +xxxxxx + +-i------>>>> +xxxxxxxx xx +xxxxxxxx xx + +-l------>>>> +xxxxxxxxxxxx +xxxxxxxxxxxx + +-m------ +xxxxxxxx +xxxxxxx + xxx + xxx +xxxxxxx +xxxxxxx + xxx + xxx +xxxxxxx +xxxxxx + +-n------ +xxxxxxxx +xxxxxxx + xxx + xx + xxx +xxxxxxx +xxxxxx + +-o------ + xxxx + xxxxxx +xxx xxx +xx xx +xxx xxx + xxxxxx + xxxx + +<<<<-p------ +xxxxxxxxxxxx +xxxxxxxxxxx + xx xx + xx xx + xxx xxx + xxxxxx + xxxx + +-r------ +xxxxxxxx +xxxxxxx + xxx + xx + xx + xx + +-s------ + x xxx +xx xxxxx +xx xx xx +xx xx xx +xxxxx xx + xxx x + +-t------>>>> + xx + xxxxxxxxx + xxxxxxxxxx +xxx xx +xx xx +xx xx + xx + +-u------ + xxxxxx + xxxxxxx +xxx +xx +xxx + xxxxxxx +xxxxxxxx + +-v------ + xx + xxxx + xxxx +xxxx + xxxx + xxxx + xx + +<<<<-y------ + x xxxxxx + xx xxxxxxx +xx xxx +xx xx +xxx xxx + xxxxxxxxxxx + xxxxxxxxx + +<<<<=*======>>>> +`); + +res += prepBitmap('lock', ` + xxxxxx + xxxxxxxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxx xxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx +`); + +res += prepBitmap('battery', ` + xxxx + xxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx +`); + +res += prepBitmap('charge', ` + x + xx + xx + xxx + xxx + xxxxxxxxx + xxxxxxxxx + xxx + xxx + xx + xx + x +`); + +res += prepBitmap('GPS', ` + x + x x + x x + x x + x x xxxx + x xxxxx + xxxxxx + xxxxx + x xxx x + x x x x x + x x x x x + x x xx x x + x x x x + x xxx x + x + xxx +`); + +res += prepBitmap('HRM', ` + xxxx xxxx + xxxxxx xxxxxx + xx xxxx xxx xxx +xxx xxxxxxxx xxxx +xxx xxxxxxxx xxxx +xxx xxxxxxxx xxxx +xx xxxxxxx xxxx +xx xx xxxx xx x + xx x x x + xx xxxxxxxx xxx + xxxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxx + xxxxx + xxx + x +`); + +res += prepBitmap('compass', ` + xxxxx + xxxxxxxxx + xxx x xxx + xx x xx + xx x xx + xx xxx xx +xx xxx xx +xx xxx xx +xx xxx xx +xx xx xx xx +xx xx xx xx + xx x x xx + xx x x xx + xx xx + xxx xxx + xxxxxxxxx + xxxxx +`); + +print(res); diff --git a/apps/pooqround/ChangeLog b/apps/pooqround/ChangeLog new file mode 100644 index 000000000..12876f71a --- /dev/null +++ b/apps/pooqround/ChangeLog @@ -0,0 +1 @@ +0.00: Initial check-in. diff --git a/apps/pooqround/README.md b/apps/pooqround/README.md new file mode 100644 index 000000000..3c651ed67 --- /dev/null +++ b/apps/pooqround/README.md @@ -0,0 +1,39 @@ +# pooq Round: a maximally readable, naturally 24hr, analogue watch face + +This is a normal watch face for telling the time. +It is unusual in that it uses a pie chart for the hour hand. This is much easier and +more precise to read at a glance than a conventional hand, and as a bonus can distinguish +midnight (all black) from noon (all white). + +The day and date are optionally displayed, typographically smooshed into the corners. +Either you'll like that, or you won't. + +## Options + +Because sometimes I don't want to burn what I'm cooking and others I'm lazy and just want to know if it's afternoon yet, +you can alter the number of ‘hands’ on the display. When the watch is unlocked, slide up to add dots representing the minute and second, +or down to remove the distraction. There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky, +in case you want the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails. + +Although we generally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face. +We don't observe the system 12/24 setting, since it the design of the face is equally good in either interpretation. + +By default, there is a backlight that comes on when you twist your wrist. This, of course, somewhat increases power draw and could be +annoying in an intentionally dark environment, so there is an option to disable it. + +## Limitations + +Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets. +Sorry about that, but control of all the pixels was just too important to me. + +There's also no support for internationalisation at present. This irks me, but since every month and day name is hand-drawn, +there's no fix other than hard work. Talk to me about it if there's a language you'd like. + +## Feedback + +[I'd be happy to hear your feedback](https://www.github.com/stephenPspackman) if you have comments or find any bugs, or (most especially) +if you find this work interesting. + +## By + +Made by [Stephen P Spackman](https://www.github.com/stephenPspackman). diff --git a/apps/pooqround/app-icon.js b/apps/pooqround/app-icon.js new file mode 100644 index 000000000..f3db61936 --- /dev/null +++ b/apps/pooqround/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkB/4AW+ABCgAJEE4IXMh8ADAMPCoYXUK4gXMAAJHCN4oSG+I+CC4gEBC5gNBO4wuGC44IBGASPEC5ovHIox3JL4hdIR5xdIC54uIC5wWIC5hGKC5pGJC5QKCC6YKDCxIXIBQTCBC6IKDC6QKEC6IKFh52KC4gLHC5wLIC5oLJC5gLKC5YALC/4XfQZYAKh4X/C5B4V/4XYJChGBC7JIT/4wVh/wGCouBC6vwI4hIQagQWDDB5dBC45JNEwIXHGBhFDLwgYNCQQXKDBCgEC5QZFB4oGBA4IA=")) diff --git a/apps/pooqround/app.js b/apps/pooqround/app.js new file mode 100644 index 000000000..29fae6ee6 --- /dev/null +++ b/apps/pooqround/app.js @@ -0,0 +1,600 @@ +/* -*- mode: Javascript; c-basic-offset: 2; indent-tabs-mode: nil; coding: latin-1 -*- */ +// pooqRound + +// Copyright (c) 2021 Stephen P Spackman +// +// 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. +// +// Notes: +// +// This only works for Bangle 2. + +const isString = x => typeof x === 'string'; +const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; + +////////////////////////////////////////////////////////////////////////////// +/* System integration */ + +const storage = require('Storage'); + +////////////////////////////////////////////////////////////////////////////// +/* Face-specific options */ + +class Options { + // Protocol: subclasses must have static id and defaults fields. + // Only fields named in the defaults will be saved. + constructor() { + this.id = this.constructor.id; + this.file = `${this.id}.json`; + this.backing = storage.readJSON(this.file, true) || {}; + Object.setPrototypeOf(this.backing, this.constructor.defaults); + this.reactivator = _ => this.active(); + Object.keys(this.constructor.defaults).forEach(k => this.bless(k)); + } + + writeBack(delay) { + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout( + () => { + this.timeout = null; + storage.writeJSON(this.file, this.backing); + }, + delay + ); + } + + bless(k) { + Object.defineProperty(this, k, { + get: () => this.backing[k], + set: v => { + this.backing[k] = v; + // Ten second writeback delay, since the user will roll values up and down. + this.writeBack(10000); + } + }); + } + + showMenu(m) { + if (m instanceof Function) m = m(); + if (m) { + for (const k in m) if ('init' in m[k]) m[k].value = m[k].init(); + m[''].selected = -1; // Workaround for self-selection bug. + Bangle.on('drag', this.reactivator); + this.active(); + } else { + if (this.bored) clearTimeout(this.bored); + this.bored = null; + Bangle.removeListener('drag', this.reactivator); + this.emit('done'); + } + g.clear(true); + E.showMenu(m); + } + + active() { + if (this.bored) clearTimeout(this.bored); + this.bored = setTimeout(_ => this.showMenu(), 15000); + } + + reset() { + this.backing = {__proto__: this.constructor.defaults}; + this.writeBack(0); + } +} + +class RoundOptions extends Options { + constructor() { + super(); + this.menu = () => ({ + '': {title: '* face options *'}, + '< Back': _ => this.showMenu(), + Ticks: { + init: _ => this.resolution, + min: 0, max: 3, + onchange: x => this.resolution = x, + format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x] + }, + Calendar: { + init: _ => this.calendric, + min: 0, max: 5, + onchange: x => this.calendric = x, + format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x], + }, + 'Auto-Illum.': { + init: _ => this.autolight, + onchange: x => this.autolight = x + }, + Defaults: _ => {this.reset(); this.interact();} + }); + } + + interact() {this.showMenu(this.menu);} +} + +RoundOptions.id = 'pooqround'; + +RoundOptions.defaults = { + resolution: 1, + calendric: 5, + dayFg: '#fff', + nightFg: '#000', + autolight: true, +}; + +////////////////////////////////////////////////////////////////////////////// +/* Assets (generated by resourcer.js, in this directory) */ + +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +const y10F = [ + dec( + 'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' + + 'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' + + 'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' + + 'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' + + 'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' + + 'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' + + 'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' + + 'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' + + 'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' + + 'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA=' + ), 48, dec('hgAI'), 34 +];const y1F = [ + dec( + 'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' + + 'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' + + '+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' + + 'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' + + 'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' + + 'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' + + 'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' + + 'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' + + 'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' + + 'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' + + 'BCQQUAA=' + ), 48, dec('hgAI'), 48 +];const y10sF = [ + dec( + 'j/+gP//0PgE8mEAmHwgfBBQINB8AWDgcAoEGAYMMj///H///wBwNgAQPAAQMgg8B' + + 'wE+hkA9kwg8Y+F4mP/4Fg/AVD4EBgcCg0MnEMmfgmH94PD4f+hkHIIgbBg44B/ng' + + 'h/H/H8n4IBg4QBhwUC//Bgf+FYMwAIPAjHDwPjg//gEPLgUAOYMAn/+DAM8j1gmH' + + 'h8fDBAMIHIRwDQAJtBg/8mH+gHPwEDCII/DAAM+n8B/v+h0+jkwuEw8fhV4UD8Yr' + + 'DjxDB/0Ch88CoLEB+fPwK0BKIOACoQA=' + ), 48, dec('hAAI'), 22 +];const y1sF = [ + dec( + 'j///0A/4ABgfAgEPgwNBg0MAYMMjwDBvAWB//gh4DBEAUDgEgAYQeBgcDEwQSCCY' + + 'oDCiACBwFgGoOBwEAnODBwPhw/Ag+Bw/gv0Bwf/+EBwAkBgPgCYOA4EQgIeB8ASB' + + 'g/AgcGnuAg0N8fAnkfIwPwnEB/40BgE8IYX8AYN/7hDB/kcg4xBv4TBC4kcLgUcv' + + '4ZBIgJIBHoNgHoJ8BgOGKQMHhijBnkYHoQlEv4DBRYWAv+eOgPwmEDg4mBXIXwni' + + 'SBDwRICSwIABWIM/HoM//57BEoMGv7dC/DrCLoU4eYfAv4kB8f/wPB98HLgP4TQM' + + 'B+EGh0PvE8QwN/+EP8E/LAK6CBIMAwPg+EDDwNgh8GJQP8h8Hz/gN4P+gBMBJIMA' + ), 48, dec('hEHhAAGA'), 31 +];const d10F = [ + dec( + 'AAXgjEAjkHgEDwPAgFwvEAh0f///44CB/ICB/4aDAQMcAQMDwAhBuAhBj0B4EH4E' + + 'wgP4h0Av4JBj3gnEHzkHgPjwF4/Fwh/+CQP/HwMD4E4gJLCvAuBj0ADgOGg+B8fA' + + 'uF5FoMeDQPH/l4vP8g/+vg4BzkAg/gA=' + ), 49, dec('hcMhYA=='), 27 +];const d1F = [ + dec( + 'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' + + 'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' + + 'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' + + 'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' + + 'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' + + '4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' + + 'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' + + '4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' + + 'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' + + 'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' + + 'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' + + 'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' + + 'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' + + 'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' + + 'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA==' + ), 48, dec('ikPigAGA'), 48 +];const dowF = [ + dec( + 'gf8AYNwgEP/4FBvEAj//wEAnkAn0H4EAjwNBgPgAoQZBAoMOgHwAongCIQFDDoIF' + + 'FDoPggYFBF4IFBGoI7B+AFCE4NwCIIlCuAdBIYU4gPwn5VBjEA//+M4d//AFDh4W' + + 'BB4IgBAAX/B4n/PoQACJQIcEAokHAqAXFEYhLF/6tCApIADn4ED/zFBAAX8gaGBA' + + 'AZZFQIR2GdQQYRBYgXFEYoWRKQQWCLoRrEHgoAIg7LEj7LEn4bEvk+AodwhwFD+C' + + '5E8DFEAqIdFFIo1FIIpNFLIoEEAtShCVwQEDVwIFDKAJBvAAv/Bgn/RIjzGjwFEW' + + 'YicBAqAXFEYh6CRIgFKTYzjEAwt/AxxvDHAkf//AAgMDPIgVBGAnwAoYRBIYk/S4' + + 'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' + + 'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' + + 'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' + + 'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' + + '//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' + + 'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' + + 'p/zC4n5EYj1BAoc//4RDU4IFDA==' + ), 48, dec('kElkMljsljw='), 48 +];const mF = [ + dec( + '/AEDvEH4AFCgPAnwMDh0B+AGD8EPwAFCg8AvgMDuED8AMEj4MDDwI0DhwOB/4ACC' + + '4M/AoX8HgIMDCoI0EAAI0EgA0DnACBGgXHL4Q0Bjn+IYXAgfOCwRpBnPHEQmcuAG' + + 'DBg3csAGDj4mCAAX/QwhkBWSEDDIp3BAoZ3BBgkeDIp9FOYQMJDIomGh5NFv/wVo' + + 'YABYIgZBYIYABgKWBHAcPHAKsCgF4VoJDD4AVCIYbtBfAnwgYDBg+Ag6bBEQM8EQ' + + 'KoCDwMDwP9EQI0Bnk9540DZ4Y/CZ4Y0BbggwBDIY0BgP8JIbcB7yBE/pjDEAOQbZ' + + '8fRwT7DAAL7E/4zEjh9EKwLCEnB9BBhIZFgPzEwkP/jcFe4iYBdYLcEAwr5CBgYj' + + 'Hh65BAxU/AwjNCIhEH/BkEGYqTCRwYMFACE4AonHZ4kcIQkB5yOEnPHIYmcuAMK7' + + 'lgNJJQBJojkBKSB3BDIk/DIkBBgseDIpmEOYwMGDIsAOYkAgxBGGYjzBIwoMDXYI' + + 'tCaAQFCCwP8jiECCwMBBhAZGEwwzHIAxNGTY5UKTYIMEjkORwomEnEHBhQZFgPzT' + + 'gkP/hBEv+ACYivFe3adBAAfwAwoNFGYJkGh/+Axc/AwkfAoggFg/4ZgwzDj4GDiD' + + '7CAAPxRQswNIp1FBgnH4TPE/0gC4fO8wMDnPHsAMDzl2BhXcsxpFBgZQB+xqE/4z' + + 'DAAMCLRJ3BwaWFBgvjDAkfuAGEu4MFfoYZBW4v/eIn/8CzEvEHBocB4E+BgcOgIn' + + 'DgHgh+ANAcAvgMDuED8AMEj4MDDwI0DhyECAAQXBn4FCf4MBBgYVBGggABGghrBD' + + 'gQqCGgJ0BL4QJBTYJDCBIMBJYRpCJoIAEUIoMGPIgmDVAYMFKgQAODJh3BBgkeDI' + + 'p9FnAMLDIomGh5NFv/we372/exgZDe0BpCDIbBBDIl/EwonBAogMEHIIZDD4KUBH' + + 'wYFDCAPBOwQWCjgMHDI4mGGYwcC+JNFiDAFOIswEAmDDAn8kAME8QYEjwMDAAN2Y' + + 'QtgTonmYQoMDEwP2YQoZEgECJoozEv5NEj/+LQaYB8YMDn0fM4mAu4MDnEHuAMD8' + + 'KVEIAPgEwn+WAuAK4LABj7PDwEAvhJBCwUB8EP8EffQMOgH4C4ITB+EHAYN4RwMA' + + 'ng/BE4PwDYITCnw2BF4YKBF4LwDgInBKYLoFFQIAJgZCBAAZdCTYjOE/p6DgE954' + + 'fEziUDgE544ME7gtEj/OExUP7hAEnJTKAAxuBFoa4BOokfBgkB4AzEniZBewhaEB' + + 'goZGj61BRxMHWQIADjwJCIgLICJQQABDIL9BAAKoBg4iCgYTBKoZABhwnDJoJCDg' + + '4OCAAQXBewIABJoI5DHQSLBAAP8B4I6CcQgANgbVEOg0fEAkB8KOEnBNBVBIMMjh' + + 'yEWo0MhhSPgJoBwCZDNwp2BJor2LJpjAFAAImEJwI2BAAfwj4GEXYgMBAwKlFv4G' + + 'GFQpYFXQx0BAwx6DLQIGCIIgeCIAkHBgoAPn4FEh/8HQpPEn0fVCPhO4kfZ4hvGg' + + 'YSEgRGFngFEgf4AwkfSws/EwgtBBhQZFEw0cOwIHEuF4AocHWIL2LBgsHGoaBBn7' + + 'SD+DZEnzIFI4MPAoS1CAwbVRTYqoGWosB/p7EnvPD4mcbgk544ME7jcF5wmKh/cI' + + 'Ak5LUvhGYk4VAIfDwBaEBgsB4AZEjkOGYnA4AA==' + ), 49, dec('k0jk0kksmj0lk8lAwIA='), 52 +]; +const lockI = dec('hURwMAj0P485w1h3/4g15wFgjPmgOAs+Yg0B//AA'); +const lockSI = dec('hMNwMAjkfjHMt/8g1zgOc4FnmEf/AA=='); +const batteryI = dec('hERwMAjH/ABw'); +const chargeI = dec('g8NwMAgkYsHDh0fw8MmFhwUA'); +const HRMI = dec('iERwMAjk4l10t/29/3AIfn+ek6VTlPX9d3/U3/Ef/EP+EH8ED4EBwAA='); +const compassI = dec('hMJwMAhEEg8Dwfh2Pc43BwA='); +const y100I = dec('h8RwMAvk5/n6nOwm9w9lnzH+mO4sc4405xk7jE2mEssEd4EbgE+gE4A='); +const y100sI = dec('hcKwMAsOWvHZ+c2s1s4uYmcD4EwA'); + +////////////////////////////////////////////////////////////////////////////// +/* Status */ + +const status = (p, i) => function (g, x, y, rl) { // Nested arrows are currently broken! + if (!p()) return x; + if (rl) x -= imageWidth(i); + g.setColor(g.theme.fg).drawImage(i, x, y); + return rl ? x - 1 : x + imageWidth(i) + 1; +}; + +const doLocked = status(_ => Bangle.isLocked(), lockI); +const doPower = (g, x, y, rl) => { + const c = Bangle.isCharging(); + const b = E.getBattery(); + if (!c && b > 50) return x; + if (rl) x -= imageWidth(batteryI); + g.setColor(g.theme.fg).drawImage(batteryI, x, y); + g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0'); + let h = 13 * (100 - b) / 100; + g.fillRect(x + 1, y + 2 + h, x + 6, y + 15); + if (c) g.setColor(g.theme.bg).drawImage(chargeI, x, y + 2); + return rl ? x - 1 : x + imageWidth(batteryI) + 1; +}; + +const doHRM = status(_ => Bangle.isHRMOn(), HRMI); // Might show Bangle.getHRM().bpm if confident? + +////////////////////////////////////////////////////////////////////////////// +/* Watch face */ + +class Round { + constructor(g) { + this.g = g; + this.b = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb: true}); + this.bI = { + width: this.b.getWidth(), height: this.b.getHeight(), bpp: this.b.getBPP(), + buffer: this.b.buffer, transparent: 0 + }; + this.c = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb: true}); + this.cI = { + width: this.c.getWidth(), height: this.c.getHeight(), bpp: this.c.getBPP(), + buffer: this.c.buffer, transparent: 0 + }; + this.options = new RoundOptions(); + this.timescales = [1000, 0, 60000, 900000]; + this.state = {}; + // Precomputed polygons for the border areas. + this.tl = [0, 0, 58, 0, 0, 58]; + this.tr = [176, 0, 176, 58, 119, 0]; + this.bl = [0, 176, 0, 119, 58, 176]; + this.br = [176, 176, 119, 176, 176, 119]; + this.xc = g.getWidth() / 2; + this.yc = g.getHeight() / 2; + this.minR = 5; + this.secR = 3; + this.r = this.xc - this.minR; + } + + reset() {this.state = {}; this.g.clear(true);} + + doIcons(which) { + this.state[which] = null; + this.render(new Date()); // Not quite right, I think. + } + + pie(f, a0, a1, invert) { + if (!invert) return this.pie(f, a1, a0 + 1, true); + let t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI); + let i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5); + let x = f.getWidth() / 2, y = f.getHeight() / 2; + let poly = [ + x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1), + y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1), + x, + y, + x + (i0 & 2 ? -x : x) * (i0 & 1 ? 1 : t0), + y + (i0 & 2 ? y : -y) / (i0 & 1 ? t0 : 1), + ]; + if (i1 - i0 > 4) i1 = i0 + 4; + for (i0++; i0 <= i1; i0++) poly.push( + 3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0 + ); + f.setColor(0).fillPoly(poly); + } + + hand(t, d, c0, r0, c1, r1) { + t *= Math.PI / 30; + const r = this.r; + const z = 2 * r0 + 1; + const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t); + const x0 = x - r0, y0 = y - r0; + d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true}); + for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) { + d.setPixel(i, j, g.getPixel(x0 + i, y0 + j)); + } + g.setColor(c0).fillCircle(x, y, r0); + if (c1 !== undefined) g.setColor(c1).fillCircle(x, y, r1); + return [d, x0, y0]; + } + + render(d) { + const g = this.g; + const b = this.b, bI = this.bI; + const c = this.c, cI = this.cI; + const state = this.state; + const options = this.options; + const cal = options.calendric; + const res = options.resolution; + const dow = (cal == 1 || cal > 2) && d.getDay(); + const ts = res < 2 && d.getSeconds(); + const tm = res < 3 && d.getMinutes() + ts / 60; + const th = d.getHours() + d.getMinutes() / 60; + const dd = cal > 1 && d.getDate(); + const dm = cal > 3 && d.getMonth(); + const dy = cal > 4 && d.getFullYear(); + const xc = this.xc, yc = this.yc, r = this.r; + const dlr = xc * 3/4, dlw = 8, dlhw = 4; + + // Restore saveunders for fast-moving, overdrawing indicators. + if (state.sd) g.drawImage.apply(g, state.sd); + if (state.md) g.drawImage.apply(g, state.md); + + if (dow !== state.dow) { + g.setColor(g.theme.bg).fillPoly(this.tl); + if (dow === +dow) { + g.setColor(g.theme.fg).setFontCustom.apply(g, dowF).drawString(dow, 5, 5); + } + state.dow = dow; + } + + const locked = Bangle.isLocked(); + const charging = Bangle.isCharging(); + const battery = E.getBattery(); + const HRMOn = Bangle.isHRMOn(); + if (dy !== state.dy || + locked !== state.locked || + charging !== state.charging || + Math.abs(battery - state.battery) > 2 || + HRMOn !== state.HRMOn + ) { + g.setColor(g.theme.bg).fillPoly(this.tr); + const u = dy % 10; + if (charging || battery < 50 || HRMOn || locked && dy !== +dy) { + let x = 172, y = 5; + x = doLocked(g, x, y, true); + x = doPower(g, x, y, true); + x = doHRM(g, x, y, true); + if (dy === +dy) { + g.setColor(g.theme.fg).drawImage(y100sI, 145, 23); + g.setFontCustom.apply(g, y10sF).drawString((dy - u) / 10 % 10, 157, 23); + g.setFontCustom.apply(g, y1sF).drawString(u, 165, 23); + } + } else if (dy === +dy) { + g.setColor(g.theme.fg); + if (locked) g.drawImage(lockSI, 136, 5); + else g.drawImage(y100I, 130, 5); + g.setFontCustom.apply(g, y10F).drawString((dy - u) / 10 % 10, 146, 5); + g.setFontCustom.apply(g, y1F).drawString(u, 160, 5); + } + state.dy = dy; + state.locked = Bangle.isLocked(); + state.charging = Bangle.isCharging(); + state.battery = E.getBattery() - E.getBattery() % 2; + state.HRMOn = Bangle.isHRMOn(); + } + if (dm !== state.dm) { + g.setColor(g.theme.bg).fillPoly(this.bl); + if (dm === +dm) { + g.setColor(g.theme.fg).setFontCustom.apply(g, mF); + g.drawString(String.fromCharCode(49 + dm), 5, 124); + } + state.dm = dm; + } + if (dd !== state.dd) { + g.setColor(g.theme.bg).fillPoly(this.br); + if (dd === +dd) { + let u = dd % 10; + g.setColor(g.theme.fg).setFontAlign(1, 1); + g.setFontCustom.apply(g, d10F).drawString((dd - u) / 10, 152, 172); + g.setFontAlign(-1, 1); + g.setFontCustom.apply(g, d1F).drawString(u, 152, 172); + g.setFontAlign(-1, -1); + } + } + if (th !== state.th) { + state.th = th; + b.clear(true).fillCircle(88, 88, r - 1); + g.setColor(options.nightFg).drawImage(bI); + if (th < 12) this.pie(b, th / 12, 1, true); + else this.pie(b, 1, th / 12, true); + g.setColor(options.dayFg).drawImage(bI); + } + state.md = tm === +tm ? + this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) : + null; + state.sd = ts === +ts ? + this.hand(ts, state.sd, g.theme.fg2, this.secR) : + null; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Master clock */ + +class Clock { + constructor(face) { + this.face = face; + this.timescales = face.timescales; + this.options = face.options; + this.rates = {}; + this.faceUp = null; + + this.options.on('done', () => this.start()); + + this.listeners = { + lcdPower: on => on ? this.active() : this.inactive(), + charging: () => {face.doIcons('charging'); this.active();}, + lock: () => {face.doIcons('locked'); this.active();}, + faceUp: up => { + this.conservative = !up; + this.faceUp = up; + this.active(); + }, + twist: _ => this.options.autolight && Bangle.setLCDPower(true), + drag: e => { + if (this.t0) { + if (e.b) { + e.x < this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x); + e.y < this.yN && (this.yN = e.y) || e.y > this.yX && (this.yX = e.y); + } else if (this.xX - this.xN < 20) { + if (e.y - this.e0.y < -50) { + this.options.resolution > 0 && this.options.resolution--; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (e.y - this.e0.y > 50) { + this.options.resolution < this.timescales.length - 1 && + this.options.resolution++; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) { + this.stop(); + this.options.interact(); + } + this.t0 = null; + } + } else if (e.b) { + this.t0 = Date.now(); this.e0 = e; + this.xN = this.xX = e.x; this.yN = this.yX = e.y; + } + } + }; + } + + redraw(rate) { + const now = this.updated = new Date(); + if (this.refresh) this.face.reset(); + this.refresh = false; + rate = this.face.render(now, rate); + if (rate !== this.rates.face) { + this.rates.face = rate; + this.active(); + } + return this; + } + + inactive() { + this.timeout && clearTimeout(this.timeout); + this.exception && clearTimeout(this.exception); + this.interval && clearInterval(this.interval); + this.timeout = this.exception = this.interval = this.rate = null; + this.face.reset(); // Cancel any ongoing background rendering + return this; + } + + active() { + const prev = this.rate; + const now = Date.now(); + let rate = Infinity; + for (const k in this.rates) { + let r = this.rates[k]; + r === +r || (r = r[+this.conservative]) + r < rate && (rate = r); + } + const delay = rate - now % rate + 1; + this.refresh = true; + + if (rate !== prev) { + this.inactive(); + this.redraw(rate); + if (rate < 31622400000) { // A year! + this.timeout = setTimeout( + () => { + this.inactive(); + this.interval = setInterval(() => this.redraw(rate), rate); + if (delay > 1000) this.redraw(rate); + this.rate = rate; + }, delay + ); + } + } else if (rate > 1000) { + if (!this.exception) this.exception = setTimeout(() => { + this.redraw(rate); + this.exception = null; + }, this.updated + 1000 - Date.now()); + } + return this; + } + + stop() { + this.inactive(); + for (const l in this.listeners) { + Bangle.removeListener(l, this.listeners[l]); + } + return this; + } + + start() { + this.inactive(); // Reset to known state. + this.conservative = false; + this.rates.clock = this.timescales[this.options.resolution]; + this.active(); + for (const l in this.listeners) { + Bangle.on(l, this.listeners[l]); + } + Bangle.setUI('clock'); + return this; + } +} + +////////////////////////////////////////////////////////////////////////////// +/* Main */ + +const clock = new Clock(new Round(g)).start(); diff --git a/apps/pooqround/app.png b/apps/pooqround/app.png new file mode 100644 index 000000000..b7ea36fb8 Binary files /dev/null and b/apps/pooqround/app.png differ diff --git a/apps/pooqround/resourcer.js b/apps/pooqround/resourcer.js new file mode 100644 index 000000000..44186e658 --- /dev/null +++ b/apps/pooqround/resourcer.js @@ -0,0 +1,1671 @@ +// pooqRoman resource maker +// +// Copyright (c) 2021 Stephen P Spackman +// +// 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. +// +// Notes: +// +////////////////////////////////////////////////////////////////////////////// +/* ==ASSETS== */ + +const heatshrink = require('heatshrink'); + +const enc = x => { + const d = btoa(require("heatshrink").compress(x)); + var r = "'" + d.substr(0, 64); + for (let i = 64; i < d.length; i += 64) r += "' +\n '" + d.substr(i, 64); + return r + "'"; +}; + +const prepBitmap = (name, data) => { + const image = Graphics.createImage(data); + const raw = String.fromCharCode(image.width, image.height, 0x81, 0) + image.buffer; + const x = ` +const ${name}I = dec(${enc(raw)}); +`; + return x; +}; + +const prepFont = (name, data) => { + const image = Graphics.createImage(data); + const lengths = Uint8Array(256); + const offsets = Uint16Array(256); + const adjustments = Uint16Array(256); + let min = Infinity, max = -Infinity; + const lines = data.split('\n'); + let m; + // This regexp is clearly suboptimal, but Espruino's regexp engine is really wonky + // and doesn't process nested parentheses or alternation correctly. + for (let i = 0; i < 5 && !(m = /^(<*)=([*\d]+)(=*)(>*)$/.exec(lines[i])); i++); + if (!m) throw new Error('Missing or incorrect header'); + const desc = m[1].length, body = 1 + m[2].length + m[3].length, asc = m[4].length; + const h = desc + body + asc; + let width = m[2] == '*' ? null : +m[2]; + let c = null, o = 0; + lines.forEach((line, l) => { + if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { + const h = m[2] == '='; + if (m[1].length > desc || h && m[1].length != desc) + throw new Error('Invalid descender height at ' + l); + if (m[2].length + m[3].length + m[4].length != body) + throw new Error('Invalid body height at ' + l); + if (m[5].length > asc || h && m[5].length != asc) + throw new Error('Invalid ascender height at ' + l); + if (c != null) { + lengths[c] = l - o; + if (width !== null && width !== lengths[c]) + throw new Error( + `Character has width ${lengths[c]} != ${width} at ${offsets[c]}` + ); + c = null + } + if (!h) { + c = m[3].charCodeAt(0); + if (c < min) min = c; + if (c > max) max = c; + o = l + 1; + offsets[c] = l; + adjustments[c] = m[1].length + } + } + }); + const xoffs = Uint8Array(lines.length); + const ypos = Uint16Array(lines.length); + ypos.fill(0xffff); + const w0 = lengths[min]; + let widths = ''; + for (c = min, o = 0; c <= max; c++) { + for (i = 0, j = offsets[c]; i < lengths[c]; i++) { + xoffs[j] = asc + body + adjustments[c] - 1; + ypos[j++] = o++; + } + widths += String.fromCharCode(lengths[c]); + } + const raster = Graphics.createArrayBuffer(h, o, 1, {msb: true}); + const writer = Graphics.createCallback( + image.width, image.height, 1, + (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col) + ); + writer.drawImage(image); + if (width === null) width = `dec(${enc(widths)})`; + const x = `const ${name}F = [ + dec( + ${enc(raster.buffer)} + ), ${min}, ${width}, ${h} +];`; + return x; +}; + +res = ` +const heatshrink = require('heatshrink'); +const dec = x => E.toString(heatshrink.decompress(atob(x))); +`; + +res += prepFont('y10', ` +=*================================ +-0-------------------------------- + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxxxx + xxxxx xxxxx + xxxx xxxx + xxxx xxxx + xxxxx xxxxx + xxxxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx +-1-------------------------------- + xxx + xxx + xxx + xxx x + xxx x + xxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx + xxx + xxx +xxx +-2-------------------------------- + x xx + xx xxx + xxxx xxx + xxxxx xxx + xxxxxxx xxx + xxxx xxx xxx + xxxx xxxx xxx + xxxx xxxx xxx + xxxx xxxxxxxx xxxxxxx + xxxx xxxxxxxxxxxxxxxxxxx + xxxx xxxxxxxxxxxxxx +xxxx xxxxxxxxxx +-3-------------------------------- + xxx x xxx + xxx xx xxx + xxxx xxx xxx + xxxx xxxx xxx + xxxx xxxxx xxx + xxxx xxxxxx xxx + xxx xxxx xxxxxx + xxxx xxxxx xxxxx + xxxxxxx xxxxxxx xxxx + xxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxx xx + xxxxxxxxxxx x +-4-------------------------------- + xxxx + xxxxxx + xxxxxxxx + xxxx xxxx + xxxx xxxxxx + xxxx xxxxxxxx + xxxxxxxxxxxx xxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxx + xxxxxxxxxxxx xxxxxxx + xxxx xxx + xxxx + xxxx +-5-------------------------------- + xxx xxxxxxxxxxxxx + xxx xxxxxxxxxxxx + xxxx xxxxxxxxxxxx + xxxx xxx xxx + xxxx xxx xxx + xxxx xxx xxx + xxx xxxx xxx + xxxx xxxxx xxx + xxxxxx xxxxxx xxx + xxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxx xxx +-6-------------------------------- + xxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxxxxx xxxxxxxxxxxxxx + xxxx xxxx xxxxx + xxxx xxxx xxxx + xxx xxx xxx + xxxx xxxx xxxx + xxxxxx xxxxxx xxx + xxxxxxxxxxxxxxxx xxxx + xxxxxxxxxxxxxx xxx + xxxxxxxxxx xx +-7-------------------------------- + xxx + xxx + xxxxxxx xxx + xxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxx xxx + xxxxxxxxxxxxx + xxxxxxxxx + xxxxxx + xxxx + xx + x +-8-------------------------------- + xxxxxxx + xxxxxxxxxxxx xxxxxx + xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxx xxxxxxxxxxxxxxx + xxxx xxxxx xxxx + xxxx xxx xxx + xxx xxx xxx + xxxx xxxxx xxxx + xxxxxx xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx xxxxxx + xxxxxxxxxx +-9-------------------------------- + xxxxxxxx + xxxxxxxxxxxx + x xxxxxxxxxxxxxx + xx xxxxx xxxxxx + xxx xxxx xxxx + xxx xxx xxx + xxxx xxxx xxx + xxxxx xxxx xxxx + xxxxxxx xxxxx xxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx +=*================================ +`); + +res += prepFont('y1', ` +=*============================================== +-0---------------------------------------------- + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxxxx + xxxx xxxx + xxx xxx + xxx xxx + xxxx xxxx + xxxxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +-1---------------------------------------------- + xxx + xxx + xxx + xxx x + xxx x + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx + xxx + xxx +xxx +-2---------------------------------------------- + x xx + xx xxx + xxxx xxx + xxxxx xxx + xxxxxxx xxx + xxxx xxxx xxx + xxxx xxxxx xxx + xxxx xxxxxxx xxxx + xxxx xxxxxxxxxxxxx xxxxxxxxxxx + xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxx xxxxxxxxxxxxxxxxxxxxxxxxx +xxxx xxxxxxxxxxxxxx +-3---------------------------------------------- + xxx x xxx + xxx xx xxx + xxxx xxxx xxx + xxxx xxxxx xxx + xxxx xxxxxxx xxx + xxxx xxxxxxxx xxx + xxx xxxx xxxxx xxx + xxxxx xxxxxxx xxxxxxxx + xxxxxxxxxx xxxxxxxxxx xxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxx xx +-4---------------------------------------------- + xxxx + xxxxxxx + xxxxxxxxxxx + xxxx xxxxxxxx + xxxx xxxxxxxxx + xxxx xxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxx + xxxx xxxxxx + xxxx + xxxx +-5---------------------------------------------- + xxx xxxxxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxxxx + xxxx xxxxxxxxxxxxxxxxxxx + xxxx xxx xxx + xxxx xxx xxx + xxxx xxx xxx + xxx xxxx xxx + xxxx xxxxxx xxx + xxxxxxxxx xxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxx xxx +-6---------------------------------------------- + xxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxx xxxxxxxxxxxxxxxxxxxxx + xxxxxx xxxx xxxxxxxx + xxxx xxxx xxxxx + xxx xxx xxxx + xxxx xxxx xxx + xxxxxxxx xxxxxxxx xxxx + xxxxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxx xx +-7---------------------------------------------- + xxx + xxx + xxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxx + xxxx + xx + x +-8---------------------------------------------- + xxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxx xxxxxxxxxxxxxxxxxxxxxxx + xxxx xxxxxx xxxx + xxxx xxx xxx + xxx xxx xxx + xxxx xxxxxx xxxx + xxxxxx xxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxxxx +-9---------------------------------------------- + xxxxxxxxxx + x xxxxxxxxxxxxxxxx + xx xxxxxxxxxxxxxxxxxxx + xxxx xxxxxxx xxxxxxxx + xxxx xxxxx xxxxx + xxxxx xxx xxx + xxxxxx xxxx xxx + xxxxxxx xxxx xxxxx + xxxxxxxxxxx xxxxxx xxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxx +=*============================================== +`); + +res += prepFont('y10s', ` +=*==================== +-0-------------------- + xxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxx xxxx + xx xx + xxxx xxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxx + +-1-------------------- + xx + xx x + xx xx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xx +xx + +-2-------------------- + xxx x + xxxxx xx + xx xxx xx + xx xxx xx + xx xxxx xxxx + xx xxxxxxxxxxx +xx xxxxx + +-3-------------------- + x xx + xx x xx + xx xxx xx + xx xxxxx xx + xxx xxx xxxxxx + xxxxxxxxx xxxx + xxxxxx xx + +-4-------------------- + xxxxx + xxxxxxx + xxx xxxxxx + xxxxxxxx xxxxxx + xxxxxxxxxx xxxxxx + xxx + xxx + +-5-------------------- + x xxxxxxxxxx + xx xxxxxxxxx + xx xx xx + xx xx xx + xxxx xxx xx + xxxxxxxxxxx xx + xxxxxxxx + +-6-------------------- + xxxxxxxxx + xxxxxxxxxxxxx + xxxx xxxxxxxxx + xx xx xxxx + xxxx xxxx xxx + xxxxxxxxx xx + xxxxxxx x + +-7-------------------- + xx + xxxxx xx + xxxxxxxxx xx + xxxxxxx xx + xxxxx xx + xxxxxx + xxx + +-8-------------------- + xxxxxx xxxxx + xxxxxxxx xxxxxxx + xxx xxxxx xxx + xx xxx xx + xxx xxxxx xxx + xxxxxxxxxxxxxxxx + xxxxxx xxxxx + +-9-------------------- + xxxx + x xxxxxxxx + xxx xxxx xxxx + xxxx xx xx + xxxxx xxxx xxxx + xxxxxxxxxxxxxx + xxxxxxxxxx + +=*==================== +`); + +res += prepFont('y1s', ` +=*============================= +-0----------------------------- + xxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxx + xxxx xxxx + xx xx + xx xx + xxxx xxxx + xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx +-1----------------------------- + xx + xx x + xx xx + xxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx + xx + xx +-2----------------------------- + xxx x + xxxxx xx + xx xxx xx + xx xxx xx + xx xxxx xxxxx + xx xxxxxx xxxxxx + xx xxxxxxxxxxxxxx +xx xxxxxxxx +-3----------------------------- + x xx + xx x xx + xx xxx xx + xx xxxxx xx + xx xx xxxx xx + xxxx xxxx xxxx xx + xxxxxxxxxxxxxx xxxxx + xxxxxxxxxx xxx +-4----------------------------- + xxxx + xxxxxxxx + xxx xxxxxxxxx + xxx xxxxxxxxxx + xxxxxxxx xxxxxxxxxxx + xxxxxxxxxx + xxx + xxx +-5----------------------------- + x xxxxxxxxxxxxx + xx xxxxxxxxxxxx + xx xx xx + xx xx xx + xxx xx xx + xxxx xxxx xx + xxxxxxxxxxxxxx xx + xxxxxxxxxx +-6----------------------------- + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxx + xxxx xxxx xxxxxxxx + xx xx xxxxx + xx xx xxxx + xxxx xxxx xx + xxxxxxxxxxxx x + xxxxxxxx +-7----------------------------- + xx + xxxxxxx xx + xxxxxxxxxxxxxxx xx + xxxxxxxxxxxx xx + xxxxxxxxx xx + xxxxxxxxx + xxxxxx + xxx +-8----------------------------- + xxxxxxx xxxxxxx + xxxxxxxxxxx xxxxxxxxxx + xxxx xxxx xxxx xxx + xx xxxxx xx + xx xxxxx xx + xxxx xxxx xxxx xxx + xxxxxxxxxxx xxxxxxxxxx + xxxxxxx xxxxxxx +-9----------------------------- + xxxxxxx + x xxxxxxxxxxx + xxx xxxx xxxx + xxxx xx xx + xxxxxx xx xx + xxxxxxxx xxxx xxxx + xxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxx +=*============================= +`); + +res += prepFont('d10', ` +=*========================= +-1------------------------- +xxx +xxx xx +xxx xxx +xxx xxx +xxx xxxx +xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxx +xxx +xxx +xxx +-2------------------------- +xxx xx +xxxx xxx +xxxxx xxx +xxxxxx xx +xxxxxxx xxx +xxx xxxx xxx +xxx xxxx xxx +xxx xxxx xxx +xxx xxxxx xxxx +xxx xxxxxxxxxxx +xxx xxxxxxxxx +xxx xxxx +-3------------------------- + xx xxxx + xxx xxxx + xxx xxxx +xxxx xx xxxx +xxx xxxx xxxx +xxx xxxxxx xxxx +xxx xxxxxxxxx xxxx +xxxx xxxx xxxxxxxxx + xxxxxxxxxx xxxxxxx + xxxxxxxx xxxxx + xxxxxx xxx +=*========================= +`); + +res += prepFont('d1', ` +=*============================================== +-0---------------------------------------------- + + + xxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxx xxxxxxxx + xxxxx xxxxx + xxx xxx +xxxx xxxx +xxx xxx +xxx xxx +xxxx xxxx + xxx xxx + xxxxx xxxxx + xxxxxxxx xxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxx +-1---------------------------------------------- + + +xxx x +xxx xx +xxx xxx +xxx xxx +xxx xxxx +xxx xxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxx +xxx +xxx +xxx +-2---------------------------------------------- + + +xxxxxx xx +xxxxxxx xxxxx +xxxxxxxx xxxxxxx +xxx xxxxx xxxxxx +xxx xxxxx xxxx +xxx xxxxx xxx +xxx xxxxx xxx +xxx xxxxx xxx +xxx xxxxx xxx +xxx xxxxx xxx +xxx xxxxx xxx +xxx xxxxx xxxx +xxx xxxxx xxxx +xxx xxxxxx xxxxxx +xxx xxxxxxxx xxxxxxx +xxx xxxxxxxxxxxxxxxx +xxx xxxxxxxxxxxx +xxx xxxxxx +-3---------------------------------------------- + + + xxx xxxx + xxxxx xxxx + xxxxxxx xxxx + xxxxx x xxxx + xxxx xxx xxxx + xxx xxxx xxxx +xxxx xxxxxx xxxx +xxx xxxxxxx xxxx +xxx xxxxxxxxx xxxx +xxx xxx xxxxxxx xxxx +xxx xxx xxxxxx xxxx +xxxx xxxx xxxxxxxxxx + xxx xxx xxxxxxxxx + xxxx xxxx xxxxxxxx + xxxxx xxxxx xxxxxxx + xxxxxxxxxxxxxxxxxxxxx xxxxxx + xxxxxxxxxxxxxxxxx xxxxx + xxxxxxxxxxxxx xxxx +-4---------------------------------------------- + + + xxxxx + xxxxxxx + xxxxxxxxx + xxx xxxxxxx + xxx xxxxxxxx + xxx xxxxxxxxx + xxx xxxxxxxxxx + xxx xxxxxxxxxx + xxx xxxxxxxxxx + xxx xxxxxxxxxxx +xxxxxxxxxxxxxxx xxxxxxxxx +xxxxxxxxxxxxxxx +xxxxxxxxxxxxxxx + xxx + xxx + xxx + xxx + xxx +-5---------------------------------------------- + + + xxx xxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxx + xxxx xxxxxxxxxxxxxxx + xxx xxx xxxx +xxxx xxxx xxxx +xxx xxx xxxx +xxx xxx xxxx +xxx xxx xxxx +xxx xxx xxxx +xxxx xxxx xxxx + xxx xxx xxxx + xxxx xxxx xxxx + xxxx xxxx xxxx + xxxx xxxx xxxx + xxxxx xxxxx xxxx + xxxxxxxxxxxxx xxxx + xxxxxxxxx xxxx + xxxxxxx xxxx +-6---------------------------------------------- + + + xxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxxxxx xxxxxxxxxxxxx + xxxx xxxxxxxxxxxxx + xxx xxxx xxxxxxxxxxx +xxxx xxxx xxxxxxxxx +xxx xxx xxxxxxxx +xxx xxx xxxxxxx +xxx xxx xxxxxxx +xxx xxx xxxxxx +xxxx xxxx xxxxx + xxx xxx xxxx + xxxx xxxx + xxxxxx xxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxx +-7---------------------------------------------- + xxxx + xxxx + xxxx + xxxx + xxxx + xxxx + xxxx + xxxx +xxxxxxxxxxxxx xxxx +xxxxxxxxxxxxxxxxxxxxx xxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxx xxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxx + xxxxxxxxxxxxxxxx xxxx + xxxxxxxxxxxx xxxx + xxxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxx + xxxxxx + xxxxx + xxxx +-8---------------------------------------------- + + + xxxxxxx + xxxxxxxxxxxxx xxxxxxx + xxxxxxxxxxxxxxx xxxxxxxxxxx + xxxxxx xxxxxx xxxxxxxxxxxxx + xxxx xxx xxxxx xxxx + xxx xxxxxxx xxxx +xxxx xxxxx xxx +xxx xxxx xxxx +xxx xxx xxx +xxx xxx xxx +xxx xxxx xxxx +xxxx xxxxx xxx + xxx xxxxxxx xxxx + xxxx xxx xxxxx xxxx + xxxxxx xxxxxx xxxxxxxxxxxxx + xxxxxxxxxxxxxxx xxxxxxxxxxx + xxxxxxxxxxxxx xxxxxxx + xxxxxxx +-9---------------------------------------------- + + xxxx +xxx xxxxxxxx +xxx xxxxxxxxxxxx +xxx xxxxxx xxxxxx +xxx xxxxx xxxxx +xxxx xxx xxx +xxxx xxxx xxxx + xxxx xxx xxx + xxxx xxxx xxxx + xxxx xxx xxx + xxxxx xxxx xxxx + xxxxx xxx xxx + xxxxx xxxx xxxx + xxxxx xxx xxx + xxxxxx xxxxx xxxxx + xxxxxxxx xxxxxx xxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx +=*============================================== +`); + +res += prepFont('dow', ` +=*============================================== +-1---------------------------------------------- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxxx xxxxx + xxx xxx + xxxx xxxx + xxx xxx + xxx xxx + xxx xxx + xxx xxx + xxxx xxxx + xxx xxx + xxxxx xxxxx + xxxxxxxxxxxx + xxxxxxxxxx + xxxxxx +-2---------------------------------------------- + xxx + xxx + xxx + xxx + xxx + xxx + xxx + xxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx + xxx + xxx + xxx + xxx + xxx + xxx + xxx + + xxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxx + xxxx + xxx + xxx + xxx + xxxx + xxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx +-3---------------------------------------------- + xxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxx + xxxxxxxxx + xxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxx + + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxxx xxxxxxxx + xxx xxx xxx + xxxx xxx xxxx + xxx xxx xxx + xxx xxx xxx + xxx xxx xxx + xxx xxx xxx + xxxx xxx xxxx + xxx xxx xxx + xxxx xxxxxxxx + xxx xxxxxxx + xx xxxxxx + xxxx +-4---------------------------------------------- + xxx + xxx + xxx + xxx + xxx + xxx + xxx + xxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx + xxx + xxx + xxx + xxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxx + xxxx + xxx + xxx + xxx + xxxx + xxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxx +-5---------------------------------------------- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx xxx + xxxx xxx + xxx xxx + xxxx xxx + xxx xxx + xxxx xxx + xxx xxx + xxxx xxx + xxx xxx + xxx xxx + xxx + xxx + xxx + xxx + xxx + xxx + + + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxx + xxxx + xxx + xxx + xxx + xxx + xxx + xxx + xxxx + xxx + xxxx + xxx + x +-6---------------------------------------------- + xxx xxxxxxxx + xxxx xxxxxxxxxxxx + xxxx xxxxxxxxxxxxxx + xxxx xxxxx xxxxx + xxxx xxxx xxxx + xxx xxx xxx + xxx xxx xxxx + xxx xxxx xxx + xxx xxx xxx + xxxx xxx xxx + xxxx xxxx xxx + xxxx xxx xxx + xxxx xxx xxxx + xxxx xxxx xxx + xxxxx xxx xxx + xxxxxx xxxxx xxx + xxxxxxxxxxxxxxx xx + xxxxxxxxxxxx xxxx + xxxxxx xxxxxxxx + xxxxxxxxxx xx + xxxx xxxx xxx + xxxx xxxx xxx + xxx xxx xxx + xxx xxx xxx + xxx xxx xxx + xxxx xxxx xxxx + xxxx xxxx xxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx +-0---------------------------------------------- + xxx xxxxxxxx + xxxx xxxxxxxxxxxx + xxxx xxxxxxxxxxxxxx + xxxx xxxxx xxxxx + xxxx xxxx xxxx + xxx xxx xxx + xxx xxx xxxx + xxx xxxx xxx + xxx xxx xxx + xxxx xxx xxx + xxxx xxxx xxx + xxxx xxx xxx + xxxx xxx xxxx + xxxx xxxx xxx + xxxxx xxx xxx + xxxxxx xxxxx xxx + xxxxxxxxxxxxxxx xx + xxxxxxxxxxxx + xxxxxx + xxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxx + xxxx + xxx + xxx + xxx + xxxx + xxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx +=*============================================== +`); + +res += prepFont('m', ` +<<<<=*============================================== +-1---------------------------------------------- + xxxx xxxxx + xxx xxxxx + xxx xxxxx + xxx xxxxx +xxx xxxxx +xxx xxxxx +xxx xxxxx +xxxx xxxxx + xxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxx + xxxxx + xxx xxxxx + xxxxx xx xxxxx + xxxxxxx xxx xxxxx + xxx xxx xxxx xxxxx +xxx xxx xxx xxxxx +xxx xxx xxx +xxx xxx xxx + xx xxx xxxx + xxxxxxxxxxxx +xxxxxxxxxxxx +xxxxxxxxxxx + + +xxxxxxxxxxxxxx +xxxxxxxxxxxxxx +xxxxxxxxxxxxx + xxxx + xxx + xxx + xxx + xxxx +xxxxxxxxxxxxx +xxxxxxxxxxxx +xxxxxxxxxxx +-2---------------------------------------------- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx xxxx + xxxx xxxx + xxx xxxx + xxxx xxxx + xxx xxxx + xxxxxx xxxx xxxx + xxxxxxxxxx xxx xxxx + xxxxxxxxxxxx xxxx xxxx + xxxx xxxxxxx xxx xxxx +xxxx xxx xxxx xxxx xxxx +xxx xxx xxx xxxx +xxx xxx xxx xxxx +xxx xxx xxx xxxx +xxxx xxx xxxx xxxx + xxx xxxxxxx xxxx + xxxx xxxxxxx + xxx xxxxxx + x xxx + + +xxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx + xxx xxx +xxxx xxxx +xxx xxx +xxxx xxxx + xxx xxx + xxxxx xxxxx + xxxxxxxxxx + xxxxxxxx + xxxx +-3---------------------------------------------- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + xxx + xxxxx xx + xxxxxxx xxx + xxx xxx xxx +xxx xxx xxx +xxx xxx xxx +xxx xxx xxx + xx xxx xxxx + xxxxxxxxxxxx +xxxxxxxxxxxx +xxxxxxxxxxx + + +xxxxxxxxxxxxxx +xxxxxxxxxxxxxx +xxxxxxxxxxxxx + xxxx + xxxx + xxx + xxxx + xxxx + xxx + xx +<<<<-4---------------------------------------------- + xxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxx + +xxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxx + xxx xxx + xxxx xxxx + xxx xxx + xxxx xxxx + xxx xxx + xxxxx xxxxx + xxxxxxxxxx + xxxxxxxx + xxxx + + + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxx + xxxx + xxx + xxxx + xxxx +<<<<-5---------------------------------------------- + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xx +xx +xx xxx + x xxxxx xx + x xxxxxxx xxx + xx xxx xxx xxxx + xx xxx xxx xxx + xx xxx xxx xxx + xx xxx xxx xxx + xx xx xxx xxxx + xx xxxxxxxxxxxx + xx xxxxxxxxxxxx + x xxxxxxxxxxx + x +xx +xx xxxxxxxxxxx +xx xxxxxxxxxxxxx +xx xxxxxxxxxxxxx +xxx xxxxx +xxx xxx +xxxxxxxxx + xxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxx +-6---------------------------------------------- + xxxx xxxxx + xxx xxxxx + xxx xxxxx + xxx xxxxx +xxx xxxxx +xxx xxxxx +xxx xxxxx +xxxx xxxxx + xxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxx + xxxxx + xxxxxxxxxxx xxxxx + xxxxxxxxxxxx xxxxx + xxxxxxxxxxxxx xxxxx + xxxx xxxxx +xxx xxxxx +xxx +xxx +xxxx + xxxxxxxxxxxxx +xxxxxxxxxxxxxx +xxxxxxxxxxxxxx + + +xxxxxxxxxxxxxx +xxxxxxxxxxxxxx +xxxxxxxxxxxxx + xxxx + xxx + xxx + xxx + xxxx +xxxxxxxxxxxxx +xxxxxxxxxxxx +xxxxxxxxxxx +-7---------------------------------------------- + xxxx xxxxx + xxx xxxxx + xxx xxxxx + xxx xxxxx +xxx xxxxx +xxx xxxxx +xxx xxxxx +xxxx xxxxx + xxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxx + xxxxx + xxxxxxxxxxx xxxxx + xxxxxxxxxxxx xxxxx + xxxxxxxxxxxxx xxxxx + xxxx xxxxx +xxx xxxxx +xxx +xxx + xxxx + xxxxxxxxxxxxx +xxxxxxxxxxxxxx +xxxxxxxxxxxxxx + + +xxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxx +<<<<-8---------------------------------------------- + xxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx +x xxxxxxxxxxxx +x xxxxxx +xx +xx +xx xxxxxxxxxxx + x xxxxxxxxxxxx + x xxxxxxxxxxxxx + x xxxx + xx xxx + xx xxx + xx xxxx + xx xxxxxxxxxxxxx + xx xxxxxxxxxxxxxx + xx xxxxxxxxxxxxxx + x + x +xx xxxxxxxx +xx xxxxxxxxxxxx +xx xxxxxxxxxxxx +xx xxxxx xxxxx +xxx xxx xxx +xxx xxx xxx +xxxxxxxxx xxxxx + xxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxx +<<<<-9---------------------------------------------- + xxxxx xxxxxxxxxxxx + xxxxx xxxxxxxxxxxxxxxxxx + xxxx xxxxxxxxxxxxxxxxxxxxxx + xxx xxxxxxxxx xxxxxxx + xxxx xxxxxx xxxxx + xxx xxxx xxxx + xxxx xxxx xxxx + xxxxx xxxxxx xxxx + xxxxxxxxxxxxxxxxxxxx xxxx + xxxxxxxxxxxxxxxxxx xxxxx + xxxxxxxxxxxxxx xxxxxx + xxxxxxx + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxx xxxxxxx + xxxx xxx xxxx + xxx xxx xxx + xxx xxx xxx + xxxx xxx xxxx + xxx xxxxxxx + xxxx xxxxxxx + xxx xxxxxx + x xxx + + +xxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxx + xxx xxx + xxxx xxxx + xxx xxx + xxxx xxxx + xxx xxx + xxxxx xxxxx + xxxxxxxxxx + xxxxxxxx + xxxx +-:---------------------------------------------- + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxxxx +xxxx xxxx +xxx xxx +xxx xxx +xxxx xxxx + xxxxx xxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxx xxxx +xxx xxx +xxx xxx +xxx xxx +xxx xxx + xxx xxx + xxx xxx + xx xx + + + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx +xxxx xxx +xxx xxx +xxx xxx +xxx xxx + xxx + xxx + xx +-;---------------------------------------------- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx +xxxxx xxxxx +xxx xxx +xxx xxx +xxxxx xxxxx + xxxxxxxxxxxx + xxxxxxxxxx + xxxxxx + x + xxxx + xxxxxxx + xxxxxxxx + xxxxxxxx +xxxxxxxx + xxxxxxxx + xxxxxxxx + xxxxxxx + xxx +-<---------------------------------------------- +xxx xxxx +xxx xxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxx xxxxx +xxxx xxxxxxx + xxxxx xxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxx xxxxxxx +xxxx xxx xxxx +xxx xxx xxx +xxx xxx xxx +xxxx xxx xxxx + xxx xxxxxxx + xxxx xxxxxxx + xxx xxxxxx + x xxx + + + xxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxx xxxx +xxx xxx +xxx xxx +xxx xxx + xxx xxx + xxx xxx + xx xx +<<<<=*============================================== +`); + +res += prepBitmap('lock', ` + xxxx + xxxxxx + xxx xxx + xx xx + xx xx +xxxxxxxxxx +x x +x xxxx x +x x +x x +x xxxx x +x x +x x +x xxxx x +x x +x x +xxxxxxxxxx +`); + +res += prepBitmap('lockS', ` + xxx + xxxxx + xx xx + xx xx +xxxxxxxxx +x x +x xxx x +x x +x xxx x +x x +x xxx x +x x +xxxxxxxxx +`); + +res += prepBitmap('battery', ` + xx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +xxxxxxxx +`); + +res += prepBitmap('charge', ` + x + xx + xx + xx + xx + xxx + xxxxxx + xxx + xx + xx + xx + xx + x +`); + +res += prepBitmap('HRM', ` + xxx xxx + x xxx xxx x + xx xxxxxxxx xx +xxx xxxxxxxx xxx +xxx xxxxxxxx xxx +xxx xxxxxxxx xxx +xxx xxxxxxx xx +x x x xx x x + x x xx x x x +xx x xxxxxx x xx + xxx xxxxxxxx x + xx xxxxxxxx + xxxxxxxxxx + xxxxxxxx + xxxxxx + xxxx + xx +`); + +res += prepBitmap('compass', ` + x + x + xxx + xxx + xxxxx + xx xx + xxx xxx + xx xx +xx xx +`); + +res += prepBitmap('y100', ` + xxxxx xxx +xxxxxxx xxxxx +x xxx xx xx + xx xxx xxx + xxx xx xx + xxxx xx xx +xxxxx xx xx + xxx xx xx + xxx xx xx + xxx xx xx + xxx xx xx + xx xx xx + x xx xx + xxx xxx + xx xx + xxxxx + xxx +`); + +res += prepBitmap('y100s', ` + xx xx +x xx xxxx + xx xx xx +xxx xx xx + xx xx xx + xx xx xx + x xx xx + xx xx + xxxx + xx +`); + +print(res); diff --git a/apps/poweroff/README.md b/apps/poweroff/README.md index 3aeff5e8d..d9d7a8dbc 100644 --- a/apps/poweroff/README.md +++ b/apps/poweroff/README.md @@ -4,10 +4,10 @@ Simple app to power off your Bangle.js ## Usage -Start the app shutdowns your Bangle.js watch after a short delay. +Start the app to shutdown your Bangle.js watch after a short delay. ## Creator -Marco (@myxor) +Marco ([myxor](https://github.com/myxor)) ## Icon -Icon taken from https://materialdesignicons.com/ Apache License 2.0 +Icon taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/poweroff/app-icon.js b/apps/poweroff/app-icon.js index 7caf256a2..81a2527b5 100644 --- a/apps/poweroff/app-icon.js +++ b/apps/poweroff/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwMB/4Ak/k/ArX8AoIGC/F8n0fAoPwAoMPAoPgAoMHAoPAC4MDAoPBAoODAoODAoPBAoOHAoPhAo8HAoPgAoMPAoPwArRQCFIRQCGoQCBHYYFEKARNCAQQIC4ACBMoXgv/+EwXwn/8GQX4g/gRIX8b4KVC/wFBv6iCwDnE+AcCAF4=")) +require("heatshrink").decompress(atob("mEwgIolgfAAqkCAoNAAoMHAoPgAoMPwfB+AFBj/D4f4AoM/AoP8AoQRBAoV/DoP+AoN+AoN+AoP8AoM/Ao/4AoMfAsBQCAo5QDAo5KCAQV/AQJZCn+AgIUD4EDAoUf+EPFgUP///RIUHAoKVCgYFBVAYFBWYc/EQQAvA")) diff --git a/apps/poweroff/app.png b/apps/poweroff/app.png index 5c199c3ba..aa186ab20 100644 Binary files a/apps/poweroff/app.png and b/apps/poweroff/app.png differ diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog new file mode 100644 index 000000000..de38d715a --- /dev/null +++ b/apps/ptlaunch/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial creation of the pattern launch app +0.02: Turn on lcd when launching an app if the lock screen was disabled in the settings +0.03: Make tap to confirm new pattern more reliable. Also allow for easier creation of single circle patterns. +0.10: Improve the management of existing patterns: Draw the linked pattern on the left hand side of the app name within a scroller, similar to the default launcher. Slighlty clean up the code to make it less horrible. \ No newline at end of file diff --git a/apps/ptlaunch/README.md b/apps/ptlaunch/README.md new file mode 100644 index 000000000..8d61afece --- /dev/null +++ b/apps/ptlaunch/README.md @@ -0,0 +1,53 @@ +# Pattern Launcher + +Directly launch apps from the clock screen with custom patterns. + +## Usage + +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. + +## Add Pattern Screenshots + +![](main_menu_add.png) +![](add_pattern.png) +![](select_app.png) + +## Manage Pattern Screenshots + +![](main_menu_manage.png) +![](manage_patterns.png) + +## Detailed Steps + +From the main menu you can: +- Add a new pattern and link it to an app (first entry) + - To create a new pattern first select "Add Pattern" + - Now draw any pattern you like, this will later launch the linked app from the clock screen + - You can also draw a single-circle pattern (meaning a single tap on one circle) instead of drawing a 'complex' pattern + - If you don't like the pattern, simply re-draw it. The previous pattern will be discarded. + - If you are happy with the pattern tap on screen or press the button to continue + - Now select the app you want to launch with the pattern. + - Note, you can bind multiple patterns to the same app. +- Manage created patterns (second entry) + - To manage your patterns first select "Manage Patterns" + - You will now see a scrollabe list of patterns + linked apps + - If you want to deletion a pattern (and unlink the app) simply tap on it, and confirm the deletion +- Disable the lock screen on the clock screen from the settings (third entry) + - To launch the app from the pattern on the clock screen the watch must be unlocked. + - If this annoys you, you can disable the lock on the clock screen from the setting here + +## FAQ + +1) Nothing happens when I draw on the clock screen! + +Please double-check if you actually have a pattern linked to an app. + +2) I have a pattern linked to an app and still nothing happens when I draw on the clock screen! + +Make sure the watch is unlocked before you start drawing. If this bothers you, you can permanently disable the watch-lock from within the Pattern Launcher app (via the Settings). + +3) I have done all that and still nothing happens! + +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! diff --git a/apps/ptlaunch/add_pattern.png b/apps/ptlaunch/add_pattern.png new file mode 100644 index 000000000..c7cc38e82 Binary files /dev/null and b/apps/ptlaunch/add_pattern.png differ diff --git a/apps/ptlaunch/app-icon.js b/apps/ptlaunch/app-icon.js new file mode 100644 index 000000000..07f025d71 --- /dev/null +++ b/apps/ptlaunch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE//ziEAiM//4ACmMAgMvA4fziIIBCAUgEAUCBwXxFIYYDCAvyHIgPCGwIgFCA0wAYIRCh49BLQoXB+AHEgYUBgQaCE4JGEHAZGDAAMBAQMjC4UBEw0Aj5PFDIchC4Q/BC5CtIgIXUGwIXDI6EBiCaCCYJ3RIganTa5AnEgbXIewwPGn4ICCA8hgESAoQABmUQgI2CCBQA/AAvzbIRuD/8xNwMTCBTfDPwbYEPAaPDf4LnFB4T/EEAS/Fj7vFZ4LvBgMiFIQXBCAwmEE4Q3BiUikTnHJAQFEJ4XwgERHgI/CJ4oAIC4QYBiYXDCxgXDgUzLQQXIGwpHDLoRHJgJmFO4arCO5MCK4QACh6nCJ4poDCAbGFe4QnEY4IgGG4oOCc4ofCbAj3C/8hiMSAoQYCiMRMQQQKAH4AGkMAJwsyiEBL4wQER4Z+DR5AQFX4ooCX44QGVobvOgMREAUQBwg3B+IXFc4cTmYUBgIXFgImCAAkf/59BkERIgMBBwo/BC5AkDCgwXOAAIMGI5xFBBgR3SJYinXa5A4EfAQQHewoABJAgfCCA/zFAMRn4OC/8xIAIWDCAJGBgIQBA==")) \ No newline at end of file diff --git a/apps/ptlaunch/app.js b/apps/ptlaunch/app.js new file mode 100644 index 000000000..b5a3bf610 --- /dev/null +++ b/apps/ptlaunch/app.js @@ -0,0 +1,608 @@ +var DEBUG = false; + +var storage = require("Storage"); + +var showMainMenu = () => { + log("loading patterns"); + var storedPatterns = storage.readJSON("ptlaunch.patterns.json", 1) || {}; + + var mainmenu = { + "": { + title: "Pattern Launcher", + }, + "< Back": () => { + log("cancel"); + load(); + }, + "Add Pattern": () => { + log("creating pattern"); + recognizeAndDrawPattern().then((pattern) => { + log("got pattern"); + log(pattern); + log(pattern.length); + + var confirmPromise = new Promise((resolve) => resolve(true)); + + if (storedPatterns[pattern]) { + log("pattern already exists. show confirmation prompt"); + confirmPromise = E.showPrompt("Pattern already exists\nOverwrite?", { + title: "Confirm", + buttons: { Yes: true, No: false }, + }); + } + + confirmPromise.then((confirm) => { + log("confirmPromise resolved: " + confirm); + if (!confirm) { + showMainMenu(); + return; + } + + log("selecting app"); + getSelectedApp().then((app) => { + E.showMessage("Saving..."); + log("got app"); + log("saving pattern"); + + storedPatterns[pattern] = { + app: { name: app.name, src: app.src }, + }; + storage.writeJSON("ptlaunch.patterns.json", storedPatterns); + showMainMenu(); + }); + }); + }); + }, + "Manage Patterns": () => { + log("selecting pattern through app"); + showScrollerContainingAppsWithPatterns().then((selected) => { + var pattern = selected.pattern; + var appName = selected.appName; + if (pattern === "back") { + showMainMenu(); + } else { + E.showPrompt(appName + "\n\npattern:\n" + pattern, { + title: "Delete?", + buttons: { Yes: true, No: false }, + }).then((confirm) => { + if (confirm) { + E.showMessage("Deleting..."); + delete storedPatterns[pattern]; + storage.writeJSON("ptlaunch.patterns.json", storedPatterns); + showMainMenu(); + } else { + showMainMenu(); + } + }); + } + }); + }, + Settings: () => { + var settings = storedPatterns.settings || {}; + + var settingsmenu = { + "": { + title: "Pattern Settings", + }, + "< Back": () => { + log("cancel"); + load(); + }, + }; + + if (settings.lockDisabled) { + settingsmenu["Enable lock"] = () => { + settings.lockDisabled = false; + storedPatterns.settings = settings; + Bangle.setOptions({ lockTimeout: 1000 * 30 }); + storage.writeJSON("ptlaunch.patterns.json", storedPatterns); + showMainMenu(); + }; + } else { + settingsmenu["Disable lock"] = () => { + settings.lockDisabled = true; + storedPatterns.settings = settings; + storage.writeJSON("ptlaunch.patterns.json", storedPatterns); + Bangle.setOptions({ lockTimeout: 1000 * 60 * 60 * 24 * 365 }); + showMainMenu(); + }; + } + + E.showMenu(settingsmenu); + }, + }; + E.showMenu(mainmenu); +}; + +var positions = []; +var recognizeAndDrawPattern = () => { + return new Promise((resolve) => { + E.showMenu(); + g.clear(); + g.setColor(0, 0, 0); + CIRCLES.forEach((circle) => drawCircle(circle)); + + var pattern = []; + + var isFinished = false; + var finishHandler = () => { + if (pattern.length === 0 || isFinished) { + return; + } + log("Pattern is finished."); + isFinished = true; + Bangle.removeListener("drag", dragHandler); + Bangle.removeListener("tap", finishHandler); + resolve(pattern.join("")); + }; + setWatch(() => finishHandler(), BTN); + setTimeout(() => Bangle.on("tap", finishHandler), 250); + + positions = []; + var dragHandler = (position) => { + log(position); + positions.push(position); + + debounce().then(() => { + if (isFinished) { + return; + } + + // This might actually be a 'tap' event. + // Use this check in addition to the actual tap handler to make it more reliable + if (pattern.length > 0 && positions.length === 2) { + if ( + positions[0].x === positions[1].x && + positions[0].y === positions[1].y + ) { + finishHandler(); + positions = []; + return; + } + } + + E.showMessage("Calculating..."); + var t0 = Date.now(); + + log(positions.length); + + var circlesClone = cloneCirclesArray(); + pattern = []; + + var step = Math.floor(positions.length / 100) + 1; + + var p, a, b, circle; + + for (var i = 0; i < positions.length; i += step) { + p = positions[i]; + + circle = circlesClone[0]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(0, 1); + } + } + + circle = circlesClone[1]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(1, 1); + } + } + + circle = circlesClone[2]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(2, 1); + } + } + + circle = circlesClone[3]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(3, 1); + } + } + + circle = circlesClone[4]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(4, 1); + } + } + + circle = circlesClone[5]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(5, 1); + } + } + + circle = circlesClone[6]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(6, 1); + } + } + circle = circlesClone[7]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(7, 1); + } + } + + circle = circlesClone[8]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circlesClone.splice(8, 1); + } + } + } + var tx = Date.now(); + log(tx - t0); + positions = []; + var t1 = Date.now(); + log(t1 - t0); + + log("pattern:"); + log(pattern); + + log("redrawing"); + g.clear(); + drawCirclesWithPattern(pattern); + }); + }; + + Bangle.on("drag", dragHandler); + }); +}; + +var getAppList = () => { + var appList = storage + .list(/\.info$/) + .map((appInfoFileName) => { + var appInfo = storage.readJSON(appInfoFileName, 1); + return ( + appInfo && { + name: appInfo.name, + // type: appInfo.type, + // icon: appInfo.icon, + sortorder: appInfo.sortorder, + src: appInfo.src, + } + ); + }) + .filter((app) => app && !!app.src); + appList.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + + return appList; +}; + +var getSelectedApp = () => { + E.showMessage("Loading apps..."); + return new Promise((resolve) => { + var selectAppMenu = { + "": { + title: "Select App", + }, + "< Cancel": () => { + log("cancel"); + showMainMenu(); + }, + }; + + var appList = getAppList(); + appList.forEach((app) => { + selectAppMenu[app.name] = () => { + log("app selected"); + log(app); + resolve(app); + }; + }); + + E.showMenu(selectAppMenu); + }); +}; + +////// +// manage pattern related variables and functions +// - draws all saved patterns and their linked app names +// - uses the scroller to allow the user to browse through them +////// + +var scrollerFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; + +var drawBackButton = (r) => { + g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); + g.setFont(scrollerFont) + .setFontAlign(-1, 0) + .drawString("< Back", 64, r.y + 32); +}; + +var drawAppWithPattern = (i, r, storedPatterns) => { + log("draw app with pattern"); + log({ i: i, r: r, storedPatterns: storedPatterns }); + var storedPattern = storedPatterns[i]; + var pattern = storedPattern.pattern; + var app = storedPattern.app; + + g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); + + g.drawLine(r.x, r.y, 176, r.y); + + drawCirclesWithPattern(pattern, { + enableCaching: true, + scale: 0.33, + offset: { x: 1, y: 3 + r.y }, + }); + + g.setColor(0, 0, 0); + if (!storedPattern.wrappedAppName) { + storedPattern.wrappedAppName = g + .wrapString(app.name, g.getWidth() - 64) + .join("\n"); + } + log(g.getWidth()); + log(storedPattern.wrappedAppName); + g.setFont(scrollerFont) + .setFontAlign(-1, 0) + .drawString(storedPattern.wrappedAppName, 64, r.y + 32); +}; + +var showScrollerContainingAppsWithPatterns = () => { + var storedPatternsArray = getStoredPatternsArray(); + log("drawing scroller for stored patterns"); + log(storedPatternsArray); + log(storedPatternsArray.length); + + g.clear(); + + var c = Math.max(storedPatternsArray.length + 1, 3); + + return new Promise((resolve) => { + E.showScroller({ + h: 64, + c: c, + draw: (i, r) => { + log("draw"); + log({ i: i, r: r }); + if (i <= 0) { + drawBackButton(r); + } else if (i <= storedPatternsArray.length) { + drawAppWithPattern(i - 1, r, storedPatternsArray); + } + }, + select: (i) => { + log("selected: " + i); + var pattern = "back"; + var appName = ""; + if (i > 0) { + var storedPattern = storedPatternsArray[i - 1]; + pattern = storedPattern.pattern.join(""); + appName = storedPattern.app.name; + } + clearCircleDrawingCache(); + resolve({ pattern: pattern, appName: appName }); + }, + }); + }); +}; + +////// +// storage related functions: +// - stored patterns +// - stored settings +////// + +var getStoredPatternsMap = () => { + log("loading stored patterns map"); + var storedPatternsMap = storage.readJSON("ptlaunch.patterns.json", 1) || {}; + delete storedPatternsMap.settings; + log(storedPatternsMap); + return storedPatternsMap; +}; + +var getStoredPatternsArray = () => { + var storedPatternsMap = getStoredPatternsMap(); + log("converting stored patterns map to array"); + var patterns = Object.keys(storedPatternsMap); + var storedPatternsArray = []; + for (var i = 0; i < patterns.length; i++) { + var pattern = "" + patterns[i]; + storedPatternsArray.push({ + pattern: pattern + .split("") + .map((circleIndex) => parseInt(circleIndex, 10)), + app: storedPatternsMap[pattern].app, + }); + } + log(storedPatternsArray); + return storedPatternsArray; +}; + +////// +// circle related variables and functions: +// - the circle array itself +// - the radius and the squared radius of the circles +// - circle draw function +////// + +var CIRCLE_RADIUS = 25; +var CIRCLE_RADIUS_2 = CIRCLE_RADIUS * CIRCLE_RADIUS; + +var CIRCLES = [ + { x: 25, y: 25, i: 0 }, + { x: 87, y: 25, i: 1 }, + { x: 150, y: 25, i: 2 }, + { x: 25, y: 87, i: 3 }, + { x: 87, y: 87, i: 4 }, + { x: 150, y: 87, i: 5 }, + { x: 25, y: 150, i: 6 }, + { x: 87, y: 150, i: 7 }, + { x: 150, y: 150, i: 8 }, +]; + +var drawCircle = (circle, drawBuffer, scale) => { + if (!drawBuffer) { + drawBuffer = g; + } + if (!scale) { + scale = 1; + } + + var x = circle.x * scale; + var y = circle.y * scale; + var r = CIRCLE_RADIUS * scale; + + log("drawing circle"); + log({ x: x, y: y, r: r }); + + drawBuffer.fillCircle(x, y, r); +}; + +var cachedCirclesDrawings = {}; + +var clearCircleDrawingCache = () => { + cachedCirclesDrawings = {}; +}; + +var drawCirclesWithPattern = (pattern, options) => { + if (!pattern || pattern.length === 0) { + pattern = []; + } + if (!options) { + options = {}; + } + var enableCaching = options.enableCaching; + var scale = options.scale; + var offset = options.offset; + if (!enableCaching) { + enableCaching = false; + } + if (!scale) { + scale = 1; + } + if (!offset) { + offset = { x: 0, y: 0 }; + } + + log("drawing circles with pattern, scale and offset"); + log(pattern); + log(scale); + log(offset); + + // cache drawn patterns. especially useful for the manage pattern menu + var image = cachedCirclesDrawings[pattern.join("")]; + if (!image) { + log("circle image not cached"); + var drawBuffer = Graphics.createArrayBuffer( + g.getWidth() * scale, + g.getHeight() * scale, + 1, + { msb: true } + ); + + drawBuffer.setColor(1); + CIRCLES.forEach((circle) => drawCircle(circle, drawBuffer, scale)); + + drawBuffer.setColor(0); + drawBuffer.setFontAlign(0, 0); + drawBuffer.setFont("6x8", 4 * scale); + pattern.forEach((circleIndex, patternIndex) => { + var circle = CIRCLES[circleIndex]; + drawBuffer.drawString( + patternIndex + 1, + circle.x * scale, + circle.y * scale + ); + }); + + image = { + width: drawBuffer.getWidth(), + height: drawBuffer.getHeight(), + bpp: 1, + buffer: drawBuffer.buffer, + }; + + if (enableCaching) { + cachedCirclesDrawings[pattern.join("")] = image; + } + } else { + log("using cached circle image"); + } + + g.drawImage(image, offset.x, offset.y); +}; + +var cloneCirclesArray = () => { + var circlesClone = Array(CIRCLES.length); + + for (var i = 0; i < CIRCLES.length; i++) { + circlesClone[i] = CIRCLES[i]; + } + + return circlesClone; +}; + +////// +// misc lib functions +////// + +var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } +}; + +var debounceTimeoutId; +var debounce = (delay) => { + if (debounceTimeoutId) { + clearTimeout(debounceTimeoutId); + } + + return new Promise((resolve) => { + debounceTimeoutId = setTimeout(() => { + debounceTimeoutId = undefined; + resolve(); + }, delay || 500); + }); +}; + +////// +// run main function +////// + +showMainMenu(); diff --git a/apps/ptlaunch/app.png b/apps/ptlaunch/app.png new file mode 100644 index 000000000..14ed77f1d Binary files /dev/null and b/apps/ptlaunch/app.png differ diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js new file mode 100644 index 000000000..a23607768 --- /dev/null +++ b/apps/ptlaunch/boot.js @@ -0,0 +1,190 @@ +var DEBUG = true; +var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } +}; + +var storedPatterns; +var positions = []; +var dragHandler = (position) => { + positions.push(position); + + debounce().then(() => { + log(positions.length); + + var CIRCLE_RADIUS = 25; + var CIRCLE_RADIUS_2 = CIRCLE_RADIUS * CIRCLE_RADIUS; + + var circles = [ + { x: 25, y: 25, i: 0 }, + { x: 87, y: 25, i: 1 }, + { x: 150, y: 25, i: 2 }, + { x: 25, y: 87, i: 3 }, + { x: 87, y: 87, i: 4 }, + { x: 150, y: 87, i: 5 }, + { x: 25, y: 150, i: 6 }, + { x: 87, y: 150, i: 7 }, + { x: 150, y: 150, i: 8 }, + ]; + var pattern = []; + + var step = Math.floor(positions.length / 100) + 1; + + var p, a, b, circle; + + for (var i = 0; i < positions.length; i += step) { + p = positions[i]; + + circle = circles[0]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(0, 1); + } + } + + circle = circles[1]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(1, 1); + } + } + + circle = circles[2]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(2, 1); + } + } + + circle = circles[3]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(3, 1); + } + } + + circle = circles[4]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(4, 1); + } + } + + circle = circles[5]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(5, 1); + } + } + + circle = circles[6]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(6, 1); + } + } + circle = circles[7]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(7, 1); + } + } + + circle = circles[8]; + if (circle) { + a = p.x - circle.x; + b = p.y - circle.y; + if (CIRCLE_RADIUS_2 - (a * a + b * b) >= 0) { + pattern.push(circle.i); + circles.splice(8, 1); + } + } + } + positions = []; + + pattern = pattern.join(""); + + if (pattern) { + if (storedPatterns[pattern]) { + var app = storedPatterns[pattern].app; + if (!!app && !!app.src) { + if (storedPatterns.settings) { + if (storedPatterns.settings.lockDisabled) { + Bangle.setLCDPower(true); + } + } + + Bangle.removeListener("drag", dragHandler); + load(app.src); + } + } + } + }); +}; + +var debounceTimeoutId; +var debounce = (delay) => { + if (debounceTimeoutId) { + clearTimeout(debounceTimeoutId); + } + + return new Promise((resolve) => { + debounceTimeoutId = setTimeout(() => { + debounceTimeoutId = undefined; + resolve(); + }, delay || 500); + }); +}; + +(function () { + var sui = Bangle.setUI; + Bangle.setUI = function (mode, cb) { + sui(mode, cb); + if (!mode) { + Bangle.removeListener("drag", dragHandler); + storedPatterns = {}; + return; + } + if (!mode.startsWith("clock")) { + storedPatterns = {}; + Bangle.removeListener("drag", dragHandler); + return; + } + + var storage = require("Storage"); + storedPatterns = storage.readJSON("ptlaunch.patterns.json", 1) || {}; + if (Object.keys(storedPatterns).length > 0) { + Bangle.on("drag", dragHandler); + if (storedPatterns.settings) { + if (storedPatterns.settings.lockDisabled) { + Bangle.setOptions({ lockTimeout: 1000 * 60 * 60 * 24 * 365 }); + } + } + } + }; +})(); diff --git a/apps/ptlaunch/main_menu_add.png b/apps/ptlaunch/main_menu_add.png new file mode 100644 index 000000000..e9a5c52a9 Binary files /dev/null and b/apps/ptlaunch/main_menu_add.png differ diff --git a/apps/ptlaunch/main_menu_manage.png b/apps/ptlaunch/main_menu_manage.png new file mode 100644 index 000000000..a6aee1427 Binary files /dev/null and b/apps/ptlaunch/main_menu_manage.png differ diff --git a/apps/ptlaunch/manage_patterns.png b/apps/ptlaunch/manage_patterns.png new file mode 100644 index 000000000..82b10ad43 Binary files /dev/null and b/apps/ptlaunch/manage_patterns.png differ diff --git a/apps/ptlaunch/select_app.png b/apps/ptlaunch/select_app.png new file mode 100644 index 000000000..56f0dfc83 Binary files /dev/null and b/apps/ptlaunch/select_app.png differ diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog index 135e69d23..fb6c751bb 100644 --- a/apps/qalarm/ChangeLog +++ b/apps/qalarm/ChangeLog @@ -1,2 +1,5 @@ 0.01: First version! -0.02: Fixed alarms not working and localised days of week. \ No newline at end of file +0.02: Fixed alarms not working and localised days of week. +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 diff --git a/apps/qalarm/app-icon.js b/apps/qalarm/app-icon.js index 1a014b796..12d2c103f 100644 --- a/apps/qalarm/app-icon.js +++ b/apps/qalarm/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("/wA/AH4A/AH4AF0WiF1wwtF73GB53MAAgkY4wABFqIxPEhQuXGB4vUFxYwMEpBpGBwouNGAwfFF5I1KF6ZQHGAwNLFx4wHF/4v/F/4v/AoYGDF6gaFF5AwHL7QuMBJQvWEpwvxBQ4uRGBAkJT4wuWGBIuIRjKRNF8wwXFy4wWFzIwU53NFzPN5wuR5/PGK4tBDYSNQ5wVCCwIzBAAQoIAAQWGSJ5HFDYYAQIYTCRKRIeBAAYmDAAZsJMCQAbeCAybFiQ0XFTQAIzgAGFcYvz0QAGF84wGF1AwFF1QA/AH4A/ADQ=")) +require("heatshrink").decompress(atob("mEw4UA///gH+93+oH9Jf8AgfABZMP+ALRmADCitUAgUMAQP8AQMBqtVoAFBn4CBDwUFBYNQFAQLEioLBEgQLBgfwE4IKBAAI3BBYXAE4ILE/gJBAIM8HQQ8CngL/n4LFKYR3BhgLFNYSDCBYqPFBZKzBUwSoDWYTLBUwSoDZYQABBQa0DBZCoBAAY6EcojhEHgoACkoLFrALD1WVBQdW1QLDtQMDBQOpHQmqAAg8DIwQKEJAg6FMApfLDIoJFAAX//4KIBbE/aAIAIh7oBAH4A==")) diff --git a/apps/qalarm/app.js b/apps/qalarm/app.js index 64f601bf6..ad071adf0 100644 --- a/apps/qalarm/app.js +++ b/apps/qalarm/app.js @@ -41,6 +41,7 @@ function getCurrentTime() { function showMainMenu() { const menu = { "": { title: "Alarms" }, + "< Back" : () => load(), "New Alarm": () => showEditAlarmMenu(-1), "New Timer": () => showEditTimerMenu(-1), }; @@ -54,9 +55,7 @@ function showMainMenu() { else showEditAlarmMenu(idx); }; }); - menu["< Back"] = () => { - load(); - }; + menu if (WIDGETS["qalarm"]) WIDGETS["qalarm"].reload(); return E.showMenu(menu); @@ -86,6 +85,7 @@ function showEditAlarmMenu(alarmIndex, alarm) { const menu = { "": { title: alarm.msg ? alarm.msg : "Alarms" }, + "< Back" : showMainMenu, Hours: { value: hrs, onchange: function (v) { @@ -162,7 +162,6 @@ function showEditAlarmMenu(alarmIndex, alarm) { showMainMenu(); }; } - menu["< Back"] = showMainMenu; return E.showMenu(menu); } @@ -206,6 +205,7 @@ function showEditTimerMenu(timerIndex) { const menu = { "": { title: "Timer" }, + "< Back" : showMainMenu, Hours: { value: hrs, onchange: function (v) { @@ -264,7 +264,7 @@ function showEditTimerMenu(timerIndex) { showMainMenu(); }; } - menu["< Back"] = showMainMenu; + return E.showMenu(menu); } diff --git a/apps/qalarm/boot.js b/apps/qalarm/boot.js index 6713ad9e1..5e9560ee2 100644 --- a/apps/qalarm/boot.js +++ b/apps/qalarm/boot.js @@ -1 +1 @@ -eval(require("Storage").read("qalarmcheck.js")); +(function() { eval(require("Storage").read("qalarmcheck.js")); })() diff --git a/apps/qalarm/qalarmcheck.js b/apps/qalarm/qalarmcheck.js index 9a3f10d5e..8dac43800 100644 --- a/apps/qalarm/qalarmcheck.js +++ b/apps/qalarm/qalarmcheck.js @@ -4,7 +4,10 @@ print("Checking for alarms..."); -clearInterval(); +if (Bangle.QALARM) { + clearInterval(Bangle.QALARM); + Bangle.QALARM = undefined; +} function getCurrentTime() { let time = new Date(); @@ -29,13 +32,13 @@ let nextAlarms = (require("Storage").readJSON("qalarm.json", 1) || []) .sort((a, b) => a.t - b.t); if (nextAlarms[0]) { - setTimeout(() => { + Bangle.QALARM = setTimeout(() => { eval(require("Storage").read("qalarmcheck.js")); load("qalarm.js"); }, nextAlarms[0].t - t); } else { // No alarms found: will re-check at midnight - setTimeout(() => { + Bangle.QALARM = setTimeout(() => { eval(require("Storage").read("qalarmcheck.js")); }, 86400000 - t); } diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog index 27b5421e8..f41fe3416 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -1,3 +1,5 @@ 0.01: First version 0.02: Add widget 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 \ No newline at end of file diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md index 033014789..535ae56e4 100644 --- a/apps/qmsched/README.md +++ b/apps/qmsched/README.md @@ -1,9 +1,14 @@ # Quiet Mode Schedule and Widget -Automatically turn Quiet Mode on or off at set times, and display a widget when enabled. +Automatically turn Quiet Mode on or off at set times, and display a widget when Quiet Mode is active. -### Edit Schedule: -![Main menu](screenshot_main.png) ![Edit Schedule menu](screenshot_edit.png) +| Bangle.js 1 | Bangle.js 2 | +|:---------------------------------------------:|:---------------------------------------------:| +| (widget: Silent mode) | (widget: Alarms mode) | +| ![Main menu](screenshot_b1_main.png) | ![Main menu](screenshot_b2_main.png) | +| ![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) | -### Widget: -![Widget, quiet mode: silent](screenshot_widget_silent.png) ![Widget, quiet mode: alarms](screenshot_widget_alarms.png) +### 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 c6377d4ba..7be3339fb 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -2,27 +2,74 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); const modeNames = ["Off", "Alarms", "Silent"]; -let scheds = require("Storage").readJSON("qmsched.json", 1); -/*scheds = [ - { hr : 6.5, // hours + minutes/60 - mode : 1, // quiet mode (0/1/2) - } -];*/ -if (!scheds) { - // set default schedule on first load of app - scheds = [ - {"hr": 8, "mode": 0}, - {"hr": 22, "mode": 1}, - ]; - require("Storage").writeJSON("qmsched.json", scheds); + +// load global brightness setting +let bSettings = require('Storage').readJSON('setting.json',true)||{}; +let current = 0|bSettings.quiet; +delete bSettings; // we don't need any other global settings + + + + + + +/** + * Save settings to qmsched.json + */ +function save() { + require('Storage').writeJSON('qmsched.json', settings); } -if (scheds.length && scheds.some(s => "last" in s)) { - // cleanup: remove "last" values (used by old versions) - scheds = scheds.map(s => { - delete s.last; - return s; - }); - require("Storage").writeJSON("qmsched.json", scheds); +function get(key, def) { + return (key in settings) ? settings[key] : def; +} +function set(key, val) { + settings[key] = val; save(); + scheds = settings.scheds; options = settings.options; // update references +} +function unset(key) { + delete settings[key]; save(); +} + +let settings, + scheds, options; // references for convenience +/** + * Load settings file, check if we need to migrate old setting formats to new + */ +function loadSettings() { + settings = require('Storage').readJSON("qmsched.json", true) || {}; + + if (Array.isArray(settings)) { + // migrate old file (plain array of schedules, qmOptions stored in global settings file) + require("Storage").erase("qmsched.json"); // need to erase old file, or Things Break, somehow... + let bOptions = require('Storage').readJSON('setting.json',true)||{}; + settings = { + options: bOptions.qmOptions || {}, + scheds: settings, + }; + // store new format + save(); + // and clean up qmOptions from global settings file + delete bOptions.qmOptions; + require('Storage').writeJSON('setting.json',bOptions); + } + // apply defaults + settings = Object.assign({ + options: {}, // Bangle options to override during quiet mode, default = none + scheds: [ + // default schedule: + {"hr": 8, "mode": 0}, + {"hr": 22, "mode": 1}, + ], + }, settings); + scheds = settings.scheds; options = settings.options; + + if (scheds.length && scheds.some(s => "last" in s)) { + // cleanup: remove "last" values (used by older versions) + set('scheds', scheds.map(s => { + delete s.last; + return s; + })); + } } function formatTime(t) { @@ -32,29 +79,35 @@ function formatTime(t) { } function showMainMenu() { - let menu = {"": {"title": "Quiet Mode"}}; + let _m, menu = { + "": {"title": "Quiet Mode"}, + "< Exit": () => load() + }; // "Current Mode""Silent" won't fit on Bangle.js 2 - menu["Current" + ((process.env.HWVERSION===2)?"":" Mode")]= { - value: (require("Storage").readJSON("setting.json", 1) || {}).quiet|0, + 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; }, }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { - const name = modeNames[sched.mode]; - const txt = formatTime(sched.hr)+" ".repeat(14-name.length)+name; - menu[txt] = function() { - showEditMenu(idx); + menu[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 + showEditMenu(idx); + } }; }); menu["Add Schedule"] = () => showEditMenu(-1); - menu["< Back"] = () => {load();}; - return E.showMenu(menu); + menu["LCD Settings"] = () => showOptionsMenu(); + _m = E.showMenu(menu); } function showEditMenu(index) { @@ -69,6 +122,7 @@ function showEditMenu(index) { } const menu = { "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, + "< Cancel": () => showMainMenu(), "Hours": { value: hrs, onchange: function(v) { @@ -110,18 +164,88 @@ function showEditMenu(index) { } else { scheds[index] = getSched(); } - require("Storage").writeJSON("qmsched.json", scheds); + save(); showMainMenu(); }; if (!isNew) { menu["> Delete"] = function() { scheds.splice(index, 1); - require("Storage").writeJSON("qmsched.json", scheds); + save(); showMainMenu(); }; } - menu["< Cancel"] = showMainMenu; return E.showMenu(menu); } +function showOptionsMenu() { + const disabledFormat = v => v ? "Off" : "-"; + function toggle(option) { + // we disable wakeOn* events by setting them to `false` in options + // not disabled = not present in options at all + if (option in options) { + delete options[option]; + } else { + options[option] = false; + } + save(); + } + let resetTimeout; + const oMenu = { + "": {"title": "LCD Settings"}, + "< Back": () => showMainMenu(), + "LCD Brightness": { + value: get("brightness", 0), + min: 0, // 0 = use default + max: 1, + step: 0.1, + format: v => (v>0.05) ? v : "-", + onchange: v => { + if (v>0.05) { // prevent v=0.000000000000001 bugs + set("brightness", v); + Bangle.setLCDBrightness(v); // show result, even if not quiet right now + // restore brightness after half a second + if (resetTimeout) clearTimeout(resetTimeout); + resetTimeout = setTimeout(() => { + resetTimeout = undefined; + require("qmsched").setMode(current); + }, 500); + } else { + unset("brightness"); + require("qmsched").setMode(current); + } + }, + }, + "LCD Timeout": { + value: get("timeout", 0), + min: 0, // 0 = use default (no constant on for quiet mode) + max: 60, + step: 5, + format: v => v>1 ? v : "-", + onchange: v => { + if (v>1) set("timeout", v); + else unset("timeout"); + }, + }, + // we disable wakeOn* events by overwriting them as false in options + // not disabled = not present in options at all + "Wake on FaceUp": { + value: "wakeOnFaceUp" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnFaceUp");}, + }, + "Wake on Touch": { + value: "wakeOnTouch" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnTouch");}, + }, + "Wake on Twist": { + value: "wakeOnTwist" in options, + format: disabledFormat, + onchange: () => {toggle("wakeOnTwist");}, + }, + }; + return E.showMenu(oMenu); +} + +loadSettings(); showMainMenu(); diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js index 2712cab30..c3bc49b58 100644 --- a/apps/qmsched/boot.js +++ b/apps/qmsched/boot.js @@ -1,7 +1,13 @@ // apply Quiet Mode schedules (function qm() { - let scheds = require("Storage").readJSON("qmsched.json", 1) || []; - if (!scheds.length) { return;} + let bSettings = require('Storage').readJSON('setting.json',true)||{}; + const curr = 0|bSettings.quiet; + delete bSettings; + if (curr) require("qmsched").applyOptions(curr); // no need to re-apply default options + + let settings = require('Storage').readJSON('qmsched.json',true)||{}; + let scheds = settings.scheds||[]; + if (!scheds.length) {return;} const now = new Date(), hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); // current (decimal) hour scheds.sort((a, b) => a.hr-b.hr); diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js index a3d36ed34..9b307769a 100644 --- a/apps/qmsched/lib.js +++ b/apps/qmsched/lib.js @@ -1,18 +1,23 @@ +/** + * Apply LCD options for given mode + * @param {int} mode Quiet Mode + */ +exports.applyOptions = function(mode) { + const s = require("Storage").readJSON(mode ? "qmsched.json" : "setting.json", 1) || {}; + const get = (k, d) => k in s ? s[k] : d; + Bangle.setOptions(get("options", {})); + Bangle.setLCDBrightness(get("brightness", 1)); + Bangle.setLCDTimeout(get("timeout", 10)); +}; /** * Set new Quiet Mode and apply Bangle options * @param {int} mode Quiet Mode */ exports.setMode = function(mode) { - let s = require("Storage").readJSON("setting.json", 1) || {}; - s.quiet = mode; - require("Storage").writeJSON("setting.json", s); - if (s.options) Bangle.setOptions(s.options); - if (mode && s.qmOptions) Bangle.setOptions(s.qmOptions); - if (mode && s.qmBrightness) { - if (s.qmBrightness!=1) Bangle.setLCDBrightness(s.qmBrightness); - } else { - if (s.brightness && s.brightness!=1) Bangle.setLCDBrightness(s.brightness); - } - if (mode && s.qmTimeout) Bangle.setLCDTimeout(s.qmTimeout); - if (typeof (WIDGETS)!=="undefined" && "qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} -}; \ No newline at end of file + require("Storage").writeJSON("setting.json", Object.assign( + require("Storage").readJSON("setting.json", 1) || {}, + {quiet:mode} + )); + exports.applyOptions(mode); + if (WIDGETS && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); +}; diff --git a/apps/qmsched/screenshot_b1_edit.png b/apps/qmsched/screenshot_b1_edit.png new file mode 100644 index 000000000..ec82e92e6 Binary files /dev/null and b/apps/qmsched/screenshot_b1_edit.png differ diff --git a/apps/qmsched/screenshot_b1_lcd.png b/apps/qmsched/screenshot_b1_lcd.png new file mode 100644 index 000000000..16f9356b8 Binary files /dev/null and b/apps/qmsched/screenshot_b1_lcd.png differ diff --git a/apps/qmsched/screenshot_b1_main.png b/apps/qmsched/screenshot_b1_main.png new file mode 100644 index 000000000..803ca69d5 Binary files /dev/null and b/apps/qmsched/screenshot_b1_main.png differ diff --git a/apps/qmsched/screenshot_b2_edit.png b/apps/qmsched/screenshot_b2_edit.png new file mode 100644 index 000000000..d26ff02cb Binary files /dev/null and b/apps/qmsched/screenshot_b2_edit.png differ diff --git a/apps/qmsched/screenshot_b2_lcd.png b/apps/qmsched/screenshot_b2_lcd.png new file mode 100644 index 000000000..3f06488c3 Binary files /dev/null and b/apps/qmsched/screenshot_b2_lcd.png differ diff --git a/apps/qmsched/screenshot_b2_main.png b/apps/qmsched/screenshot_b2_main.png new file mode 100644 index 000000000..f6d22a8b8 Binary files /dev/null and b/apps/qmsched/screenshot_b2_main.png differ diff --git a/apps/qmsched/screenshot_edit.png b/apps/qmsched/screenshot_edit.png deleted file mode 100644 index 88b7fcad4..000000000 Binary files a/apps/qmsched/screenshot_edit.png and /dev/null differ diff --git a/apps/qmsched/screenshot_main.png b/apps/qmsched/screenshot_main.png deleted file mode 100644 index 634abd633..000000000 Binary files a/apps/qmsched/screenshot_main.png and /dev/null differ diff --git a/apps/qmsched/screenshot_widget_alarms.png b/apps/qmsched/screenshot_widget_alarms.png deleted file mode 100644 index 52dbe2464..000000000 Binary files a/apps/qmsched/screenshot_widget_alarms.png and /dev/null differ diff --git a/apps/qmsched/screenshot_widget_silent.png b/apps/qmsched/screenshot_widget_silent.png deleted file mode 100644 index 38b133650..000000000 Binary files a/apps/qmsched/screenshot_widget_silent.png and /dev/null differ diff --git a/apps/qmsched/widget.js b/apps/qmsched/widget.js index c602288ad..b25192b06 100644 --- a/apps/qmsched/widget.js +++ b/apps/qmsched/widget.js @@ -1,32 +1,36 @@ -WIDGETS["qmsched"] = { - area: "tl", width: 24, draw: function() { - const mode = (require("Storage").readJSON("setting.json", 1) || {}).quiet|0; - if (mode===0) { // Off - if (this.width!==0) { - this.width = 0; - Bangle.drawWidgets(); +(function() { + WIDGETS["qmsched"] = { + area: "tl", + width: ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? 24 : 0, + draw: function() { + const mode = (require("Storage").readJSON("setting.json", 1) || {}).quiet|0; + if (mode===0) { // Off + if (this.width!==0) { + this.width = 0; + Bangle.drawWidgets(); + } + return; } - return; - } - // not Off: make sure width is correct - if (this.width!==24) { - this.width = 24; - Bangle.drawWidgets(); - return; // drawWidgets will call draw again - } - let x = this.x, y = this.y; - g.clearRect(x, y, x+23, y+23); - // quiet mode: draw dim red one-way-street sign - x = this.x+11;y = this.y+11; // center of widget - g.setColor(0.8, 0, 0).fillCircle(x, y, 8); - g.setColor(g.theme.bg).fillRect(x-6, y-2, x+6, y+2); - if (mode>1) {return;} // no alarms - // alarms still on: draw alarm icon in bottom-right corner - x = this.x+18;y = this.y+17; // center of alarm - g.setColor(1, 1, 0) - .fillCircle(x, y, 3) // alarm body - .fillRect(x-5, y+2, x+5, y+3) // bottom ridge - .fillRect(x-1, y-5, x+1, y+5).drawLine(x, y-6, x, y+6) // top+bottom - .drawLine(x+5, y-3, x+3, y-5).drawLine(x-5, y-3, x-3, y-5); // wriggles - }, -}; \ No newline at end of file + // not Off: make sure width is correct + if (this.width!==24) { + this.width = 24; + Bangle.drawWidgets(); + return; // drawWidgets will call draw again + } + let x = this.x, y = this.y; + g.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); + g.setColor(g.theme.bg).fillRect(x-6, y-2, x+6, y+2); + if (mode>1) {return;} // no alarms + // alarms still on: draw alarm icon in bottom-right corner + x = this.x+18;y = this.y+17; // center of alarm + g.setColor(1, 1, 0) + .fillCircle(x, y, 3) // alarm body + .fillRect(x-5, y+2, x+5, y+3) // bottom ridge + .fillRect(x-1, y-5, x+1, y+5).drawLine(x, y-6, x, y+6) // top+bottom + .drawLine(x+5, y-3, x+3, y-5).drawLine(x-5, y-3, x-3, y-5); // wriggles + }, + }; +})(); \ No newline at end of file diff --git a/apps/qrcode/ChangeLog b/apps/qrcode/ChangeLog index e2ae6b02a..edcc41cfd 100644 --- a/apps/qrcode/ChangeLog +++ b/apps/qrcode/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Add posibillity to generate Wifi code. +0.03: Forces integer scaling and adds more configuration (error correction, description, display) +0.04: Allow scanning of QR codes from camera or file diff --git a/apps/qrcode/custom.html b/apps/qrcode/custom.html index 618840da9..4920be655 100644 --- a/apps/qrcode/custom.html +++ b/apps/qrcode/custom.html @@ -3,56 +3,168 @@ - - - -
+ Datasource:
+ +
- - -

Wifi password:

-
- -
- -
+
+ +
+ +
+
+ +
+

Text/URL:

-
- - + +
+
+ +
+
+ +
+
+ +
+
+ Detected QR code: + None +
+ +
+ +
+ +
+ Detected QR code: + None +
+ +
+

Wifi name:

+

Wifi password:

+
+ +
+ +
+ +
+
+ + +
+
+ +
+

Try your QR Code:

+ +
+

Additional options:

+ +
+ +
+ +
+ +
+ + + +
+ +

Click

+ - + + diff --git a/apps/qrcode/qr-scanner-worker.min.js b/apps/qrcode/qr-scanner-worker.min.js new file mode 100644 index 000000000..0d026ac07 --- /dev/null +++ b/apps/qrcode/qr-scanner-worker.min.js @@ -0,0 +1,87 @@ +'use strict';(function(){function T(a,b){let c=[],d="";b=a.readBits([8,16,16][b]);for(let d=0;d`%${("0"+a.toString(16)).substr(-2)}`).join(""))}catch(e){}return{bytes:c,text:d}}function U(a,b){a=new V(a);let c=9>=b?0:26>=b?1:2;for(b={text:"",bytes:[],chunks:[],version:b};4<=a.available();){var d=a.readBits(4);if(d===t.Terminator)return b;if(d===t.ECI)0===a.readBits(1)?b.chunks.push({type:r.ECI,assignmentNumber:a.readBits(7)}): +0===a.readBits(1)?b.chunks.push({type:r.ECI,assignmentNumber:a.readBits(14)}):0===a.readBits(1)?b.chunks.push({type:r.ECI,assignmentNumber:a.readBits(21)}):b.chunks.push({type:r.ECI,assignmentNumber:-1});else if(d===t.Numeric){var e=a;d=[];for(var f="",g=e.readBits([10,12,14][c]);3<=g;){var h=e.readBits(10);if(1E3<=h)throw Error("Invalid numeric value above 999");var k=Math.floor(h/100),n=Math.floor(h/10)%10;h%=10;d.push(48+k,48+n,48+h);f+=k.toString()+n.toString()+h.toString();g-=3}if(2===g){g=e.readBits(7); +if(100<=g)throw Error("Invalid numeric value above 99");e=Math.floor(g/10);g%=10;d.push(48+e,48+g);f+=e.toString()+g.toString()}else if(1===g){e=e.readBits(4);if(10<=e)throw Error("Invalid numeric value above 9");d.push(48+e);f+=e.toString()}d={bytes:d,text:f};b.text+=d.text;b.bytes.push(...d.bytes);b.chunks.push({type:r.Numeric,text:d.text})}else if(d===t.Alphanumeric){e=a;d=[];f="";for(g=e.readBits([9,11,13][c]);2<=g;)n=e.readBits(11),k=Math.floor(n/45),n%=45,d.push(B[k].charCodeAt(0),B[n].charCodeAt(0)), +f+=B[k]+B[n],g-=2;1===g&&(e=e.readBits(6),d.push(B[e].charCodeAt(0)),f+=B[e]);d={bytes:d,text:f};b.text+=d.text;b.bytes.push(...d.bytes);b.chunks.push({type:r.Alphanumeric,text:d.text})}else if(d===t.Byte)d=T(a,c),b.text+=d.text,b.bytes.push(...d.bytes),b.chunks.push({type:r.Byte,bytes:d.bytes,text:d.text});else if(d===t.Kanji){f=a;d=[];e=f.readBits([8,10,12][c]);for(g=0;gk?k+33088:k+49472,d.push(k>>8,k&255);f=(new TextDecoder("shift-jis")).decode(Uint8Array.from(d)); +d={bytes:d,text:f};b.text+=d.text;b.bytes.push(...d.bytes);b.chunks.push({type:r.Kanji,bytes:d.bytes,text:d.text})}else d===t.StructuredAppend&&b.chunks.push({type:r.StructuredAppend,currentSequence:a.readBits(4),totalSequence:a.readBits(4),parity:a.readBits(8)})}if(0===a.available()||0===a.readBits(a.available()))return b}function J(a,b){return a^b}function W(a,b,c,d){b.degree()=d/2;){var g=b;let d=e;b=c;e=f;if(b.isZero())return null; +c=g;f=a.zero;g=b.getCoefficient(b.degree());for(g=a.inverse(g);c.degree()>=b.degree()&&!c.isZero();){let d=c.degree()-b.degree(),e=a.multiply(c.getCoefficient(c.degree()),g);f=f.addOrSubtract(a.buildMonomial(d,e));c=c.addOrSubtract(b.multiplyByMonomial(d,e))}f=f.multiplyPoly(e).addOrSubtract(d);if(c.degree()>=b.degree())return null}d=f.getCoefficient(0);if(0===d)return null;a=a.inverse(d);return[f.multiply(a),c.multiply(a)]}function X(a,b){let c=new Uint8ClampedArray(a.length);c.set(a);a=new Y(285, +256,0);var d=new w(a,c),e=new Uint8ClampedArray(b),f=!1;for(var g=0;gf)return null;c[f]^=d[e]}return c}function E(a,b){a^=b;for(b=0;a;)b++,a&=a-1;return b}function C(a,b){return b<<1|a}function Z(a,b,c){c=aa[c.dataMask];let d=a.height;var e=17+4*b.versionNumber,f=A.createEmpty(e,e);f.setRegion(0,0,9,9,!0);f.setRegion(e-8,0,8,9,!0);f.setRegion(0,e-8,9,8,!0);for(var g of b.alignmentPatternCenters)for(var h of b.alignmentPatternCenters)6=== +g&&6===h||6===g&&h===e-7||g===e-7&&6===h||f.setRegion(g-2,h-2,5,5,!0);f.setRegion(6,9,1,e-17,!0);f.setRegion(9,6,e-17,1,!0);6d;d++){let f=k-d;if(!b.get(f,m)){e++;let b=a.get(f,m);c({y:m,x:f})&&(b=!b);h=h<<1|b;8===e&&(g.push(h),h=e=0)}}}f=!f}return g}function ba(a){var b=a.height,c=Math.floor((b-17)/4);if(6>=c)return K[c- +1];c=0;for(var d=5;0<=d;d--)for(var e=b-9;e>=b-11;e--)c=C(a.get(e,d),c);d=0;for(e=5;0<=e;e--)for(let c=b-9;c>=b-11;c--)d=C(a.get(e,c),d);a=Infinity;let f;for(let e of K){if(e.infoBits===c||e.infoBits===d)return e;b=E(c,e.infoBits);b=a)return f}function ca(a){let b=0;for(var c=0;8>=c;c++)6!==c&&(b=C(a.get(c,8),b));for(c=7;0<=c;c--)6!==c&&(b=C(a.get(8,c),b));var d=a.height;c=0;for(var e=d-1;e>=d-7;e--)c=C(a.get(8,e),c);for(e=d-8;e=a?d:null}function ea(a,b,c){let d=b.errorCorrectionLevels[c],e=[],f=0;d.ecBlocks.forEach(a=>{for(let b=0;ba+b.numDataCodewords,0);c=new Uint8ClampedArray(c);a=0;for(let b of d){d=X(b.codewords,b.codewords.length-b.numDataCodewords);if(!d)return null;for(let e= +0;e{const c=g*a+n*b+p;return{x:(e*a+h*b+m)/c,y:(f*a+k*b+l)/c}};for(let e=0;ea+c)}function ia(a,b,c){let d=y(a,b),e=y(b,c),f=y(a,c),g,h,k;e>=d&&e>=f?[g,h,k]=[b,a,c]:f>=e&&f>=d?[g,h,k]=[a,b,c]:[g,h,k]=[a,c,b];0>(k.x-h.x)*(g.y-h.y)-(k.y-h.y)*(g.x-h.x)&&([g,k]=[k,g]);return{bottomLeft:g,topLeft:h,topRight:k}}function ja(a,b,c,d){d=(x(z(a,c,d,5))/7+x(z(a,b,d,5))/7+x(z(c,a,d,5))/7+x(z(b,a,d,5))/7)/4;if(1>d)throw Error("Invalid module size");b=Math.round(y(a,b)/d);a=Math.round(y(a,c)/d);a= +Math.floor((b+a)/2)+7;switch(a%4){case 0:a++;break;case 2:a--}return{dimension:a,moduleSize:d}}function N(a,b,c,d){let e=[{x:Math.floor(a.x),y:Math.floor(a.y)}];var f=Math.abs(b.y-a.y)>Math.abs(b.x-a.x);if(f){var g=Math.floor(a.y);var h=Math.floor(a.x);a=Math.floor(b.y);b=Math.floor(b.x)}else g=Math.floor(a.x),h=Math.floor(a.y),a=Math.floor(b.x),b=Math.floor(b.y);let k=Math.abs(a-g),n=Math.abs(b-h),m=Math.floor(-k/2),l=g{d+=Math.pow(a[f]-b*c,2)});return{averageSize:c,error:d}}function O(a,b,c){try{let d=z(a,{x:-1,y:a.y},c,b.length), +e=z(a,{x:a.x,y:-1},c,b.length),f=z(a,{x:Math.max(0,a.x-a.y)-1,y:Math.max(0,a.y-a.x)-1},c,b.length),g=z(a,{x:Math.min(c.width,a.x+a.y)+1,y:Math.min(c.height,a.y+a.x)+1},c,b.length),h=F(d,b),k=F(e,b),n=F(f,b),m=F(g,b),l=(h.averageSize+k.averageSize+n.averageSize+m.averageSize)/4;return Math.sqrt(h.error*h.error+k.error*k.error+n.error*n.error+m.error*m.error)+(Math.pow(h.averageSize-l,2)+Math.pow(k.averageSize-l,2)+Math.pow(n.averageSize-l,2)+Math.pow(m.averageSize-l,2))/l}catch(d){return Infinity}} +function H(a,b){for(var c=Math.round(b.x);a.get(c,Math.round(b.y));)c--;for(var d=Math.round(b.x);a.get(d,Math.round(b.y));)d++;c=(c+d)/2;for(d=Math.round(b.y);a.get(Math.round(c),d);)d--;for(b=Math.round(b.y);a.get(Math.round(c),b);)b++;return{x:c,y:(d+b)/2}}function ka(a){var b=[],c=[];let d=[];var e=[];for(let m=0;m<=a.height;m++){var f=0,g=!1;let l=[0,0,0,0,0];for(let b=-1;b<=a.width;b++){var h=a.get(b,m);if(h===g)f++;else{l=[l[1],l[2],l[3],l[4],f];f=1;g=h;var k=x(l)/7;k=Math.abs(l[0]-k)d>=b.bottom.startX&&d<=b.bottom.endX||a>=b.bottom.startX&&d<=b.bottom.endX||d<=b.bottom.startX&&a>=b.bottom.endX&&1.5>l[2]/(b.bottom.endX-b.bottom.startX)&&.5c>=b.bottom.startX&&c<=b.bottom.endX||a>=b.bottom.startX&&c<=b.bottom.endX||c<=b.bottom.startX&&a>=b.bottom.endX&&1.5>l[2]/(b.bottom.endX-b.bottom.startX)&&.5a.bottom.y!==m&&2<=a.bottom.y-a.top.y));c=c.filter(a=>a.bottom.y===m);d.push(...e.filter(a=>a.bottom.y!==m));e=e.filter(a=>a.bottom.y===m)}b.push(...c.filter(a=>2<=a.bottom.y-a.top.y));d.push(...e); +c=[];for(var m of b)2>m.bottom.y-m.top.y||(b=(m.top.startX+m.top.endX+m.bottom.startX+m.bottom.endX)/4,e=(m.top.y+m.bottom.y+1)/2,a.get(Math.round(b),Math.round(e))&&(f=[m.top.endX-m.top.startX,m.bottom.endX-m.bottom.startX,m.bottom.y-m.top.y+1],f=x(f)/f.length,g=O({x:Math.round(b),y:Math.round(e)},[1,1,3,1,1],a),c.push({score:g,x:b,y:e,size:f})));if(3>c.length)return null;c.sort((a,b)=>a.score-b.score);m=[];for(b=0;ba.score-b.score);m.push({points:[e,f[0],f[1]],score:e.score+f[0].score+f[1].score})}m.sort((a,b)=>a.score-b.score);let {topRight:p,topLeft:q,bottomLeft:v}=ia(...m[0].points);m=P(a,d,p,q,v);l=[];m&&l.push({alignmentPattern:{x:m.alignmentPattern.x,y:m.alignmentPattern.y},bottomLeft:{x:v.x,y:v.y},dimension:m.dimension,topLeft:{x:q.x,y:q.y},topRight:{x:p.x,y:p.y}});m=H(a,p);b=H(a,q);c=H(a,v);(a=P(a,d,m,b,c))&&l.push({alignmentPattern:{x:a.alignmentPattern.x, +y:a.alignmentPattern.y},bottomLeft:{x:c.x,y:c.y},topLeft:{x:b.x,y:b.y},topRight:{x:m.x,y:m.y},dimension:a.dimension});return 0===l.length?null:l}function P(a,b,c,d,e){let f,g;try{({dimension:f,moduleSize:g}=ja(d,c,e,a))}catch(m){return null}var h=c.x-d.x+e.x,k=c.y-d.y+e.y;c=(y(d,e)+y(d,c))/2/g;e=1-3/c;let n={x:d.x+e*(h-d.x),y:d.y+e*(k-d.y)};b=b.map(b=>{const c=(b.top.startX+b.top.endX+b.bottom.startX+b.bottom.endX)/4;b=(b.top.y+b.bottom.y+1)/2;if(a.get(Math.floor(c),Math.floor(b))){var d=O({x:Math.floor(c), +y:Math.floor(b)},[1,1,1],a)+y({x:c,y:b},n);return{x:c,y:b,score:d}}}).filter(a=>!!a).sort((a,b)=>a.score-b.score);return{alignmentPattern:15<=c&&b.length?b[0]:n,dimension:f}}function Q(a){var b=ka(a);if(!b)return null;for(let e of b){b=ha(a,e);var c=b.matrix;if(null==c)c=null;else{var d=L(c);if(d)c=d;else{for(d=0;d{a[c]=b[c]})}function I(a,b,c,d={}){let e=Object.create(null);R(e,la);R(e,d); +d="onlyInvert"===e.inversionAttempts||"invertFirst"===e.inversionAttempts;var f="attemptBoth"===e.inversionAttempts||d;var g=e.greyScaleWeights,h=e.canOverwriteImage,k=b*c;if(a.length!==4*k)throw Error("Malformed data passed to binarizer.");var n=0;if(h){var m=new Uint8ClampedArray(a.buffer,n,k);n+=k}m=new S(b,c,m);if(g.useIntegerApproximation)for(var l=0;l>8)}else for(l=0;lt;t++)for(let a=0;8>a;a++){let b=m.get(8*q+a,8*p+t);u=Math.min(u,b);r=Math.max(r,b)}t=(u+r)/2;t=Math.min(255,1.11*t);24>=r-u&&(t=u/2,0a?2:a>c?c:a;h=l-3;h=2>b?2:b>h?h:b;k=0;for(n=-2;2>=n;n++)for(u=-2;2>=u;u++)k+=v.get(c+n,h+u);c=k/25;for(h=0;8>h;h++)for(k=0;8>k;k++)n=8*a+h,u=8*b+k,r=m.get(n,u),p.set(n,u,r<=c),f&&q.set(n,u,!(r<=c))}f=f?{binarized:p,inverted:q}:{binarized:p};let {binarized:w,inverted:x}=f;(f=Q(d?x:w))||"attemptBoth"!==e.inversionAttempts&&"invertFirst"!==e.inversionAttempts|| +(f=Q(d?w:x));return f}class A{constructor(a,b){this.width=b;this.height=a.length/b;this.data=a}static createEmpty(a,b){return new A(new Uint8ClampedArray(a*b),a)}get(a,b){return 0>a||a>=this.width||0>b||b>=this.height?!1:!!this.data[b*this.width+a]}set(a,b,c){this.data[b*this.width+a]=c?1:0}setRegion(a,b,c,d,e){for(let f=b;fa||32this.available())throw Error("Cannot read "+a.toString()+" bits");var b=0;if(0>8-c<>b;a-=c;this.bitOffset+=c;8===this.bitOffset&&(this.bitOffset=0,this.byteOffset++)}if(0>c<>c,this.bitOffset+=a)}return b}available(){return 8*(this.bytes.length-this.byteOffset)-this.bitOffset}}var r;(function(a){a.Numeric="numeric";a.Alphanumeric="alphanumeric";a.Byte="byte";a.Kanji="kanji";a.ECI="eci";a.StructuredAppend="structuredappend"})(r||(r={}));var t;(function(a){a[a.Terminator=0]="Terminator";a[a.Numeric=1]="Numeric";a[a.Alphanumeric=2]="Alphanumeric";a[a.Byte=4]="Byte";a[a.Kanji=8]="Kanji";a[a.ECI=7]="ECI";a[a.StructuredAppend= +3]="StructuredAppend"})(t||(t={}));let B="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");class w{constructor(a,b){if(0===b.length)throw Error("No coefficients.");this.field=a;let c=b.length;if(1a.length&&([b,a]=[a,b]);let c=new Uint8ClampedArray(a.length),d=a.length-b.length;for(var e=0;ea)throw Error("Invalid degree less than 0");if(0===b)return this.field.zero; +let c=this.coefficients.length;a=new Uint8ClampedArray(c+a);for(let d=0;d{b^=a}),b;b=this.coefficients[0];for(let d=1;d=this.size&&(a=(a^this.primitive)&this.size-1);for(a=0;aa)throw Error("Invalid monomial degree less than 0");if(0===b)return this.zero;a=new Uint8ClampedArray(a+1);a[0]=b;return new w(this,a)}log(a){if(0===a)throw Error("Can't take log(0)");return this.logTable[a]}exp(a){return this.expTable[a]}}let K=[{infoBits:null,versionNumber:1,alignmentPatternCenters:[],errorCorrectionLevels:[{ecCodewordsPerBlock:7,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:10,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:16}]},{ecCodewordsPerBlock:13, +ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:13}]},{ecCodewordsPerBlock:17,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:9}]}]},{infoBits:null,versionNumber:2,alignmentPatternCenters:[6,18],errorCorrectionLevels:[{ecCodewordsPerBlock:10,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:34}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:28}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:22}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:16}]}]}, +{infoBits:null,versionNumber:3,alignmentPatternCenters:[6,22],errorCorrectionLevels:[{ecCodewordsPerBlock:15,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:55}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:13}]}]},{infoBits:null,versionNumber:4,alignmentPatternCenters:[6,26],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:1, +dataCodewordsPerBlock:80}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:32}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:9}]}]},{infoBits:null,versionNumber:5,alignmentPatternCenters:[6,30],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:43}]},{ecCodewordsPerBlock:18, +ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:15},{numBlocks:2,dataCodewordsPerBlock:16}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:11},{numBlocks:2,dataCodewordsPerBlock:12}]}]},{infoBits:null,versionNumber:6,alignmentPatternCenters:[6,34],errorCorrectionLevels:[{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:68}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:27}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:19}]}, +{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:15}]}]},{infoBits:31892,versionNumber:7,alignmentPatternCenters:[6,22,38],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:78}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:31}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:4,dataCodewordsPerBlock:15}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:13}, +{numBlocks:1,dataCodewordsPerBlock:14}]}]},{infoBits:34236,versionNumber:8,alignmentPatternCenters:[6,24,42],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:97}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:38},{numBlocks:2,dataCodewordsPerBlock:39}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:18},{numBlocks:2,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:14}, +{numBlocks:2,dataCodewordsPerBlock:15}]}]},{infoBits:39577,versionNumber:9,alignmentPatternCenters:[6,26,46],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:36},{numBlocks:2,dataCodewordsPerBlock:37}]},{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:16},{numBlocks:4,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:12}, +{numBlocks:4,dataCodewordsPerBlock:13}]}]},{infoBits:42195,versionNumber:10,alignmentPatternCenters:[6,28,50],errorCorrectionLevels:[{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:68},{numBlocks:2,dataCodewordsPerBlock:69}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:43},{numBlocks:1,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:19},{numBlocks:2,dataCodewordsPerBlock:20}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6, +dataCodewordsPerBlock:15},{numBlocks:2,dataCodewordsPerBlock:16}]}]},{infoBits:48118,versionNumber:11,alignmentPatternCenters:[6,30,54],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:81}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:50},{numBlocks:4,dataCodewordsPerBlock:51}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:22},{numBlocks:4,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:3, +dataCodewordsPerBlock:12},{numBlocks:8,dataCodewordsPerBlock:13}]}]},{infoBits:51042,versionNumber:12,alignmentPatternCenters:[6,32,58],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:92},{numBlocks:2,dataCodewordsPerBlock:93}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:36},{numBlocks:2,dataCodewordsPerBlock:37}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:20},{numBlocks:6,dataCodewordsPerBlock:21}]}, +{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:14},{numBlocks:4,dataCodewordsPerBlock:15}]}]},{infoBits:55367,versionNumber:13,alignmentPatternCenters:[6,34,62],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:107}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:37},{numBlocks:1,dataCodewordsPerBlock:38}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:20},{numBlocks:4,dataCodewordsPerBlock:21}]}, +{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:11},{numBlocks:4,dataCodewordsPerBlock:12}]}]},{infoBits:58893,versionNumber:14,alignmentPatternCenters:[6,26,46,66],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:115},{numBlocks:1,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:40},{numBlocks:5,dataCodewordsPerBlock:41}]},{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:16}, +{numBlocks:5,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:12},{numBlocks:5,dataCodewordsPerBlock:13}]}]},{infoBits:63784,versionNumber:15,alignmentPatternCenters:[6,26,48,70],errorCorrectionLevels:[{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:87},{numBlocks:1,dataCodewordsPerBlock:88}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:41},{numBlocks:5,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:30, +ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:24},{numBlocks:7,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:12},{numBlocks:7,dataCodewordsPerBlock:13}]}]},{infoBits:68472,versionNumber:16,alignmentPatternCenters:[6,26,50,74],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:98},{numBlocks:1,dataCodewordsPerBlock:99}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:45},{numBlocks:3,dataCodewordsPerBlock:46}]}, +{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:19},{numBlocks:2,dataCodewordsPerBlock:20}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:15},{numBlocks:13,dataCodewordsPerBlock:16}]}]},{infoBits:70749,versionNumber:17,alignmentPatternCenters:[6,30,54,78],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:107},{numBlocks:5,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:46}, +{numBlocks:1,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:22},{numBlocks:15,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:17,dataCodewordsPerBlock:15}]}]},{infoBits:76311,versionNumber:18,alignmentPatternCenters:[6,30,56,82],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:120},{numBlocks:1,dataCodewordsPerBlock:121}]},{ecCodewordsPerBlock:26, +ecBlocks:[{numBlocks:9,dataCodewordsPerBlock:43},{numBlocks:4,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:22},{numBlocks:1,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:19,dataCodewordsPerBlock:15}]}]},{infoBits:79154,versionNumber:19,alignmentPatternCenters:[6,30,58,86],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:113},{numBlocks:4, +dataCodewordsPerBlock:114}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:44},{numBlocks:11,dataCodewordsPerBlock:45}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:21},{numBlocks:4,dataCodewordsPerBlock:22}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:9,dataCodewordsPerBlock:13},{numBlocks:16,dataCodewordsPerBlock:14}]}]},{infoBits:84390,versionNumber:20,alignmentPatternCenters:[6,34,62,90],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3, +dataCodewordsPerBlock:107},{numBlocks:5,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:41},{numBlocks:13,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:24},{numBlocks:5,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:15},{numBlocks:10,dataCodewordsPerBlock:16}]}]},{infoBits:87683,versionNumber:21,alignmentPatternCenters:[6,28,50,72,94],errorCorrectionLevels:[{ecCodewordsPerBlock:28, +ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:116},{numBlocks:4,dataCodewordsPerBlock:117}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:22},{numBlocks:6,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:16},{numBlocks:6,dataCodewordsPerBlock:17}]}]},{infoBits:92361,versionNumber:22,alignmentPatternCenters:[6,26,50,74,98],errorCorrectionLevels:[{ecCodewordsPerBlock:28, +ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:111},{numBlocks:7,dataCodewordsPerBlock:112}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:24},{numBlocks:16,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:34,dataCodewordsPerBlock:13}]}]},{infoBits:96236,versionNumber:23,alignmentPatternCenters:[6,30,54,74,102],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4, +dataCodewordsPerBlock:121},{numBlocks:5,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:47},{numBlocks:14,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:16,dataCodewordsPerBlock:15},{numBlocks:14,dataCodewordsPerBlock:16}]}]},{infoBits:102084,versionNumber:24,alignmentPatternCenters:[6,28,54,80,106],errorCorrectionLevels:[{ecCodewordsPerBlock:30, +ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:117},{numBlocks:4,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:45},{numBlocks:14,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:24},{numBlocks:16,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:30,dataCodewordsPerBlock:16},{numBlocks:2,dataCodewordsPerBlock:17}]}]},{infoBits:102881,versionNumber:25,alignmentPatternCenters:[6, +32,58,84,110],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:106},{numBlocks:4,dataCodewordsPerBlock:107}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:47},{numBlocks:13,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:24},{numBlocks:22,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:15},{numBlocks:13,dataCodewordsPerBlock:16}]}]}, +{infoBits:110507,versionNumber:26,alignmentPatternCenters:[6,30,58,86,114],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:114},{numBlocks:2,dataCodewordsPerBlock:115}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:46},{numBlocks:4,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:28,dataCodewordsPerBlock:22},{numBlocks:6,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:33,dataCodewordsPerBlock:16}, +{numBlocks:4,dataCodewordsPerBlock:17}]}]},{infoBits:110734,versionNumber:27,alignmentPatternCenters:[6,34,62,90,118],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:122},{numBlocks:4,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:45},{numBlocks:3,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:23},{numBlocks:26,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:30, +ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:15},{numBlocks:28,dataCodewordsPerBlock:16}]}]},{infoBits:117786,versionNumber:28,alignmentPatternCenters:[6,26,50,74,98,122],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:117},{numBlocks:10,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:45},{numBlocks:23,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:24},{numBlocks:31, +dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:15},{numBlocks:31,dataCodewordsPerBlock:16}]}]},{infoBits:119615,versionNumber:29,alignmentPatternCenters:[6,30,54,78,102,126],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:116},{numBlocks:7,dataCodewordsPerBlock:117}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:21,dataCodewordsPerBlock:45},{numBlocks:7,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30, +ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:23},{numBlocks:37,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:15},{numBlocks:26,dataCodewordsPerBlock:16}]}]},{infoBits:126325,versionNumber:30,alignmentPatternCenters:[6,26,52,78,104,130],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:115},{numBlocks:10,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:47}, +{numBlocks:10,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:24},{numBlocks:25,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:23,dataCodewordsPerBlock:15},{numBlocks:25,dataCodewordsPerBlock:16}]}]},{infoBits:127568,versionNumber:31,alignmentPatternCenters:[6,30,56,82,108,134],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:115},{numBlocks:3,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28, +ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:46},{numBlocks:29,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:42,dataCodewordsPerBlock:24},{numBlocks:1,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:23,dataCodewordsPerBlock:15},{numBlocks:28,dataCodewordsPerBlock:16}]}]},{infoBits:133589,versionNumber:32,alignmentPatternCenters:[6,34,60,86,112,138],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:115}]}, +{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:46},{numBlocks:23,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:24},{numBlocks:35,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:15},{numBlocks:35,dataCodewordsPerBlock:16}]}]},{infoBits:136944,versionNumber:33,alignmentPatternCenters:[6,30,58,86,114,142],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:115}, +{numBlocks:1,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:14,dataCodewordsPerBlock:46},{numBlocks:21,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:29,dataCodewordsPerBlock:24},{numBlocks:19,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:15},{numBlocks:46,dataCodewordsPerBlock:16}]}]},{infoBits:141498,versionNumber:34,alignmentPatternCenters:[6,34,62,90,118,146],errorCorrectionLevels:[{ecCodewordsPerBlock:30, +ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:115},{numBlocks:6,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:14,dataCodewordsPerBlock:46},{numBlocks:23,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:44,dataCodewordsPerBlock:24},{numBlocks:7,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:59,dataCodewordsPerBlock:16},{numBlocks:1,dataCodewordsPerBlock:17}]}]},{infoBits:145311,versionNumber:35,alignmentPatternCenters:[6, +30,54,78,102,126,150],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:121},{numBlocks:7,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:47},{numBlocks:26,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:39,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:15},{numBlocks:41,dataCodewordsPerBlock:16}]}]}, +{infoBits:150283,versionNumber:36,alignmentPatternCenters:[6,24,50,76,102,128,154],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:121},{numBlocks:14,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:47},{numBlocks:34,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:46,dataCodewordsPerBlock:24},{numBlocks:10,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:2, +dataCodewordsPerBlock:15},{numBlocks:64,dataCodewordsPerBlock:16}]}]},{infoBits:152622,versionNumber:37,alignmentPatternCenters:[6,28,54,80,106,132,158],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:122},{numBlocks:4,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:29,dataCodewordsPerBlock:46},{numBlocks:14,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:49,dataCodewordsPerBlock:24},{numBlocks:10,dataCodewordsPerBlock:25}]}, +{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:24,dataCodewordsPerBlock:15},{numBlocks:46,dataCodewordsPerBlock:16}]}]},{infoBits:158308,versionNumber:38,alignmentPatternCenters:[6,32,58,84,110,136,162],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:122},{numBlocks:18,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:46},{numBlocks:32,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:48, +dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:42,dataCodewordsPerBlock:15},{numBlocks:32,dataCodewordsPerBlock:16}]}]},{infoBits:161089,versionNumber:39,alignmentPatternCenters:[6,26,54,82,110,138,166],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:20,dataCodewordsPerBlock:117},{numBlocks:4,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:40,dataCodewordsPerBlock:47},{numBlocks:7,dataCodewordsPerBlock:48}]}, +{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:43,dataCodewordsPerBlock:24},{numBlocks:22,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:15},{numBlocks:67,dataCodewordsPerBlock:16}]}]},{infoBits:167017,versionNumber:40,alignmentPatternCenters:[6,30,58,86,114,142,170],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:118},{numBlocks:6,dataCodewordsPerBlock:119}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:18, +dataCodewordsPerBlock:47},{numBlocks:31,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:34,dataCodewordsPerBlock:24},{numBlocks:34,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:20,dataCodewordsPerBlock:15},{numBlocks:61,dataCodewordsPerBlock:16}]}]}],da=[{bits:21522,formatInfo:{errorCorrectionLevel:1,dataMask:0}},{bits:20773,formatInfo:{errorCorrectionLevel:1,dataMask:1}},{bits:24188,formatInfo:{errorCorrectionLevel:1,dataMask:2}},{bits:23371,formatInfo:{errorCorrectionLevel:1, +dataMask:3}},{bits:17913,formatInfo:{errorCorrectionLevel:1,dataMask:4}},{bits:16590,formatInfo:{errorCorrectionLevel:1,dataMask:5}},{bits:20375,formatInfo:{errorCorrectionLevel:1,dataMask:6}},{bits:19104,formatInfo:{errorCorrectionLevel:1,dataMask:7}},{bits:30660,formatInfo:{errorCorrectionLevel:0,dataMask:0}},{bits:29427,formatInfo:{errorCorrectionLevel:0,dataMask:1}},{bits:32170,formatInfo:{errorCorrectionLevel:0,dataMask:2}},{bits:30877,formatInfo:{errorCorrectionLevel:0,dataMask:3}},{bits:26159, +formatInfo:{errorCorrectionLevel:0,dataMask:4}},{bits:25368,formatInfo:{errorCorrectionLevel:0,dataMask:5}},{bits:27713,formatInfo:{errorCorrectionLevel:0,dataMask:6}},{bits:26998,formatInfo:{errorCorrectionLevel:0,dataMask:7}},{bits:5769,formatInfo:{errorCorrectionLevel:3,dataMask:0}},{bits:5054,formatInfo:{errorCorrectionLevel:3,dataMask:1}},{bits:7399,formatInfo:{errorCorrectionLevel:3,dataMask:2}},{bits:6608,formatInfo:{errorCorrectionLevel:3,dataMask:3}},{bits:1890,formatInfo:{errorCorrectionLevel:3, +dataMask:4}},{bits:597,formatInfo:{errorCorrectionLevel:3,dataMask:5}},{bits:3340,formatInfo:{errorCorrectionLevel:3,dataMask:6}},{bits:2107,formatInfo:{errorCorrectionLevel:3,dataMask:7}},{bits:13663,formatInfo:{errorCorrectionLevel:2,dataMask:0}},{bits:12392,formatInfo:{errorCorrectionLevel:2,dataMask:1}},{bits:16177,formatInfo:{errorCorrectionLevel:2,dataMask:2}},{bits:14854,formatInfo:{errorCorrectionLevel:2,dataMask:3}},{bits:9396,formatInfo:{errorCorrectionLevel:2,dataMask:4}},{bits:8579,formatInfo:{errorCorrectionLevel:2, +dataMask:5}},{bits:11994,formatInfo:{errorCorrectionLevel:2,dataMask:6}},{bits:11245,formatInfo:{errorCorrectionLevel:2,dataMask:7}}],aa=[a=>0===(a.y+a.x)%2,a=>0===a.y%2,a=>0===a.x%3,a=>0===(a.y+a.x)%3,a=>0===(Math.floor(a.y/2)+Math.floor(a.x/3))%2,a=>0===a.x*a.y%2+a.x*a.y%3,a=>0===(a.y*a.x%2+a.y*a.x%3)%2,a=>0===((a.y+a.x)%2+a.y*a.x%3)%2],y=(a,b)=>Math.sqrt(Math.pow(b.x-a.x,2)+Math.pow(b.y-a.y,2)),la={inversionAttempts:"attemptBoth",greyScaleWeights:{red:.2126,green:.7152,blue:.0722,useIntegerApproximation:!1}, +canOverwriteImage:!0};I.default=I;let G="dontInvert",D={red:77,green:150,blue:29,useIntegerApproximation:!0};self.onmessage=a=>{let b=a.data.data;switch(a.data.type){case "decode":a=I(b.data,b.width,b.height,{inversionAttempts:G,greyScaleWeights:D});self.postMessage({type:"qrResult",data:a?a.data:null});break;case "grayscaleWeights":D.red=b.red;D.green=b.green;D.blue=b.blue;D.useIntegerApproximation=b.useIntegerApproximation;break;case "inversionMode":switch(b){case "original":G="dontInvert";break; +case "invert":G="onlyInvert";break;case "both":G="attemptBoth";break;default:throw Error("Invalid inversion mode");}break;case "close":self.close()}}})() +//# sourceMappingURL=qr-scanner-worker.min.js.map diff --git a/apps/qrcode/qr-scanner-worker.min.js.map b/apps/qrcode/qr-scanner-worker.min.js.map new file mode 100644 index 000000000..dbf29b5cd --- /dev/null +++ b/apps/qrcode/qr-scanner-worker.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"qr-scanner-worker.min.js","sources":["node_modules/jsqr-es6/src/decoder/decodeData/index.ts","node_modules/jsqr-es6/src/decoder/reedsolomon/GenericGF.ts","node_modules/jsqr-es6/src/decoder/reedsolomon/index.ts","node_modules/jsqr-es6/src/decoder/decoder.ts","node_modules/jsqr-es6/src/extractor/index.ts","node_modules/jsqr-es6/src/locator/index.ts","node_modules/jsqr-es6/src/index.ts","node_modules/jsqr-es6/src/binarizer/index.ts","node_modules/jsqr-es6/src/BitMatrix.ts","node_modules/jsqr-es6/src/decoder/decodeData/BitStream.ts","node_modules/jsqr-es6/src/decoder/reedsolomon/GenericGFPoly.ts","node_modules/jsqr-es6/src/decoder/version.ts","src/worker.js"],"sourcesContent":["// tslint:disable:no-bitwise\nimport { BitStream } from \"./BitStream\";\n\nexport interface Chunk {\n type: Mode;\n text: string;\n}\n\nexport interface ByteChunk {\n type: Mode.Byte | Mode.Kanji;\n bytes: number[];\n}\n\nexport interface ECIChunk {\n type: Mode.ECI;\n assignmentNumber: number;\n}\n\nexport interface StructuredAppend {\n type: Mode.StructuredAppend;\n currentSequence: number;\n totalSequence: number;\n parity: number;\n}\n\nexport type Chunks = Array;\n\nexport interface DecodedQR {\n text: string;\n bytes: number[];\n chunks: Chunks;\n version: number;\n}\n\nexport enum Mode {\n Numeric = \"numeric\",\n Alphanumeric = \"alphanumeric\",\n Byte = \"byte\",\n Kanji = \"kanji\",\n ECI = \"eci\",\n StructuredAppend = \"structuredappend\",\n}\n\nenum ModeByte {\n Terminator = 0x0,\n Numeric = 0x1,\n Alphanumeric = 0x2,\n Byte = 0x4,\n Kanji = 0x8,\n ECI = 0x7,\n StructuredAppend = 0x3,\n // FNC1FirstPosition = 0x5,\n // FNC1SecondPosition = 0x9,\n}\n\nfunction decodeNumeric(stream: BitStream, size: number) {\n const bytes: number[] = [];\n let text = \"\";\n\n const characterCountSize = [10, 12, 14][size];\n let length = stream.readBits(characterCountSize);\n // Read digits in groups of 3\n while (length >= 3) {\n const num = stream.readBits(10);\n if (num >= 1000) {\n throw new Error(\"Invalid numeric value above 999\");\n }\n\n const a = Math.floor(num / 100);\n const b = Math.floor(num / 10) % 10;\n const c = num % 10;\n\n bytes.push(48 + a, 48 + b, 48 + c);\n text += a.toString() + b.toString() + c.toString();\n length -= 3;\n }\n\n // If the number of digits aren't a multiple of 3, the remaining digits are special cased.\n if (length === 2) {\n const num = stream.readBits(7);\n if (num >= 100) {\n throw new Error(\"Invalid numeric value above 99\");\n }\n\n const a = Math.floor(num / 10);\n const b = num % 10;\n\n bytes.push(48 + a, 48 + b);\n text += a.toString() + b.toString();\n } else if (length === 1) {\n const num = stream.readBits(4);\n if (num >= 10) {\n throw new Error(\"Invalid numeric value above 9\");\n }\n\n bytes.push(48 + num);\n text += num.toString();\n }\n\n return { bytes, text };\n}\n\nconst AlphanumericCharacterCodes = [\n \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\",\n \"9\", \"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\",\n \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\", \"P\", \"Q\",\n \"R\", \"S\", \"T\", \"U\", \"V\", \"W\", \"X\", \"Y\", \"Z\",\n \" \", \"$\", \"%\", \"*\", \"+\", \"-\", \".\", \"/\", \":\",\n];\n\nfunction decodeAlphanumeric(stream: BitStream, size: number) {\n const bytes: number[] = [];\n let text = \"\";\n\n const characterCountSize = [9, 11, 13][size];\n let length = stream.readBits(characterCountSize);\n while (length >= 2) {\n const v = stream.readBits(11);\n\n const a = Math.floor(v / 45);\n const b = v % 45;\n\n bytes.push(AlphanumericCharacterCodes[a].charCodeAt(0), AlphanumericCharacterCodes[b].charCodeAt(0));\n text += AlphanumericCharacterCodes[a] + AlphanumericCharacterCodes[b];\n length -= 2;\n }\n\n if (length === 1) {\n const a = stream.readBits(6);\n bytes.push(AlphanumericCharacterCodes[a].charCodeAt(0));\n text += AlphanumericCharacterCodes[a];\n }\n\n return { bytes, text };\n}\n\nfunction decodeByte(stream: BitStream, size: number) {\n const bytes: number[] = [];\n let text = \"\";\n\n const characterCountSize = [8, 16, 16][size];\n const length = stream.readBits(characterCountSize);\n for (let i = 0; i < length; i++) {\n const b = stream.readBits(8);\n bytes.push(b);\n }\n try {\n text += decodeURIComponent(bytes.map(b => `%${(\"0\" + b.toString(16)).substr(-2)}`).join(\"\"));\n } catch {\n // failed to decode\n }\n\n return { bytes, text };\n}\n\nfunction decodeKanji(stream: BitStream, size: number) {\n const bytes: number[] = [];\n\n const characterCountSize = [8, 10, 12][size];\n const length = stream.readBits(characterCountSize);\n for (let i = 0; i < length; i++) {\n const k = stream.readBits(13);\n\n let c = (Math.floor(k / 0xC0) << 8) | (k % 0xC0);\n if (c < 0x1F00) {\n c += 0x8140;\n } else {\n c += 0xC140;\n }\n\n bytes.push(c >> 8, c & 0xFF);\n }\n\n const text = new TextDecoder(\"shift-jis\").decode(Uint8Array.from(bytes));\n return { bytes, text };\n}\n\nexport function decode(data: Uint8ClampedArray, version: number): DecodedQR {\n const stream = new BitStream(data);\n\n // There are 3 'sizes' based on the version. 1-9 is small (0), 10-26 is medium (1) and 27-40 is large (2).\n const size = version <= 9 ? 0 : version <= 26 ? 1 : 2;\n\n const result: DecodedQR = {\n text: \"\",\n bytes: [],\n chunks: [],\n version,\n };\n\n while (stream.available() >= 4) {\n const mode = stream.readBits(4);\n if (mode === ModeByte.Terminator) {\n return result;\n } else if (mode === ModeByte.ECI) {\n if (stream.readBits(1) === 0) {\n result.chunks.push({\n type: Mode.ECI,\n assignmentNumber: stream.readBits(7),\n });\n } else if (stream.readBits(1) === 0) {\n result.chunks.push({\n type: Mode.ECI,\n assignmentNumber: stream.readBits(14),\n });\n } else if (stream.readBits(1) === 0) {\n result.chunks.push({\n type: Mode.ECI,\n assignmentNumber: stream.readBits(21),\n });\n } else {\n // ECI data seems corrupted\n result.chunks.push({\n type: Mode.ECI,\n assignmentNumber: -1,\n });\n }\n } else if (mode === ModeByte.Numeric) {\n const numericResult = decodeNumeric(stream, size);\n result.text += numericResult.text;\n result.bytes.push(...numericResult.bytes);\n result.chunks.push({\n type: Mode.Numeric,\n text: numericResult.text,\n });\n } else if (mode === ModeByte.Alphanumeric) {\n const alphanumericResult = decodeAlphanumeric(stream, size);\n result.text += alphanumericResult.text;\n result.bytes.push(...alphanumericResult.bytes);\n result.chunks.push({\n type: Mode.Alphanumeric,\n text: alphanumericResult.text,\n });\n } else if (mode === ModeByte.Byte) {\n const byteResult = decodeByte(stream, size);\n result.text += byteResult.text;\n result.bytes.push(...byteResult.bytes);\n result.chunks.push({\n type: Mode.Byte,\n bytes: byteResult.bytes,\n text: byteResult.text,\n });\n } else if (mode === ModeByte.Kanji) {\n const kanjiResult = decodeKanji(stream, size);\n result.text += kanjiResult.text;\n result.bytes.push(...kanjiResult.bytes);\n result.chunks.push({\n type: Mode.Kanji,\n bytes: kanjiResult.bytes,\n text: kanjiResult.text,\n });\n } else if (mode === ModeByte.StructuredAppend) {\n result.chunks.push({\n type: Mode.StructuredAppend,\n currentSequence: stream.readBits(4),\n totalSequence: stream.readBits(4),\n parity: stream.readBits(8),\n });\n }\n }\n\n // If there is no data left, or the remaining bits are all 0, then that counts as a termination marker\n if (stream.available() === 0 || stream.readBits(stream.available()) === 0) {\n return result;\n }\n}\n","import GenericGFPoly from \"./GenericGFPoly\";\n\nexport function addOrSubtractGF(a: number, b: number) {\n return a ^ b; // tslint:disable-line:no-bitwise\n}\n\nexport default class GenericGF {\n public primitive: number;\n public size: number;\n public generatorBase: number;\n public zero: GenericGFPoly;\n public one: GenericGFPoly;\n\n private expTable: number[];\n private logTable: number[];\n\n constructor(primitive: number, size: number, genBase: number) {\n this.primitive = primitive;\n this.size = size;\n this.generatorBase = genBase;\n this.expTable = new Array(this.size);\n this.logTable = new Array(this.size);\n\n let x = 1;\n for (let i = 0; i < this.size; i++) {\n this.expTable[i] = x;\n x = x * 2;\n if (x >= this.size) {\n x = (x ^ this.primitive) & (this.size - 1); // tslint:disable-line:no-bitwise\n }\n }\n\n for (let i = 0; i < this.size - 1; i++) {\n this.logTable[this.expTable[i]] = i;\n }\n this.zero = new GenericGFPoly(this, Uint8ClampedArray.from([0]));\n this.one = new GenericGFPoly(this, Uint8ClampedArray.from([1]));\n }\n\n public multiply(a: number, b: number) {\n if (a === 0 || b === 0) {\n return 0;\n }\n return this.expTable[(this.logTable[a] + this.logTable[b]) % (this.size - 1)];\n }\n\n public inverse(a: number) {\n if (a === 0) {\n throw new Error(\"Can't invert 0\");\n }\n return this.expTable[this.size - this.logTable[a] - 1];\n }\n\n public buildMonomial(degree: number, coefficient: number): GenericGFPoly {\n if (degree < 0) {\n throw new Error(\"Invalid monomial degree less than 0\");\n }\n if (coefficient === 0) {\n return this.zero;\n }\n const coefficients = new Uint8ClampedArray(degree + 1);\n coefficients[0] = coefficient;\n return new GenericGFPoly(this, coefficients);\n }\n\n public log(a: number) {\n if (a === 0) {\n throw new Error(\"Can't take log(0)\");\n }\n return this.logTable[a];\n }\n\n public exp(a: number) {\n return this.expTable[a];\n }\n}\n","import GenericGF, { addOrSubtractGF } from \"./GenericGF\";\nimport GenericGFPoly from \"./GenericGFPoly\";\n\nfunction runEuclideanAlgorithm(field: GenericGF, a: GenericGFPoly, b: GenericGFPoly, R: number): GenericGFPoly[] {\n // Assume a's degree is >= b's\n if (a.degree() < b.degree()) {\n [a, b] = [b, a];\n }\n\n let rLast = a;\n let r = b;\n let tLast = field.zero;\n let t = field.one;\n\n // Run Euclidean algorithm until r's degree is less than R/2\n while (r.degree() >= R / 2) {\n const rLastLast = rLast;\n const tLastLast = tLast;\n rLast = r;\n tLast = t;\n\n // Divide rLastLast by rLast, with quotient in q and remainder in r\n if (rLast.isZero()) {\n // Euclidean algorithm already terminated?\n return null;\n }\n r = rLastLast;\n let q = field.zero;\n const denominatorLeadingTerm = rLast.getCoefficient(rLast.degree());\n const dltInverse = field.inverse(denominatorLeadingTerm);\n while (r.degree() >= rLast.degree() && !r.isZero()) {\n const degreeDiff = r.degree() - rLast.degree();\n const scale = field.multiply(r.getCoefficient(r.degree()), dltInverse);\n q = q.addOrSubtract(field.buildMonomial(degreeDiff, scale));\n r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale));\n }\n\n t = q.multiplyPoly(tLast).addOrSubtract(tLastLast);\n\n if (r.degree() >= rLast.degree()) {\n return null;\n }\n }\n\n const sigmaTildeAtZero = t.getCoefficient(0);\n if (sigmaTildeAtZero === 0) {\n return null;\n }\n\n const inverse = field.inverse(sigmaTildeAtZero);\n return [t.multiply(inverse), r.multiply(inverse)];\n}\n\nfunction findErrorLocations(field: GenericGF, errorLocator: GenericGFPoly): number[] {\n // This is a direct application of Chien's search\n const numErrors = errorLocator.degree();\n if (numErrors === 1) {\n return [errorLocator.getCoefficient(1)];\n }\n const result: number[] = new Array(numErrors);\n let errorCount = 0;\n for (let i = 1; i < field.size && errorCount < numErrors; i++) {\n if (errorLocator.evaluateAt(i) === 0) {\n result[errorCount] = field.inverse(i);\n errorCount++;\n }\n }\n if (errorCount !== numErrors) {\n return null;\n }\n return result;\n}\n\nfunction findErrorMagnitudes(field: GenericGF, errorEvaluator: GenericGFPoly, errorLocations: number[]): number[] {\n // This is directly applying Forney's Formula\n const s = errorLocations.length;\n const result: number[] = new Array(s);\n for (let i = 0; i < s; i++) {\n const xiInverse = field.inverse(errorLocations[i]);\n let denominator = 1;\n for (let j = 0; j < s; j++) {\n if (i !== j) {\n denominator = field.multiply(denominator, addOrSubtractGF(1, field.multiply(errorLocations[j], xiInverse)));\n }\n }\n result[i] = field.multiply(errorEvaluator.evaluateAt(xiInverse), field.inverse(denominator));\n if (field.generatorBase !== 0) {\n result[i] = field.multiply(result[i], xiInverse);\n }\n }\n return result;\n}\n\nexport function decode(bytes: number[], twoS: number) {\n const outputBytes = new Uint8ClampedArray(bytes.length);\n outputBytes.set(bytes);\n\n const field = new GenericGF(0x011D, 256, 0); // x^8 + x^4 + x^3 + x^2 + 1\n const poly = new GenericGFPoly(field, outputBytes);\n\n const syndromeCoefficients = new Uint8ClampedArray(twoS);\n let error = false;\n for (let s = 0; s < twoS; s++) {\n const evaluation = poly.evaluateAt(field.exp(s + field.generatorBase));\n syndromeCoefficients[syndromeCoefficients.length - 1 - s] = evaluation;\n if (evaluation !== 0) {\n error = true;\n }\n }\n if (!error) {\n return outputBytes;\n }\n\n const syndrome = new GenericGFPoly(field, syndromeCoefficients);\n\n const sigmaOmega = runEuclideanAlgorithm(field, field.buildMonomial(twoS, 1), syndrome, twoS);\n if (sigmaOmega === null) {\n return null;\n }\n\n const errorLocations = findErrorLocations(field, sigmaOmega[0]);\n if (errorLocations == null) {\n return null;\n }\n\n const errorMagnitudes = findErrorMagnitudes(field, sigmaOmega[1], errorLocations);\n for (let i = 0; i < errorLocations.length; i++) {\n const position = outputBytes.length - 1 - field.log(errorLocations[i]);\n if (position < 0) {\n return null;\n }\n outputBytes[position] = addOrSubtractGF(outputBytes[position], errorMagnitudes[i]);\n }\n\n return outputBytes;\n}\n","import { BitMatrix } from \"../BitMatrix\";\nimport { Point } from \"../Point\";\nimport { decode as decodeData, DecodedQR } from \"./decodeData\";\nimport { decode as rsDecode } from \"./reedsolomon\";\nimport { Version, VERSIONS } from \"./version\";\n\n// tslint:disable:no-bitwise\nfunction numBitsDiffering(x: number, y: number) {\n let z = x ^ y;\n let bitCount = 0;\n while (z) {\n bitCount++;\n z &= z - 1;\n }\n return bitCount;\n}\n\nfunction pushBit(bit: any, byte: number) {\n return (byte << 1) | bit;\n}\n// tslint:enable:no-bitwise\n\nconst FORMAT_INFO_TABLE = [\n { bits: 0x5412, formatInfo: { errorCorrectionLevel: 1, dataMask: 0 } },\n { bits: 0x5125, formatInfo: { errorCorrectionLevel: 1, dataMask: 1 } },\n { bits: 0x5E7C, formatInfo: { errorCorrectionLevel: 1, dataMask: 2 } },\n { bits: 0x5B4B, formatInfo: { errorCorrectionLevel: 1, dataMask: 3 } },\n { bits: 0x45F9, formatInfo: { errorCorrectionLevel: 1, dataMask: 4 } },\n { bits: 0x40CE, formatInfo: { errorCorrectionLevel: 1, dataMask: 5 } },\n { bits: 0x4F97, formatInfo: { errorCorrectionLevel: 1, dataMask: 6 } },\n { bits: 0x4AA0, formatInfo: { errorCorrectionLevel: 1, dataMask: 7 } },\n { bits: 0x77C4, formatInfo: { errorCorrectionLevel: 0, dataMask: 0 } },\n { bits: 0x72F3, formatInfo: { errorCorrectionLevel: 0, dataMask: 1 } },\n { bits: 0x7DAA, formatInfo: { errorCorrectionLevel: 0, dataMask: 2 } },\n { bits: 0x789D, formatInfo: { errorCorrectionLevel: 0, dataMask: 3 } },\n { bits: 0x662F, formatInfo: { errorCorrectionLevel: 0, dataMask: 4 } },\n { bits: 0x6318, formatInfo: { errorCorrectionLevel: 0, dataMask: 5 } },\n { bits: 0x6C41, formatInfo: { errorCorrectionLevel: 0, dataMask: 6 } },\n { bits: 0x6976, formatInfo: { errorCorrectionLevel: 0, dataMask: 7 } },\n { bits: 0x1689, formatInfo: { errorCorrectionLevel: 3, dataMask: 0 } },\n { bits: 0x13BE, formatInfo: { errorCorrectionLevel: 3, dataMask: 1 } },\n { bits: 0x1CE7, formatInfo: { errorCorrectionLevel: 3, dataMask: 2 } },\n { bits: 0x19D0, formatInfo: { errorCorrectionLevel: 3, dataMask: 3 } },\n { bits: 0x0762, formatInfo: { errorCorrectionLevel: 3, dataMask: 4 } },\n { bits: 0x0255, formatInfo: { errorCorrectionLevel: 3, dataMask: 5 } },\n { bits: 0x0D0C, formatInfo: { errorCorrectionLevel: 3, dataMask: 6 } },\n { bits: 0x083B, formatInfo: { errorCorrectionLevel: 3, dataMask: 7 } },\n { bits: 0x355F, formatInfo: { errorCorrectionLevel: 2, dataMask: 0 } },\n { bits: 0x3068, formatInfo: { errorCorrectionLevel: 2, dataMask: 1 } },\n { bits: 0x3F31, formatInfo: { errorCorrectionLevel: 2, dataMask: 2 } },\n { bits: 0x3A06, formatInfo: { errorCorrectionLevel: 2, dataMask: 3 } },\n { bits: 0x24B4, formatInfo: { errorCorrectionLevel: 2, dataMask: 4 } },\n { bits: 0x2183, formatInfo: { errorCorrectionLevel: 2, dataMask: 5 } },\n { bits: 0x2EDA, formatInfo: { errorCorrectionLevel: 2, dataMask: 6 } },\n { bits: 0x2BED, formatInfo: { errorCorrectionLevel: 2, dataMask: 7 } },\n];\n\nconst DATA_MASKS = [\n (p: Point) => ((p.y + p.x) % 2) === 0,\n (p: Point) => (p.y % 2) === 0,\n (p: Point) => p.x % 3 === 0,\n (p: Point) => (p.y + p.x) % 3 === 0,\n (p: Point) => (Math.floor(p.y / 2) + Math.floor(p.x / 3)) % 2 === 0,\n (p: Point) => ((p.x * p.y) % 2) + ((p.x * p.y) % 3) === 0,\n (p: Point) => ((((p.y * p.x) % 2) + (p.y * p.x) % 3) % 2) === 0,\n (p: Point) => ((((p.y + p.x) % 2) + (p.y * p.x) % 3) % 2) === 0,\n];\n\ninterface FormatInformation {\n errorCorrectionLevel: number;\n dataMask: number;\n}\n\nfunction buildFunctionPatternMask(version: Version): BitMatrix {\n const dimension = 17 + 4 * version.versionNumber;\n const matrix = BitMatrix.createEmpty(dimension, dimension);\n\n matrix.setRegion(0, 0, 9, 9, true); // Top left finder pattern + separator + format\n matrix.setRegion(dimension - 8, 0, 8, 9, true); // Top right finder pattern + separator + format\n matrix.setRegion(0, dimension - 8, 9, 8, true); // Bottom left finder pattern + separator + format\n\n // Alignment patterns\n for (const x of version.alignmentPatternCenters) {\n for (const y of version.alignmentPatternCenters) {\n if (!(x === 6 && y === 6 || x === 6 && y === dimension - 7 || x === dimension - 7 && y === 6)) {\n matrix.setRegion(x - 2, y - 2, 5, 5, true);\n }\n }\n }\n\n matrix.setRegion(6, 9, 1, dimension - 17, true); // Vertical timing pattern\n matrix.setRegion(9, 6, dimension - 17, 1, true); // Horizontal timing pattern\n\n if (version.versionNumber > 6) {\n matrix.setRegion(dimension - 11, 0, 3, 6, true); // Version info, top right\n matrix.setRegion(0, dimension - 11, 6, 3, true); // Version info, bottom left\n }\n\n return matrix;\n}\n\nfunction readCodewords(matrix: BitMatrix, version: Version, formatInfo: FormatInformation) {\n const dataMask = DATA_MASKS[formatInfo.dataMask];\n const dimension = matrix.height;\n\n const functionPatternMask = buildFunctionPatternMask(version);\n\n const codewords: number[] = [];\n let currentByte = 0;\n let bitsRead = 0;\n\n // Read columns in pairs, from right to left\n let readingUp = true;\n for (let columnIndex = dimension - 1; columnIndex > 0; columnIndex -= 2) {\n if (columnIndex === 6) { // Skip whole column with vertical alignment pattern;\n columnIndex--;\n }\n for (let i = 0; i < dimension; i++) {\n const y = readingUp ? dimension - 1 - i : i;\n for (let columnOffset = 0; columnOffset < 2; columnOffset++) {\n const x = columnIndex - columnOffset;\n if (!functionPatternMask.get(x, y)) {\n bitsRead++;\n let bit = matrix.get(x, y);\n if (dataMask({y, x})) {\n bit = !bit;\n }\n currentByte = pushBit(bit, currentByte);\n if (bitsRead === 8) { // Whole bytes\n codewords.push(currentByte);\n bitsRead = 0;\n currentByte = 0;\n }\n }\n }\n }\n readingUp = !readingUp;\n }\n return codewords;\n}\n\nfunction readVersion(matrix: BitMatrix): Version {\n const dimension = matrix.height;\n\n const provisionalVersion = Math.floor((dimension - 17) / 4);\n if (provisionalVersion <= 6) { // 6 and under dont have version info in the QR code\n return VERSIONS[provisionalVersion - 1];\n }\n\n let topRightVersionBits = 0;\n for (let y = 5; y >= 0; y--) {\n for (let x = dimension - 9; x >= dimension - 11; x--) {\n topRightVersionBits = pushBit(matrix.get(x, y), topRightVersionBits);\n }\n }\n\n let bottomLeftVersionBits = 0;\n for (let x = 5; x >= 0; x--) {\n for (let y = dimension - 9; y >= dimension - 11; y--) {\n bottomLeftVersionBits = pushBit(matrix.get(x, y), bottomLeftVersionBits);\n }\n }\n\n let bestDifference = Infinity;\n let bestVersion: Version;\n for (const version of VERSIONS) {\n if (version.infoBits === topRightVersionBits || version.infoBits === bottomLeftVersionBits) {\n return version;\n }\n\n let difference = numBitsDiffering(topRightVersionBits, version.infoBits);\n if (difference < bestDifference) {\n bestVersion = version;\n bestDifference = difference;\n }\n\n difference = numBitsDiffering(bottomLeftVersionBits, version.infoBits);\n if (difference < bestDifference) {\n bestVersion = version;\n bestDifference = difference;\n }\n }\n // We can tolerate up to 3 bits of error since no two version info codewords will\n // differ in less than 8 bits.\n if (bestDifference <= 3) {\n return bestVersion;\n }\n}\n\nfunction readFormatInformation(matrix: BitMatrix) {\n let topLeftFormatInfoBits = 0;\n for (let x = 0; x <= 8; x++) {\n if (x !== 6) { // Skip timing pattern bit\n topLeftFormatInfoBits = pushBit(matrix.get(x, 8), topLeftFormatInfoBits);\n }\n }\n for (let y = 7; y >= 0; y--) {\n if (y !== 6) { // Skip timing pattern bit\n topLeftFormatInfoBits = pushBit(matrix.get(8, y), topLeftFormatInfoBits);\n }\n }\n\n const dimension = matrix.height;\n let topRightBottomRightFormatInfoBits = 0;\n for (let y = dimension - 1; y >= dimension - 7; y--) { // bottom left\n topRightBottomRightFormatInfoBits = pushBit(matrix.get(8, y), topRightBottomRightFormatInfoBits);\n }\n for (let x = dimension - 8; x < dimension; x++) { // top right\n topRightBottomRightFormatInfoBits = pushBit(matrix.get(x, 8), topRightBottomRightFormatInfoBits);\n }\n\n let bestDifference = Infinity;\n let bestFormatInfo = null;\n for (const {bits, formatInfo} of FORMAT_INFO_TABLE) {\n if (bits === topLeftFormatInfoBits || bits === topRightBottomRightFormatInfoBits) {\n return formatInfo;\n }\n let difference = numBitsDiffering(topLeftFormatInfoBits, bits);\n if (difference < bestDifference) {\n bestFormatInfo = formatInfo;\n bestDifference = difference;\n }\n if (topLeftFormatInfoBits !== topRightBottomRightFormatInfoBits) { // also try the other option\n difference = numBitsDiffering(topRightBottomRightFormatInfoBits, bits);\n if (difference < bestDifference) {\n bestFormatInfo = formatInfo;\n bestDifference = difference;\n }\n }\n }\n // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match\n if (bestDifference <= 3) {\n return bestFormatInfo;\n }\n return null;\n}\n\nfunction getDataBlocks(codewords: number[], version: Version, ecLevel: number) {\n const ecInfo = version.errorCorrectionLevels[ecLevel];\n const dataBlocks: Array<{\n numDataCodewords: number;\n codewords: number[];\n }> = [];\n\n let totalCodewords = 0;\n ecInfo.ecBlocks.forEach(block => {\n for (let i = 0; i < block.numBlocks; i++) {\n dataBlocks.push({ numDataCodewords: block.dataCodewordsPerBlock, codewords: [] });\n totalCodewords += block.dataCodewordsPerBlock + ecInfo.ecCodewordsPerBlock;\n }\n });\n\n // In some cases the QR code will be malformed enough that we pull off more or less than we should.\n // If we pull off less there's nothing we can do.\n // If we pull off more we can safely truncate\n if (codewords.length < totalCodewords) {\n return null;\n }\n codewords = codewords.slice(0, totalCodewords);\n\n const shortBlockSize = ecInfo.ecBlocks[0].dataCodewordsPerBlock;\n // Pull codewords to fill the blocks up to the minimum size\n for (let i = 0; i < shortBlockSize; i++) {\n for (const dataBlock of dataBlocks) {\n dataBlock.codewords.push(codewords.shift());\n }\n }\n\n // If there are any large blocks, pull codewords to fill the last element of those\n if (ecInfo.ecBlocks.length > 1) {\n const smallBlockCount = ecInfo.ecBlocks[0].numBlocks;\n const largeBlockCount = ecInfo.ecBlocks[1].numBlocks;\n for (let i = 0; i < largeBlockCount; i++) {\n dataBlocks[smallBlockCount + i].codewords.push(codewords.shift());\n }\n }\n\n // Add the rest of the codewords to the blocks. These are the error correction codewords.\n while (codewords.length > 0) {\n for (const dataBlock of dataBlocks) {\n dataBlock.codewords.push(codewords.shift());\n }\n }\n\n return dataBlocks;\n}\n\nfunction decodeMatrix(matrix: BitMatrix) {\n const version = readVersion(matrix);\n if (!version) {\n return null;\n }\n\n const formatInfo = readFormatInformation(matrix);\n if (!formatInfo) {\n return null;\n }\n\n const codewords = readCodewords(matrix, version, formatInfo);\n const dataBlocks = getDataBlocks(codewords, version, formatInfo.errorCorrectionLevel);\n if (!dataBlocks) {\n return null;\n }\n\n // Count total number of data bytes\n const totalBytes = dataBlocks.reduce((a, b) => a + b.numDataCodewords, 0);\n const resultBytes = new Uint8ClampedArray(totalBytes);\n\n let resultIndex = 0;\n for (const dataBlock of dataBlocks) {\n const correctedBytes = rsDecode(dataBlock.codewords, dataBlock.codewords.length - dataBlock.numDataCodewords);\n if (!correctedBytes) {\n return null;\n }\n for (let i = 0; i < dataBlock.numDataCodewords; i++) {\n resultBytes[resultIndex++] = correctedBytes[i];\n }\n }\n\n try {\n return decodeData(resultBytes, version.versionNumber);\n } catch {\n return null;\n }\n}\n\nexport function decode(matrix: BitMatrix): DecodedQR {\n if (matrix == null) {\n return null;\n }\n const result = decodeMatrix(matrix);\n if (result) {\n return result;\n }\n // Decoding didn't work, try mirroring the QR across the topLeft -> bottomRight line.\n for (let x = 0; x < matrix.width; x++) {\n for (let y = x + 1; y < matrix.height; y++) {\n if (matrix.get(x, y) !== matrix.get(y, x)) {\n matrix.set(x, y, !matrix.get(x, y));\n matrix.set(y, x, !matrix.get(y, x));\n }\n }\n }\n return decodeMatrix(matrix);\n}\n","import {BitMatrix} from \"../BitMatrix\";\nimport {Point, QRLocation} from \"../locator\";\n\ninterface PerspectiveTransform {\n a11: number;\n a21: number;\n a31: number;\n a12: number;\n a22: number;\n a32: number;\n a13: number;\n a23: number;\n a33: number;\n}\n\nfunction squareToQuadrilateral(p1: Point, p2: Point, p3: Point, p4: Point): PerspectiveTransform {\n const dx3 = p1.x - p2.x + p3.x - p4.x;\n const dy3 = p1.y - p2.y + p3.y - p4.y;\n if (dx3 === 0 && dy3 === 0) { // Affine\n return {\n a11: p2.x - p1.x,\n a12: p2.y - p1.y,\n a13: 0,\n a21: p3.x - p2.x,\n a22: p3.y - p2.y,\n a23: 0,\n a31: p1.x,\n a32: p1.y,\n a33: 1,\n };\n } else {\n const dx1 = p2.x - p3.x;\n const dx2 = p4.x - p3.x;\n const dy1 = p2.y - p3.y;\n const dy2 = p4.y - p3.y;\n const denominator = dx1 * dy2 - dx2 * dy1;\n const a13 = (dx3 * dy2 - dx2 * dy3) / denominator;\n const a23 = (dx1 * dy3 - dx3 * dy1) / denominator;\n return {\n a11: p2.x - p1.x + a13 * p2.x,\n a12: p2.y - p1.y + a13 * p2.y,\n a13,\n a21: p4.x - p1.x + a23 * p4.x,\n a22: p4.y - p1.y + a23 * p4.y,\n a23,\n a31: p1.x,\n a32: p1.y,\n a33: 1,\n };\n }\n}\n\nfunction quadrilateralToSquare(p1: Point, p2: Point, p3: Point, p4: Point): PerspectiveTransform {\n // Here, the adjoint serves as the inverse:\n const sToQ = squareToQuadrilateral(p1, p2, p3, p4);\n return {\n a11: sToQ.a22 * sToQ.a33 - sToQ.a23 * sToQ.a32,\n a12: sToQ.a13 * sToQ.a32 - sToQ.a12 * sToQ.a33,\n a13: sToQ.a12 * sToQ.a23 - sToQ.a13 * sToQ.a22,\n a21: sToQ.a23 * sToQ.a31 - sToQ.a21 * sToQ.a33,\n a22: sToQ.a11 * sToQ.a33 - sToQ.a13 * sToQ.a31,\n a23: sToQ.a13 * sToQ.a21 - sToQ.a11 * sToQ.a23,\n a31: sToQ.a21 * sToQ.a32 - sToQ.a22 * sToQ.a31,\n a32: sToQ.a12 * sToQ.a31 - sToQ.a11 * sToQ.a32,\n a33: sToQ.a11 * sToQ.a22 - sToQ.a12 * sToQ.a21,\n };\n}\n\nfunction times(a: PerspectiveTransform, b: PerspectiveTransform): PerspectiveTransform {\n return {\n a11: a.a11 * b.a11 + a.a21 * b.a12 + a.a31 * b.a13,\n a12: a.a12 * b.a11 + a.a22 * b.a12 + a.a32 * b.a13,\n a13: a.a13 * b.a11 + a.a23 * b.a12 + a.a33 * b.a13,\n a21: a.a11 * b.a21 + a.a21 * b.a22 + a.a31 * b.a23,\n a22: a.a12 * b.a21 + a.a22 * b.a22 + a.a32 * b.a23,\n a23: a.a13 * b.a21 + a.a23 * b.a22 + a.a33 * b.a23,\n a31: a.a11 * b.a31 + a.a21 * b.a32 + a.a31 * b.a33,\n a32: a.a12 * b.a31 + a.a22 * b.a32 + a.a32 * b.a33,\n a33: a.a13 * b.a31 + a.a23 * b.a32 + a.a33 * b.a33,\n };\n}\n\nexport function extract(image: BitMatrix, location: QRLocation) {\n const qToS = quadrilateralToSquare(\n {x: 3.5, y: 3.5},\n {x: location.dimension - 3.5, y: 3.5},\n {x: location.dimension - 6.5, y: location.dimension - 6.5},\n {x: 3.5, y: location.dimension - 3.5},\n );\n const sToQ = squareToQuadrilateral(location.topLeft, location.topRight, location.alignmentPattern, location.bottomLeft);\n const transform = times(sToQ, qToS);\n\n const matrix = BitMatrix.createEmpty(location.dimension, location.dimension);\n const mappingFunction = (x: number, y: number) => {\n const denominator = transform.a13 * x + transform.a23 * y + transform.a33;\n return {\n x: (transform.a11 * x + transform.a21 * y + transform.a31) / denominator,\n y: (transform.a12 * x + transform.a22 * y + transform.a32) / denominator,\n };\n };\n\n for (let y = 0; y < location.dimension; y++) {\n for (let x = 0; x < location.dimension; x++) {\n const xValue = x + 0.5;\n const yValue = y + 0.5;\n const sourcePixel = mappingFunction(xValue, yValue);\n matrix.set(x, y, image.get(Math.floor(sourcePixel.x), Math.floor(sourcePixel.y)));\n }\n }\n\n return {\n matrix,\n mappingFunction,\n };\n}\n","import { BitMatrix } from \"../BitMatrix\";\n\nconst MAX_FINDERPATTERNS_TO_SEARCH = 5;\nconst MIN_QUAD_RATIO = 0.5;\nconst MAX_QUAD_RATIO = 1.5;\n\nexport interface Point {\n x: number;\n y: number;\n}\n\nexport interface QRLocation {\n topRight: Point;\n bottomLeft: Point;\n topLeft: Point;\n alignmentPattern: Point;\n dimension: number;\n}\n\nconst distance = (a: Point, b: Point) => Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);\n\nfunction sum(values: number[]) {\n return values.reduce((a, b) => a + b);\n}\n\n// Takes three finder patterns and organizes them into topLeft, topRight, etc\nfunction reorderFinderPatterns(pattern1: Point, pattern2: Point, pattern3: Point) {\n // Find distances between pattern centers\n const oneTwoDistance = distance(pattern1, pattern2);\n const twoThreeDistance = distance(pattern2, pattern3);\n const oneThreeDistance = distance(pattern1, pattern3);\n\n let bottomLeft: Point;\n let topLeft: Point;\n let topRight: Point;\n\n // Assume one closest to other two is B; A and C will just be guesses at first\n if (twoThreeDistance >= oneTwoDistance && twoThreeDistance >= oneThreeDistance) {\n [bottomLeft, topLeft, topRight] = [pattern2, pattern1, pattern3];\n } else if (oneThreeDistance >= twoThreeDistance && oneThreeDistance >= oneTwoDistance) {\n [bottomLeft, topLeft, topRight] = [pattern1, pattern2, pattern3];\n } else {\n [bottomLeft, topLeft, topRight] = [pattern1, pattern3, pattern2];\n }\n\n // Use cross product to figure out whether bottomLeft (A) and topRight (C) are correct or flipped in relation to topLeft (B)\n // This asks whether BC x BA has a positive z component, which is the arrangement we want. If it's negative, then\n // we've got it flipped around and should swap topRight and bottomLeft.\n if (((topRight.x - topLeft.x) * (bottomLeft.y - topLeft.y)) - ((topRight.y - topLeft.y) * (bottomLeft.x - topLeft.x)) < 0) {\n [bottomLeft, topRight] = [topRight, bottomLeft];\n }\n\n return { bottomLeft, topLeft, topRight };\n}\n\n// Computes the dimension (number of modules on a side) of the QR Code based on the position of the finder patterns\nfunction computeDimension(topLeft: Point, topRight: Point, bottomLeft: Point, matrix: BitMatrix) {\n const moduleSize = (\n sum(countBlackWhiteRun(topLeft, bottomLeft, matrix, 5)) / 7 + // Divide by 7 since the ratio is 1:1:3:1:1\n sum(countBlackWhiteRun(topLeft, topRight, matrix, 5)) / 7 +\n sum(countBlackWhiteRun(bottomLeft, topLeft, matrix, 5)) / 7 +\n sum(countBlackWhiteRun(topRight, topLeft, matrix, 5)) / 7\n ) / 4;\n\n if (moduleSize < 1) {\n throw new Error(\"Invalid module size\");\n }\n\n const topDimension = Math.round(distance(topLeft, topRight) / moduleSize);\n const sideDimension = Math.round(distance(topLeft, bottomLeft) / moduleSize);\n let dimension = Math.floor((topDimension + sideDimension) / 2) + 7;\n switch (dimension % 4) {\n case 0:\n dimension++;\n break;\n case 2:\n dimension--;\n break;\n }\n return { dimension, moduleSize };\n}\n\n// Takes an origin point and an end point and counts the sizes of the black white run from the origin towards the end point.\n// Returns an array of elements, representing the pixel size of the black white run.\n// Uses a variant of http://en.wikipedia.org/wiki/Bresenham's_line_algorithm\nfunction countBlackWhiteRunTowardsPoint(origin: Point, end: Point, matrix: BitMatrix, length: number) {\n const switchPoints: Point[] = [{x: Math.floor(origin.x), y: Math.floor(origin.y)}];\n const steep = Math.abs(end.y - origin.y) > Math.abs(end.x - origin.x);\n\n let fromX: number;\n let fromY: number;\n let toX: number;\n let toY: number;\n if (steep) {\n fromX = Math.floor(origin.y);\n fromY = Math.floor(origin.x);\n toX = Math.floor(end.y);\n toY = Math.floor(end.x);\n } else {\n fromX = Math.floor(origin.x);\n fromY = Math.floor(origin.y);\n toX = Math.floor(end.x);\n toY = Math.floor(end.y);\n }\n\n const dx = Math.abs(toX - fromX);\n const dy = Math.abs(toY - fromY);\n let error = Math.floor(-dx / 2);\n const xStep = fromX < toX ? 1 : -1;\n const yStep = fromY < toY ? 1 : -1;\n\n let currentPixel = true;\n // Loop up until x == toX, but not beyond\n for (let x = fromX, y = fromY; x !== toX + xStep; x += xStep) {\n // Does current pixel mean we have moved white to black or vice versa?\n // Scanning black in state 0,2 and white in state 1, so if we find the wrong\n // color, advance to next state or end if we are in state 2 already\n const realX = steep ? y : x;\n const realY = steep ? x : y;\n if (matrix.get(realX, realY) !== currentPixel) {\n currentPixel = !currentPixel;\n switchPoints.push({x: realX, y: realY});\n if (switchPoints.length === length + 1) {\n break;\n }\n }\n error += dy;\n if (error > 0) {\n if (y === toY) {\n break;\n }\n y += yStep;\n error -= dx;\n }\n }\n const distances: number[] = [];\n for (let i = 0; i < length; i++) {\n if (switchPoints[i] && switchPoints[i + 1]) {\n distances.push(distance(switchPoints[i], switchPoints[i + 1]));\n } else {\n distances.push(0);\n }\n }\n return distances;\n}\n\n// Takes an origin point and an end point and counts the sizes of the black white run in the origin point\n// along the line that intersects with the end point. Returns an array of elements, representing the pixel sizes\n// of the black white run. Takes a length which represents the number of switches from black to white to look for.\nfunction countBlackWhiteRun(origin: Point, end: Point, matrix: BitMatrix, length: number) {\n const rise = end.y - origin.y;\n const run = end.x - origin.x;\n\n const towardsEnd = countBlackWhiteRunTowardsPoint(origin, end, matrix, Math.ceil(length / 2));\n const awayFromEnd = countBlackWhiteRunTowardsPoint(origin, {x: origin.x - run, y: origin.y - rise}, matrix, Math.ceil(length / 2));\n\n const middleValue = towardsEnd.shift() + awayFromEnd.shift() - 1; // Substract one so we don't double count a pixel\n return awayFromEnd.concat(middleValue).concat(...towardsEnd);\n}\n\n// Takes in a black white run and an array of expected ratios. Returns the average size of the run as well as the \"error\" -\n// that is the amount the run diverges from the expected ratio\nfunction scoreBlackWhiteRun(sequence: number[], ratios: number[]) {\n const averageSize = sum(sequence) / sum(ratios);\n let error = 0;\n ratios.forEach((ratio, i) => {\n error += (sequence[i] - ratio * averageSize) ** 2;\n });\n\n return { averageSize, error };\n}\n\n// Takes an X,Y point and an array of sizes and scores the point against those ratios.\n// For example for a finder pattern takes the ratio list of 1:1:3:1:1 and checks horizontal, vertical and diagonal ratios\n// against that.\nfunction scorePattern(point: Point, ratios: number[], matrix: BitMatrix) {\n try {\n const horizontalRun = countBlackWhiteRun(point, {x: -1, y: point.y}, matrix, ratios.length);\n const verticalRun = countBlackWhiteRun(point, {x: point.x, y: -1}, matrix, ratios.length);\n\n const topLeftPoint = {\n x: Math.max(0, point.x - point.y) - 1,\n y: Math.max(0, point.y - point.x) - 1,\n };\n const topLeftBottomRightRun = countBlackWhiteRun(point, topLeftPoint, matrix, ratios.length);\n\n const bottomLeftPoint = {\n x: Math.min(matrix.width, point.x + point.y) + 1,\n y: Math.min(matrix.height, point.y + point.x) + 1,\n };\n const bottomLeftTopRightRun = countBlackWhiteRun(point, bottomLeftPoint, matrix, ratios.length);\n\n const horzError = scoreBlackWhiteRun(horizontalRun, ratios);\n const vertError = scoreBlackWhiteRun(verticalRun, ratios);\n const diagDownError = scoreBlackWhiteRun(topLeftBottomRightRun, ratios);\n const diagUpError = scoreBlackWhiteRun(bottomLeftTopRightRun, ratios);\n\n const ratioError = Math.sqrt(horzError.error * horzError.error +\n vertError.error * vertError.error +\n diagDownError.error * diagDownError.error +\n diagUpError.error * diagUpError.error);\n\n const avgSize = (horzError.averageSize + vertError.averageSize + diagDownError.averageSize + diagUpError.averageSize) / 4;\n\n const sizeError = ((horzError.averageSize - avgSize) ** 2 +\n (vertError.averageSize - avgSize) ** 2 +\n (diagDownError.averageSize - avgSize) ** 2 +\n (diagUpError.averageSize - avgSize) ** 2) / avgSize;\n return ratioError + sizeError;\n } catch {\n return Infinity;\n }\n}\n\nfunction recenterLocation(matrix: BitMatrix, p: Point): Point {\n let leftX = Math.round(p.x);\n while (matrix.get(leftX, Math.round(p.y))) {\n leftX--;\n }\n let rightX = Math.round(p.x);\n while (matrix.get(rightX, Math.round(p.y))) {\n rightX++;\n }\n const x = (leftX + rightX) / 2;\n\n let topY = Math.round(p.y);\n while (matrix.get(Math.round(x), topY)) {\n topY--;\n }\n let bottomY = Math.round(p.y);\n while (matrix.get(Math.round(x), bottomY)) {\n bottomY++;\n }\n const y = (topY + bottomY) / 2;\n\n return { x, y };\n}\n\ninterface Quad {\n top: {\n startX: number;\n endX: number;\n y: number;\n };\n bottom: {\n startX: number;\n endX: number;\n y: number;\n };\n}\n\nexport function locate(matrix: BitMatrix): QRLocation[] {\n const finderPatternQuads: Quad[] = [];\n let activeFinderPatternQuads: Quad[] = [];\n const alignmentPatternQuads: Quad[] = [];\n let activeAlignmentPatternQuads: Quad[] = [];\n\n for (let y = 0; y <= matrix.height; y++) {\n let length = 0;\n let lastBit = false;\n let scans = [0, 0, 0, 0, 0];\n\n for (let x = -1; x <= matrix.width; x++) {\n const v = matrix.get(x, y);\n if (v === lastBit) {\n length++;\n } else {\n scans = [scans[1], scans[2], scans[3], scans[4], length];\n length = 1;\n lastBit = v;\n\n // Do the last 5 color changes ~ match the expected ratio for a finder pattern? 1:1:3:1:1 of b:w:b:w:b\n const averageFinderPatternBlocksize = sum(scans) / 7;\n const validFinderPattern =\n Math.abs(scans[0] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&\n Math.abs(scans[1] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&\n Math.abs(scans[2] - 3 * averageFinderPatternBlocksize) < 3 * averageFinderPatternBlocksize &&\n Math.abs(scans[3] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&\n Math.abs(scans[4] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&\n !v; // And make sure the current pixel is white since finder patterns are bordered in white\n\n // Do the last 3 color changes ~ match the expected ratio for an alignment pattern? 1:1:1 of w:b:w\n const averageAlignmentPatternBlocksize = sum(scans.slice(-3)) / 3;\n const validAlignmentPattern =\n Math.abs(scans[2] - averageAlignmentPatternBlocksize) < averageAlignmentPatternBlocksize &&\n Math.abs(scans[3] - averageAlignmentPatternBlocksize) < averageAlignmentPatternBlocksize &&\n Math.abs(scans[4] - averageAlignmentPatternBlocksize) < averageAlignmentPatternBlocksize &&\n v; // Is the current pixel black since alignment patterns are bordered in black\n\n if (validFinderPattern) {\n // Compute the start and end x values of the large center black square\n const endX = x - scans[3] - scans[4];\n const startX = endX - scans[2];\n\n const line = { startX, endX, y };\n // Is there a quad directly above the current spot? If so, extend it with the new line. Otherwise, create a new quad with\n // that line as the starting point.\n const matchingQuads = activeFinderPatternQuads.filter(q =>\n (startX >= q.bottom.startX && startX <= q.bottom.endX) ||\n (endX >= q.bottom.startX && startX <= q.bottom.endX) ||\n (startX <= q.bottom.startX && endX >= q.bottom.endX && (\n (scans[2] / (q.bottom.endX - q.bottom.startX)) < MAX_QUAD_RATIO &&\n (scans[2] / (q.bottom.endX - q.bottom.startX)) > MIN_QUAD_RATIO\n )),\n );\n if (matchingQuads.length > 0) {\n matchingQuads[0].bottom = line;\n } else {\n activeFinderPatternQuads.push({ top: line, bottom: line });\n }\n }\n if (validAlignmentPattern) {\n // Compute the start and end x values of the center black square\n const endX = x - scans[4];\n const startX = endX - scans[3];\n\n const line = { startX, y, endX };\n // Is there a quad directly above the current spot? If so, extend it with the new line. Otherwise, create a new quad with\n // that line as the starting point.\n const matchingQuads = activeAlignmentPatternQuads.filter(q =>\n (startX >= q.bottom.startX && startX <= q.bottom.endX) ||\n (endX >= q.bottom.startX && startX <= q.bottom.endX) ||\n (startX <= q.bottom.startX && endX >= q.bottom.endX && (\n (scans[2] / (q.bottom.endX - q.bottom.startX)) < MAX_QUAD_RATIO &&\n (scans[2] / (q.bottom.endX - q.bottom.startX)) > MIN_QUAD_RATIO\n )),\n );\n if (matchingQuads.length > 0) {\n matchingQuads[0].bottom = line;\n } else {\n activeAlignmentPatternQuads.push({ top: line, bottom: line });\n }\n }\n }\n }\n finderPatternQuads.push(...activeFinderPatternQuads.filter(q => q.bottom.y !== y && q.bottom.y - q.top.y >= 2));\n activeFinderPatternQuads = activeFinderPatternQuads.filter(q => q.bottom.y === y);\n\n alignmentPatternQuads.push(...activeAlignmentPatternQuads.filter(q => q.bottom.y !== y));\n activeAlignmentPatternQuads = activeAlignmentPatternQuads.filter(q => q.bottom.y === y);\n\n }\n\n finderPatternQuads.push(...activeFinderPatternQuads.filter(q => q.bottom.y - q.top.y >= 2));\n alignmentPatternQuads.push(...activeAlignmentPatternQuads);\n\n // Refactored from cozmo/jsQR to (hopefully) circumvent an issue in Safari 13+ on both Mac and iOS (also including\n // iOS Chrome and other Safari iOS derivatives). Safari was very occasionally and apparently not deterministically\n // throwing a \"RangeError: Array size is not a small enough positive integer.\" exception seemingly within the second\n // .map of the original code (here the second for-loop). This second .map contained a nested .map call over the same\n // array instance which was the chained result from previous calls to .map, .filter and .sort which potentially caused\n // this bug in Safari?\n // Also see https://github.com/cozmo/jsQR/issues/157 and https://bugs.webkit.org/show_bug.cgi?id=211619#c3\n const scoredFinderPatternPositions: Array = [];\n for (const quad of finderPatternQuads) {\n if (quad.bottom.y - quad.top.y < 2) {\n // All quads must be at least 2px tall since the center square is larger than a block\n continue;\n }\n\n // calculate quad center\n const x = (quad.top.startX + quad.top.endX + quad.bottom.startX + quad.bottom.endX) / 4;\n const y = (quad.top.y + quad.bottom.y + 1) / 2;\n if (!matrix.get(Math.round(x), Math.round(y))) {\n continue;\n }\n\n const lengths = [quad.top.endX - quad.top.startX, quad.bottom.endX - quad.bottom.startX, quad.bottom.y - quad.top.y + 1];\n const size = sum(lengths) / lengths.length;\n // Initial scoring of finder pattern quads by looking at their ratios, not taking into account position\n const score = scorePattern({x: Math.round(x), y: Math.round(y)}, [1, 1, 3, 1, 1], matrix);\n scoredFinderPatternPositions.push({ score, x, y, size });\n }\n if (scoredFinderPatternPositions.length < 3) {\n // A QR code has 3 finder patterns, therefore we need at least 3 candidates.\n return null;\n }\n scoredFinderPatternPositions.sort((a, b) => a.score - b.score);\n\n // Now take the top finder pattern options and try to find 2 other options with a similar size.\n const finderPatternGroups: Array<{ points: [Point, Point, Point], score: number }> = [];\n for (let i = 0; i < Math.min(scoredFinderPatternPositions.length, MAX_FINDERPATTERNS_TO_SEARCH); ++i) {\n const point = scoredFinderPatternPositions[i];\n const otherPoints: typeof scoredFinderPatternPositions = [];\n\n for (const otherPoint of scoredFinderPatternPositions) {\n if (otherPoint === point) {\n continue;\n }\n otherPoints.push({\n ...otherPoint,\n score: otherPoint.score + ((otherPoint.size - point.size) ** 2) / point.size, // score similarity of sizes\n });\n }\n otherPoints.sort((a, b) => a.score - b.score);\n\n finderPatternGroups.push({\n points: [point, otherPoints[0], otherPoints[1]], // note that otherPoints.length >= 2 as scoredFinderPatternPositions.length >= 3\n score: point.score + otherPoints[0].score + otherPoints[1].score, // total combined score of the three points in the group\n });\n }\n finderPatternGroups.sort((a, b) => a.score - b.score);\n const bestFinderPatternGroup = finderPatternGroups[0];\n\n const { topRight, topLeft, bottomLeft } = reorderFinderPatterns(...bestFinderPatternGroup.points);\n const alignment = findAlignmentPattern(matrix, alignmentPatternQuads, topRight, topLeft, bottomLeft);\n const result: QRLocation[] = [];\n if (alignment) {\n result.push({\n alignmentPattern: { x: alignment.alignmentPattern.x, y: alignment.alignmentPattern.y },\n bottomLeft: {x: bottomLeft.x, y: bottomLeft.y },\n dimension: alignment.dimension,\n topLeft: {x: topLeft.x, y: topLeft.y },\n topRight: {x: topRight.x, y: topRight.y },\n });\n }\n\n // We normally use the center of the quads as the location of the tracking points, which is optimal for most cases and will account\n // for a skew in the image. However, In some cases, a slight skew might not be real and instead be caused by image compression\n // errors and/or low resolution. For those cases, we'd be better off centering the point exactly in the middle of the black area. We\n // compute and return the location data for the naively centered points as it is little additional work and allows for multiple\n // attempts at decoding harder images.\n const midTopRight = recenterLocation(matrix, topRight);\n const midTopLeft = recenterLocation(matrix, topLeft);\n const midBottomLeft = recenterLocation(matrix, bottomLeft);\n const centeredAlignment = findAlignmentPattern(matrix, alignmentPatternQuads, midTopRight, midTopLeft, midBottomLeft);\n if (centeredAlignment) {\n result.push({\n alignmentPattern: { x: centeredAlignment.alignmentPattern.x, y: centeredAlignment.alignmentPattern.y },\n bottomLeft: { x: midBottomLeft.x, y: midBottomLeft. y },\n topLeft: { x: midTopLeft.x, y: midTopLeft. y },\n topRight: { x: midTopRight.x, y: midTopRight. y },\n dimension: centeredAlignment.dimension,\n });\n }\n\n if (result.length === 0) {\n return null;\n }\n\n return result;\n}\n\nfunction findAlignmentPattern(matrix: BitMatrix, alignmentPatternQuads: Quad[], topRight: Point, topLeft: Point, bottomLeft: Point) {\n // Now that we've found the three finder patterns we can determine the blockSize and the size of the QR code.\n // We'll use these to help find the alignment pattern but also later when we do the extraction.\n let dimension: number;\n let moduleSize: number;\n try {\n ({ dimension, moduleSize } = computeDimension(topLeft, topRight, bottomLeft, matrix));\n } catch (e) {\n return null;\n }\n\n // Now find the alignment pattern\n const bottomRightFinderPattern = { // Best guess at where a bottomRight finder pattern would be\n x: topRight.x - topLeft.x + bottomLeft.x,\n y: topRight.y - topLeft.y + bottomLeft.y,\n };\n const modulesBetweenFinderPatterns = ((distance(topLeft, bottomLeft) + distance(topLeft, topRight)) / 2 / moduleSize);\n const correctionToTopLeft = 1 - (3 / modulesBetweenFinderPatterns);\n const expectedAlignmentPattern = {\n x: topLeft.x + correctionToTopLeft * (bottomRightFinderPattern.x - topLeft.x),\n y: topLeft.y + correctionToTopLeft * (bottomRightFinderPattern.y - topLeft.y),\n };\n\n const alignmentPatterns = alignmentPatternQuads\n .map(q => {\n const x = (q.top.startX + q.top.endX + q.bottom.startX + q.bottom.endX) / 4;\n const y = (q.top.y + q.bottom.y + 1) / 2;\n if (!matrix.get(Math.floor(x), Math.floor(y))) {\n return;\n }\n\n const sizeScore = scorePattern({x: Math.floor(x), y: Math.floor(y)}, [1, 1, 1], matrix);\n const score = sizeScore + distance({x, y}, expectedAlignmentPattern);\n return { x, y, score };\n })\n .filter(v => !!v)\n .sort((a, b) => a.score - b.score);\n\n // If there are less than 15 modules between finder patterns it's a version 1 QR code and as such has no alignmemnt pattern\n // so we can only use our best guess.\n const alignmentPattern = modulesBetweenFinderPatterns >= 15 && alignmentPatterns.length ? alignmentPatterns[0] : expectedAlignmentPattern;\n\n return { alignmentPattern, dimension };\n}\n","import {binarize} from \"./binarizer\";\nimport {BitMatrix} from \"./BitMatrix\";\nimport {Chunks} from \"./decoder/decodeData\";\nimport {decode} from \"./decoder/decoder\";\nimport { Version } from \"./decoder/version\";\nimport {extract} from \"./extractor\";\nimport {locate, Point} from \"./locator\";\n\nexport interface QRCode {\n binaryData: number[];\n data: string;\n chunks: Chunks;\n version: number;\n location: {\n topRightCorner: Point;\n topLeftCorner: Point;\n bottomRightCorner: Point;\n bottomLeftCorner: Point;\n\n topRightFinderPattern: Point;\n topLeftFinderPattern: Point;\n bottomLeftFinderPattern: Point;\n\n bottomRightAlignmentPattern?: Point;\n };\n matrix: BitMatrix;\n}\n\nfunction scan(matrix: BitMatrix): QRCode | null {\n const locations = locate(matrix);\n if (!locations) {\n return null;\n }\n\n for (const location of locations) {\n const extracted = extract(matrix, location);\n const decoded = decode(extracted.matrix);\n if (decoded) {\n return {\n binaryData: decoded.bytes,\n data: decoded.text,\n chunks: decoded.chunks,\n version: decoded.version,\n location: {\n topRightCorner: extracted.mappingFunction(location.dimension, 0),\n topLeftCorner: extracted.mappingFunction(0, 0),\n bottomRightCorner: extracted.mappingFunction(location.dimension, location.dimension),\n bottomLeftCorner: extracted.mappingFunction(0, location.dimension),\n\n topRightFinderPattern: location.topRight,\n topLeftFinderPattern: location.topLeft,\n bottomLeftFinderPattern: location.bottomLeft,\n\n bottomRightAlignmentPattern: location.alignmentPattern,\n },\n matrix: extracted.matrix,\n };\n }\n }\n return null;\n}\n\nexport interface Options {\n inversionAttempts?: \"dontInvert\" | \"onlyInvert\" | \"attemptBoth\" | \"invertFirst\";\n greyScaleWeights?: GreyscaleWeights;\n canOverwriteImage?: boolean;\n}\n\nexport interface GreyscaleWeights {\n red: number;\n green: number;\n blue: number;\n useIntegerApproximation?: boolean;\n}\n\nconst defaultOptions: Options = {\n inversionAttempts: \"attemptBoth\",\n greyScaleWeights: {\n red: 0.2126,\n green: 0.7152,\n blue: 0.0722,\n useIntegerApproximation: false,\n },\n canOverwriteImage: true,\n};\n\nfunction mergeObject(target: any, src: any) {\n Object.keys(src).forEach(opt => { // Sad implementation of Object.assign since we target es5 not es6\n target[opt] = src[opt];\n });\n}\n\nfunction jsQR(data: Uint8ClampedArray, width: number, height: number, providedOptions: Options = {}): QRCode | null {\n const options = Object.create(null);\n mergeObject(options, defaultOptions);\n mergeObject(options, providedOptions);\n\n const tryInvertedFirst = options.inversionAttempts === \"onlyInvert\" || options.inversionAttempts === \"invertFirst\";\n const shouldInvert = options.inversionAttempts === \"attemptBoth\" || tryInvertedFirst;\n const {binarized, inverted} = binarize(data, width, height, shouldInvert, options.greyScaleWeights,\n options.canOverwriteImage);\n let result = scan(tryInvertedFirst ? inverted : binarized);\n if (!result && (options.inversionAttempts === \"attemptBoth\" || options.inversionAttempts === \"invertFirst\")) {\n result = scan(tryInvertedFirst ? binarized : inverted);\n }\n return result;\n}\n\n(jsQR as any).default = jsQR;\nexport default jsQR;\n","import {BitMatrix} from \"../BitMatrix\";\nimport {GreyscaleWeights} from \"../index\";\n\nconst REGION_SIZE = 8;\nconst MIN_DYNAMIC_RANGE = 24;\n\nfunction numBetween(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\n// Like BitMatrix but accepts arbitry Uint8 values\nclass Matrix {\n private data: Uint8ClampedArray;\n private width: number;\n constructor(width: number, height: number, buffer?: Uint8ClampedArray) {\n this.width = width;\n const bufferSize = width * height;\n if (buffer && buffer.length !== bufferSize) {\n throw new Error(\"Wrong buffer size\");\n }\n this.data = buffer || new Uint8ClampedArray(bufferSize);\n }\n public get(x: number, y: number) {\n return this.data[y * this.width + x];\n }\n public set(x: number, y: number, value: number) {\n this.data[y * this.width + x] = value;\n }\n}\n\nexport function binarize(data: Uint8ClampedArray, width: number, height: number, returnInverted: boolean,\n greyscaleWeights: GreyscaleWeights, canOverwriteImage: boolean) {\n const pixelCount = width * height;\n if (data.length !== pixelCount * 4) {\n throw new Error(\"Malformed data passed to binarizer.\");\n }\n // assign the greyscale and binary image within the rgba buffer as the rgba image will not be needed after conversion\n let bufferOffset = 0;\n // Convert image to greyscale\n let greyscaleBuffer: Uint8ClampedArray;\n if (canOverwriteImage) {\n greyscaleBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);\n bufferOffset += pixelCount;\n }\n const greyscalePixels = new Matrix(width, height, greyscaleBuffer);\n if (greyscaleWeights.useIntegerApproximation) {\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const pixelPosition = (y * width + x) * 4;\n const r = data[pixelPosition];\n const g = data[pixelPosition + 1];\n const b = data[pixelPosition + 2];\n greyscalePixels.set(x, y,\n // tslint:disable-next-line no-bitwise\n (greyscaleWeights.red * r + greyscaleWeights.green * g + greyscaleWeights.blue * b + 128) >> 8);\n }\n }\n } else {\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const pixelPosition = (y * width + x) * 4;\n const r = data[pixelPosition];\n const g = data[pixelPosition + 1];\n const b = data[pixelPosition + 2];\n greyscalePixels.set(x, y,\n greyscaleWeights.red * r + greyscaleWeights.green * g + greyscaleWeights.blue * b);\n }\n }\n }\n const horizontalRegionCount = Math.ceil(width / REGION_SIZE);\n const verticalRegionCount = Math.ceil(height / REGION_SIZE);\n const blackPointsCount = horizontalRegionCount * verticalRegionCount;\n\n let blackPointsBuffer: Uint8ClampedArray;\n if (canOverwriteImage) {\n blackPointsBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, blackPointsCount);\n bufferOffset += blackPointsCount;\n }\n const blackPoints = new Matrix(horizontalRegionCount, verticalRegionCount, blackPointsBuffer);\n for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) {\n for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) {\n let min = Infinity;\n let max = 0;\n for (let y = 0; y < REGION_SIZE; y++) {\n for (let x = 0; x < REGION_SIZE; x++) {\n const pixelLumosity =\n greyscalePixels.get(hortizontalRegion * REGION_SIZE + x, verticalRegion * REGION_SIZE + y);\n min = Math.min(min, pixelLumosity);\n max = Math.max(max, pixelLumosity);\n }\n }\n // We could also compute the real average of all pixels but following the assumption that the qr code consists\n // of bright and dark pixels and essentially not much in between, by (min + max)/2 we make the cut really between\n // those two classes. If using the average over all pixel in a block of mostly bright pixels and few dark pixels,\n // the avg would tend to the bright side and darker bright pixels could be interpreted as dark.\n let average = (min + max) / 2;\n // Small bias towards black by moving the threshold up. We do this, as in the finder patterns white holes tend\n // to appear which makes them undetectable.\n const blackBias = 1.11;\n average = Math.min(255, average * blackBias);\n if (max - min <= MIN_DYNAMIC_RANGE) {\n // If variation within the block is low, assume this is a block with only light or only\n // dark pixels. In that case we do not want to use the average, as it would divide this\n // low contrast area into black and white pixels, essentially creating data out of noise.\n //\n // Default the blackpoint for these blocks to be half the min - effectively white them out\n average = min / 2;\n\n if (verticalRegion > 0 && hortizontalRegion > 0) {\n // Correct the \"white background\" assumption for blocks that have neighbors by comparing\n // the pixels in this block to the previously calculated black points. This is based on\n // the fact that dark barcode symbology is always surrounded by some amount of light\n // background for which reasonable black point estimates were made. The bp estimated at\n // the boundaries is used for the interior.\n\n // The (min < bp) is arbitrary but works better than other heuristics that were tried.\n const averageNeighborBlackPoint = (\n blackPoints.get(hortizontalRegion, verticalRegion - 1) +\n (2 * blackPoints.get(hortizontalRegion - 1, verticalRegion)) +\n blackPoints.get(hortizontalRegion - 1, verticalRegion - 1)\n ) / 4;\n if (min < averageNeighborBlackPoint) {\n average = averageNeighborBlackPoint; // no need to apply black bias as already applied to neighbors\n }\n }\n }\n blackPoints.set(hortizontalRegion, verticalRegion, average);\n }\n }\n\n let binarized: BitMatrix;\n if (canOverwriteImage) {\n const binarizedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);\n bufferOffset += pixelCount;\n binarized = new BitMatrix(binarizedBuffer, width);\n } else {\n binarized = BitMatrix.createEmpty(width, height);\n }\n\n let inverted: BitMatrix = null;\n if (returnInverted) {\n if (canOverwriteImage) {\n const invertedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);\n inverted = new BitMatrix(invertedBuffer, width);\n } else {\n inverted = BitMatrix.createEmpty(width, height);\n }\n }\n\n for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) {\n for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) {\n const left = numBetween(hortizontalRegion, 2, horizontalRegionCount - 3);\n const top = numBetween(verticalRegion, 2, verticalRegionCount - 3);\n let sum = 0;\n for (let xRegion = -2; xRegion <= 2; xRegion++) {\n for (let yRegion = -2; yRegion <= 2; yRegion++) {\n sum += blackPoints.get(left + xRegion, top + yRegion);\n }\n }\n const threshold = sum / 25;\n for (let xRegion = 0; xRegion < REGION_SIZE; xRegion++) {\n for (let yRegion = 0; yRegion < REGION_SIZE; yRegion++) {\n const x = hortizontalRegion * REGION_SIZE + xRegion;\n const y = verticalRegion * REGION_SIZE + yRegion;\n const lum = greyscalePixels.get(x, y);\n binarized.set(x, y, lum <= threshold);\n if (returnInverted) {\n inverted.set(x, y, !(lum <= threshold));\n }\n }\n }\n }\n }\n if (returnInverted) {\n return { binarized, inverted };\n }\n return { binarized };\n}\n","export class BitMatrix {\n public static createEmpty(width: number, height: number) {\n return new BitMatrix(new Uint8ClampedArray(width * height), width);\n }\n\n public width: number;\n public height: number;\n private data: Uint8ClampedArray;\n\n constructor(data: Uint8ClampedArray, width: number) {\n this.width = width;\n this.height = data.length / width;\n this.data = data;\n }\n\n public get(x: number, y: number): boolean {\n if (x < 0 || x >= this.width || y < 0 || y >= this.height) {\n return false;\n }\n return !!this.data[y * this.width + x];\n }\n\n public set(x: number, y: number, v: boolean) {\n this.data[y * this.width + x] = v ? 1 : 0;\n }\n\n public setRegion(left: number, top: number, width: number, height: number, v: boolean) {\n for (let y = top; y < top + height; y++) {\n for (let x = left; x < left + width; x++) {\n this.set(x, y, !!v);\n }\n }\n }\n}\n","// tslint:disable:no-bitwise\n\nexport class BitStream {\n private bytes: Uint8ClampedArray;\n private byteOffset: number = 0;\n private bitOffset: number = 0;\n\n constructor(bytes: Uint8ClampedArray) {\n this.bytes = bytes;\n }\n\n public readBits(numBits: number): number {\n if (numBits < 1 || numBits > 32 || numBits > this.available()) {\n throw new Error(\"Cannot read \" + numBits.toString() + \" bits\");\n }\n\n let result = 0;\n // First, read remainder from current byte\n if (this.bitOffset > 0) {\n const bitsLeft = 8 - this.bitOffset;\n const toRead = numBits < bitsLeft ? numBits : bitsLeft;\n const bitsToNotRead = bitsLeft - toRead;\n const mask = (0xFF >> (8 - toRead)) << bitsToNotRead;\n result = (this.bytes[this.byteOffset] & mask) >> bitsToNotRead;\n numBits -= toRead;\n this.bitOffset += toRead;\n if (this.bitOffset === 8) {\n this.bitOffset = 0;\n this.byteOffset++;\n }\n }\n\n // Next read whole bytes\n if (numBits > 0) {\n while (numBits >= 8) {\n result = (result << 8) | (this.bytes[this.byteOffset] & 0xFF);\n this.byteOffset++;\n numBits -= 8;\n }\n\n // Finally read a partial byte\n if (numBits > 0) {\n const bitsToNotRead = 8 - numBits;\n const mask = (0xFF >> bitsToNotRead) << bitsToNotRead;\n result = (result << numBits) | ((this.bytes[this.byteOffset] & mask) >> bitsToNotRead);\n this.bitOffset += numBits;\n }\n }\n return result;\n }\n\n public available(): number {\n return 8 * (this.bytes.length - this.byteOffset) - this.bitOffset;\n }\n}\n","import GenericGF, { addOrSubtractGF } from \"./GenericGF\";\n\nexport default class GenericGFPoly {\n private field: GenericGF;\n private coefficients: Uint8ClampedArray;\n\n constructor(field: GenericGF, coefficients: Uint8ClampedArray) {\n if (coefficients.length === 0) {\n throw new Error(\"No coefficients.\");\n }\n this.field = field;\n const coefficientsLength = coefficients.length;\n if (coefficientsLength > 1 && coefficients[0] === 0) {\n // Leading term must be non-zero for anything except the constant polynomial \"0\"\n let firstNonZero = 1;\n while (firstNonZero < coefficientsLength && coefficients[firstNonZero] === 0) {\n firstNonZero++;\n }\n if (firstNonZero === coefficientsLength) {\n this.coefficients = field.zero.coefficients;\n } else {\n this.coefficients = new Uint8ClampedArray(coefficientsLength - firstNonZero);\n for (let i = 0; i < this.coefficients.length; i++) {\n this.coefficients[i] = coefficients[firstNonZero + i];\n }\n }\n } else {\n this.coefficients = coefficients;\n }\n }\n\n public degree() {\n return this.coefficients.length - 1;\n }\n\n public isZero() {\n return this.coefficients[0] === 0;\n }\n\n public getCoefficient(degree: number) {\n return this.coefficients[this.coefficients.length - 1 - degree];\n }\n\n public addOrSubtract(other: GenericGFPoly) {\n if (this.isZero()) {\n return other;\n }\n if (other.isZero()) {\n return this;\n }\n\n let smallerCoefficients = this.coefficients;\n let largerCoefficients = other.coefficients;\n if (smallerCoefficients.length > largerCoefficients.length) {\n [smallerCoefficients, largerCoefficients] = [largerCoefficients, smallerCoefficients];\n }\n const sumDiff = new Uint8ClampedArray(largerCoefficients.length);\n const lengthDiff = largerCoefficients.length - smallerCoefficients.length;\n for (let i = 0; i < lengthDiff; i++) {\n sumDiff[i] = largerCoefficients[i];\n }\n\n for (let i = lengthDiff; i < largerCoefficients.length; i++) {\n sumDiff[i] = addOrSubtractGF(smallerCoefficients[i - lengthDiff], largerCoefficients[i]);\n }\n\n return new GenericGFPoly(this.field, sumDiff);\n }\n\n public multiply(scalar: number) {\n if (scalar === 0) {\n return this.field.zero;\n }\n if (scalar === 1) {\n return this;\n }\n const size = this.coefficients.length;\n const product = new Uint8ClampedArray(size);\n for (let i = 0; i < size; i++) {\n product[i] = this.field.multiply(this.coefficients[i], scalar);\n }\n\n return new GenericGFPoly(this.field, product);\n }\n\n public multiplyPoly(other: GenericGFPoly): GenericGFPoly {\n if (this.isZero() || other.isZero()) {\n return this.field.zero;\n }\n const aCoefficients = this.coefficients;\n const aLength = aCoefficients.length;\n const bCoefficients = other.coefficients;\n const bLength = bCoefficients.length;\n const product = new Uint8ClampedArray(aLength + bLength - 1);\n for (let i = 0; i < aLength; i++) {\n const aCoeff = aCoefficients[i];\n for (let j = 0; j < bLength; j++) {\n product[i + j] = addOrSubtractGF(product[i + j],\n this.field.multiply(aCoeff, bCoefficients[j]));\n }\n }\n return new GenericGFPoly(this.field, product);\n }\n\n public multiplyByMonomial(degree: number, coefficient: number) {\n if (degree < 0) {\n throw new Error(\"Invalid degree less than 0\");\n }\n if (coefficient === 0) {\n return this.field.zero;\n }\n const size = this.coefficients.length;\n const product = new Uint8ClampedArray(size + degree);\n for (let i = 0; i < size; i++) {\n product[i] = this.field.multiply(this.coefficients[i], coefficient);\n }\n return new GenericGFPoly(this.field, product);\n }\n\n public evaluateAt(a: number) {\n let result = 0;\n if (a === 0) {\n // Just return the x^0 coefficient\n return this.getCoefficient(0);\n }\n const size = this.coefficients.length;\n if (a === 1) {\n // Just the sum of the coefficients\n this.coefficients.forEach((coefficient) => {\n result = addOrSubtractGF(result, coefficient);\n });\n return result;\n }\n result = this.coefficients[0];\n for (let i = 1; i < size; i++) {\n result = addOrSubtractGF(this.field.multiply(a, result), this.coefficients[i]);\n }\n return result;\n }\n}\n","export interface Version {\n infoBits: number;\n versionNumber: number;\n alignmentPatternCenters: number[];\n errorCorrectionLevels: Array<{\n ecCodewordsPerBlock: number;\n ecBlocks: Array<{\n numBlocks: number;\n dataCodewordsPerBlock: number;\n }>\n }>;\n}\n\nexport const VERSIONS: Version[] = [\n {\n infoBits: null,\n versionNumber: 1,\n alignmentPatternCenters: [],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 7,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 19 }],\n },\n {\n ecCodewordsPerBlock: 10,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 16 }],\n },\n {\n ecCodewordsPerBlock: 13,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 13 }],\n },\n {\n ecCodewordsPerBlock: 17,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 9 }],\n },\n ],\n },\n {\n infoBits: null,\n versionNumber: 2,\n alignmentPatternCenters: [6, 18],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 10,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 34 }],\n },\n {\n ecCodewordsPerBlock: 16,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 28 }],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 22 }],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 16 }],\n },\n ],\n },\n {\n infoBits: null,\n versionNumber: 3,\n alignmentPatternCenters: [6, 22],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 15,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 55 }],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 44 }],\n },\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 17 }],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 13 }],\n },\n ],\n },\n {\n infoBits: null,\n versionNumber: 4,\n alignmentPatternCenters: [6, 26],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 20,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 80 }],\n },\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 32 }],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 24 }],\n },\n {\n ecCodewordsPerBlock: 16,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 9 }],\n },\n ],\n },\n {\n infoBits: null,\n versionNumber: 5,\n alignmentPatternCenters: [6, 30],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 108 }],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 43 }],\n },\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 15 },\n { numBlocks: 2, dataCodewordsPerBlock: 16 },\n ],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 11 },\n { numBlocks: 2, dataCodewordsPerBlock: 12 },\n ],\n },\n ],\n },\n {\n infoBits: null,\n versionNumber: 6,\n alignmentPatternCenters: [6, 34],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 68 }],\n },\n {\n ecCodewordsPerBlock: 16,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 27 }],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 19 }],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 15 }],\n },\n ],\n },\n {\n infoBits: 0x07C94,\n versionNumber: 7,\n alignmentPatternCenters: [6, 22, 38],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 20,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 78 }],\n },\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 31 }],\n },\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 14 },\n { numBlocks: 4, dataCodewordsPerBlock: 15 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 13 },\n { numBlocks: 1, dataCodewordsPerBlock: 14 },\n ],\n },\n ],\n },\n {\n infoBits: 0x085BC,\n versionNumber: 8,\n alignmentPatternCenters: [6, 24, 42],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 97 }],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 38 },\n { numBlocks: 2, dataCodewordsPerBlock: 39 },\n ],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 18 },\n { numBlocks: 2, dataCodewordsPerBlock: 19 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 14 },\n { numBlocks: 2, dataCodewordsPerBlock: 15 },\n ],\n },\n ],\n },\n {\n infoBits: 0x09A99,\n versionNumber: 9,\n alignmentPatternCenters: [6, 26, 46],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 116 }],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 36 },\n { numBlocks: 2, dataCodewordsPerBlock: 37 },\n ],\n },\n {\n ecCodewordsPerBlock: 20,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 16 },\n { numBlocks: 4, dataCodewordsPerBlock: 17 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 12 },\n { numBlocks: 4, dataCodewordsPerBlock: 13 },\n ],\n },\n ],\n },\n {\n infoBits: 0x0A4D3,\n versionNumber: 10,\n alignmentPatternCenters: [6, 28, 50],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 18,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 68 },\n { numBlocks: 2, dataCodewordsPerBlock: 69 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 43 },\n { numBlocks: 1, dataCodewordsPerBlock: 44 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 19 },\n { numBlocks: 2, dataCodewordsPerBlock: 20 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 15 },\n { numBlocks: 2, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x0BBF6,\n versionNumber: 11,\n alignmentPatternCenters: [6, 30, 54],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 20,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 81 }],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 1, dataCodewordsPerBlock: 50 },\n { numBlocks: 4, dataCodewordsPerBlock: 51 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 22 },\n { numBlocks: 4, dataCodewordsPerBlock: 23 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 12 },\n { numBlocks: 8, dataCodewordsPerBlock: 13 },\n ],\n },\n ],\n },\n {\n infoBits: 0x0C762,\n versionNumber: 12,\n alignmentPatternCenters: [6, 32, 58],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 92 },\n { numBlocks: 2, dataCodewordsPerBlock: 93 },\n ],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 36 },\n { numBlocks: 2, dataCodewordsPerBlock: 37 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 20 },\n { numBlocks: 6, dataCodewordsPerBlock: 21 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 7, dataCodewordsPerBlock: 14 },\n { numBlocks: 4, dataCodewordsPerBlock: 15 },\n ],\n },\n ],\n },\n {\n infoBits: 0x0D847,\n versionNumber: 13,\n alignmentPatternCenters: [6, 34, 62],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 107 }],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 8, dataCodewordsPerBlock: 37 },\n { numBlocks: 1, dataCodewordsPerBlock: 38 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 8, dataCodewordsPerBlock: 20 },\n { numBlocks: 4, dataCodewordsPerBlock: 21 },\n ],\n },\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 12, dataCodewordsPerBlock: 11 },\n { numBlocks: 4, dataCodewordsPerBlock: 12 },\n ],\n },\n ],\n },\n {\n infoBits: 0x0E60D,\n versionNumber: 14,\n alignmentPatternCenters: [6, 26, 46, 66],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 115 },\n { numBlocks: 1, dataCodewordsPerBlock: 116 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 40 },\n { numBlocks: 5, dataCodewordsPerBlock: 41 },\n ],\n },\n {\n ecCodewordsPerBlock: 20,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 16 },\n { numBlocks: 5, dataCodewordsPerBlock: 17 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 12 },\n { numBlocks: 5, dataCodewordsPerBlock: 13 },\n ],\n },\n ],\n },\n {\n infoBits: 0x0F928,\n versionNumber: 15,\n alignmentPatternCenters: [6, 26, 48, 70],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 22,\n ecBlocks: [\n { numBlocks: 5, dataCodewordsPerBlock: 87 },\n { numBlocks: 1, dataCodewordsPerBlock: 88 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 5, dataCodewordsPerBlock: 41 },\n { numBlocks: 5, dataCodewordsPerBlock: 42 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 5, dataCodewordsPerBlock: 24 },\n { numBlocks: 7, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 12 },\n { numBlocks: 7, dataCodewordsPerBlock: 13 },\n ],\n },\n ],\n },\n {\n infoBits: 0x10B78,\n versionNumber: 16,\n alignmentPatternCenters: [6, 26, 50, 74],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 5, dataCodewordsPerBlock: 98 },\n { numBlocks: 1, dataCodewordsPerBlock: 99 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 7, dataCodewordsPerBlock: 45 },\n { numBlocks: 3, dataCodewordsPerBlock: 46 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [\n { numBlocks: 15, dataCodewordsPerBlock: 19 },\n { numBlocks: 2, dataCodewordsPerBlock: 20 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 15 },\n { numBlocks: 13, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1145D,\n versionNumber: 17,\n alignmentPatternCenters: [6, 30, 54, 78],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 1, dataCodewordsPerBlock: 107 },\n { numBlocks: 5, dataCodewordsPerBlock: 108 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 10, dataCodewordsPerBlock: 46 },\n { numBlocks: 1, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 1, dataCodewordsPerBlock: 22 },\n { numBlocks: 15, dataCodewordsPerBlock: 23 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 14 },\n { numBlocks: 17, dataCodewordsPerBlock: 15 },\n ],\n },\n ],\n },\n {\n infoBits: 0x12A17,\n versionNumber: 18,\n alignmentPatternCenters: [6, 30, 56, 82],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 5, dataCodewordsPerBlock: 120 },\n { numBlocks: 1, dataCodewordsPerBlock: 121 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 9, dataCodewordsPerBlock: 43 },\n { numBlocks: 4, dataCodewordsPerBlock: 44 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 17, dataCodewordsPerBlock: 22 },\n { numBlocks: 1, dataCodewordsPerBlock: 23 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 14 },\n { numBlocks: 19, dataCodewordsPerBlock: 15 },\n ],\n },\n ],\n },\n {\n infoBits: 0x13532,\n versionNumber: 19,\n alignmentPatternCenters: [6, 30, 58, 86],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 113 },\n { numBlocks: 4, dataCodewordsPerBlock: 114 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 44 },\n { numBlocks: 11, dataCodewordsPerBlock: 45 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 17, dataCodewordsPerBlock: 21 },\n { numBlocks: 4, dataCodewordsPerBlock: 22 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 9, dataCodewordsPerBlock: 13 },\n { numBlocks: 16, dataCodewordsPerBlock: 14 },\n ],\n },\n ],\n },\n {\n infoBits: 0x149A6,\n versionNumber: 20,\n alignmentPatternCenters: [6, 34, 62, 90],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 107 },\n { numBlocks: 5, dataCodewordsPerBlock: 108 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 41 },\n { numBlocks: 13, dataCodewordsPerBlock: 42 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 15, dataCodewordsPerBlock: 24 },\n { numBlocks: 5, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 15, dataCodewordsPerBlock: 15 },\n { numBlocks: 10, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x15683,\n versionNumber: 21,\n alignmentPatternCenters: [6, 28, 50, 72, 94],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 116 },\n { numBlocks: 4, dataCodewordsPerBlock: 117 },\n ],\n },\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [{ numBlocks: 17, dataCodewordsPerBlock: 42 }],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 17, dataCodewordsPerBlock: 22 },\n { numBlocks: 6, dataCodewordsPerBlock: 23 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 19, dataCodewordsPerBlock: 16 },\n { numBlocks: 6, dataCodewordsPerBlock: 17 },\n ],\n },\n ],\n },\n {\n infoBits: 0x168C9,\n versionNumber: 22,\n alignmentPatternCenters: [6, 26, 50, 74, 98],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 111 },\n { numBlocks: 7, dataCodewordsPerBlock: 112 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [{ numBlocks: 17, dataCodewordsPerBlock: 46 }],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 7, dataCodewordsPerBlock: 24 },\n { numBlocks: 16, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 24,\n ecBlocks: [{ numBlocks: 34, dataCodewordsPerBlock: 13 }],\n },\n ],\n },\n {\n infoBits: 0x177EC,\n versionNumber: 23,\n alignmentPatternCenters: [6, 30, 54, 74, 102],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 121 },\n { numBlocks: 5, dataCodewordsPerBlock: 122 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 47 },\n { numBlocks: 14, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 24 },\n { numBlocks: 14, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 16, dataCodewordsPerBlock: 15 },\n { numBlocks: 14, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x18EC4,\n versionNumber: 24,\n alignmentPatternCenters: [6, 28, 54, 80, 106],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 117 },\n { numBlocks: 4, dataCodewordsPerBlock: 118 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 45 },\n { numBlocks: 14, dataCodewordsPerBlock: 46 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 24 },\n { numBlocks: 16, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 30, dataCodewordsPerBlock: 16 },\n { numBlocks: 2, dataCodewordsPerBlock: 17 },\n ],\n },\n ],\n },\n {\n infoBits: 0x191E1,\n versionNumber: 25,\n alignmentPatternCenters: [6, 32, 58, 84, 110],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 26,\n ecBlocks: [\n { numBlocks: 8, dataCodewordsPerBlock: 106 },\n { numBlocks: 4, dataCodewordsPerBlock: 107 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 8, dataCodewordsPerBlock: 47 },\n { numBlocks: 13, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 7, dataCodewordsPerBlock: 24 },\n { numBlocks: 22, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 22, dataCodewordsPerBlock: 15 },\n { numBlocks: 13, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1AFAB,\n versionNumber: 26,\n alignmentPatternCenters: [6, 30, 58, 86, 114],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 10, dataCodewordsPerBlock: 114 },\n { numBlocks: 2, dataCodewordsPerBlock: 115 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 19, dataCodewordsPerBlock: 46 },\n { numBlocks: 4, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 28, dataCodewordsPerBlock: 22 },\n { numBlocks: 6, dataCodewordsPerBlock: 23 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 33, dataCodewordsPerBlock: 16 },\n { numBlocks: 4, dataCodewordsPerBlock: 17 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1B08E,\n versionNumber: 27,\n alignmentPatternCenters: [6, 34, 62, 90, 118],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 8, dataCodewordsPerBlock: 122 },\n { numBlocks: 4, dataCodewordsPerBlock: 123 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 22, dataCodewordsPerBlock: 45 },\n { numBlocks: 3, dataCodewordsPerBlock: 46 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 8, dataCodewordsPerBlock: 23 },\n { numBlocks: 26, dataCodewordsPerBlock: 24 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 12, dataCodewordsPerBlock: 15 },\n { numBlocks: 28, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1CC1A,\n versionNumber: 28,\n alignmentPatternCenters: [6, 26, 50, 74, 98, 122],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 117 },\n { numBlocks: 10, dataCodewordsPerBlock: 118 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 3, dataCodewordsPerBlock: 45 },\n { numBlocks: 23, dataCodewordsPerBlock: 46 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 24 },\n { numBlocks: 31, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 15 },\n { numBlocks: 31, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1D33F,\n versionNumber: 29,\n alignmentPatternCenters: [6, 30, 54, 78, 102, 126],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 7, dataCodewordsPerBlock: 116 },\n { numBlocks: 7, dataCodewordsPerBlock: 117 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 21, dataCodewordsPerBlock: 45 },\n { numBlocks: 7, dataCodewordsPerBlock: 46 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 1, dataCodewordsPerBlock: 23 },\n { numBlocks: 37, dataCodewordsPerBlock: 24 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 19, dataCodewordsPerBlock: 15 },\n { numBlocks: 26, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1ED75,\n versionNumber: 30,\n alignmentPatternCenters: [6, 26, 52, 78, 104, 130],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 5, dataCodewordsPerBlock: 115 },\n { numBlocks: 10, dataCodewordsPerBlock: 116 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 19, dataCodewordsPerBlock: 47 },\n { numBlocks: 10, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 15, dataCodewordsPerBlock: 24 },\n { numBlocks: 25, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 23, dataCodewordsPerBlock: 15 },\n { numBlocks: 25, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x1F250,\n versionNumber: 31,\n alignmentPatternCenters: [6, 30, 56, 82, 108, 134],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 13, dataCodewordsPerBlock: 115 },\n { numBlocks: 3, dataCodewordsPerBlock: 116 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 46 },\n { numBlocks: 29, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 42, dataCodewordsPerBlock: 24 },\n { numBlocks: 1, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 23, dataCodewordsPerBlock: 15 },\n { numBlocks: 28, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x209D5,\n versionNumber: 32,\n alignmentPatternCenters: [6, 34, 60, 86, 112, 138],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [{ numBlocks: 17, dataCodewordsPerBlock: 115 }],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 10, dataCodewordsPerBlock: 46 },\n { numBlocks: 23, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 10, dataCodewordsPerBlock: 24 },\n { numBlocks: 35, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 19, dataCodewordsPerBlock: 15 },\n { numBlocks: 35, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x216F0,\n versionNumber: 33,\n alignmentPatternCenters: [6, 30, 58, 86, 114, 142],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 17, dataCodewordsPerBlock: 115 },\n { numBlocks: 1, dataCodewordsPerBlock: 116 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 14, dataCodewordsPerBlock: 46 },\n { numBlocks: 21, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 29, dataCodewordsPerBlock: 24 },\n { numBlocks: 19, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 11, dataCodewordsPerBlock: 15 },\n { numBlocks: 46, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x228BA,\n versionNumber: 34,\n alignmentPatternCenters: [6, 34, 62, 90, 118, 146],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 13, dataCodewordsPerBlock: 115 },\n { numBlocks: 6, dataCodewordsPerBlock: 116 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 14, dataCodewordsPerBlock: 46 },\n { numBlocks: 23, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 44, dataCodewordsPerBlock: 24 },\n { numBlocks: 7, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 59, dataCodewordsPerBlock: 16 },\n { numBlocks: 1, dataCodewordsPerBlock: 17 },\n ],\n },\n ],\n },\n {\n infoBits: 0x2379F,\n versionNumber: 35,\n alignmentPatternCenters: [6, 30, 54, 78, 102, 126, 150],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 12, dataCodewordsPerBlock: 121 },\n { numBlocks: 7, dataCodewordsPerBlock: 122 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 12, dataCodewordsPerBlock: 47 },\n { numBlocks: 26, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 39, dataCodewordsPerBlock: 24 },\n { numBlocks: 14, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 22, dataCodewordsPerBlock: 15 },\n { numBlocks: 41, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x24B0B,\n versionNumber: 36,\n alignmentPatternCenters: [ 6, 24, 50, 76, 102, 128, 154 ],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 121 },\n { numBlocks: 14, dataCodewordsPerBlock: 122 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 6, dataCodewordsPerBlock: 47 },\n { numBlocks: 34, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 46, dataCodewordsPerBlock: 24 },\n { numBlocks: 10, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 2, dataCodewordsPerBlock: 15 },\n { numBlocks: 64, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x2542E,\n versionNumber: 37,\n alignmentPatternCenters: [ 6, 28, 54, 80, 106, 132, 158 ],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 17, dataCodewordsPerBlock: 122 },\n { numBlocks: 4, dataCodewordsPerBlock: 123 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 29, dataCodewordsPerBlock: 46 },\n { numBlocks: 14, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 49, dataCodewordsPerBlock: 24 },\n { numBlocks: 10, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 24, dataCodewordsPerBlock: 15 },\n { numBlocks: 46, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x26A64,\n versionNumber: 38,\n alignmentPatternCenters: [ 6, 32, 58, 84, 110, 136, 162 ],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 4, dataCodewordsPerBlock: 122 },\n { numBlocks: 18, dataCodewordsPerBlock: 123 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 13, dataCodewordsPerBlock: 46 },\n { numBlocks: 32, dataCodewordsPerBlock: 47 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 48, dataCodewordsPerBlock: 24 },\n { numBlocks: 14, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 42, dataCodewordsPerBlock: 15 },\n { numBlocks: 32, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x27541,\n versionNumber: 39,\n alignmentPatternCenters: [ 6, 26, 54, 82, 110, 138, 166 ],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 20, dataCodewordsPerBlock: 117 },\n { numBlocks: 4, dataCodewordsPerBlock: 118 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 40, dataCodewordsPerBlock: 47 },\n { numBlocks: 7, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 43, dataCodewordsPerBlock: 24 },\n { numBlocks: 22, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 10, dataCodewordsPerBlock: 15 },\n { numBlocks: 67, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n {\n infoBits: 0x28C69,\n versionNumber: 40,\n alignmentPatternCenters: [ 6, 30, 58, 86, 114, 142, 170 ],\n errorCorrectionLevels: [\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 19, dataCodewordsPerBlock: 118 },\n { numBlocks: 6, dataCodewordsPerBlock: 119 },\n ],\n },\n {\n ecCodewordsPerBlock: 28,\n ecBlocks: [\n { numBlocks: 18, dataCodewordsPerBlock: 47 },\n { numBlocks: 31, dataCodewordsPerBlock: 48 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 34, dataCodewordsPerBlock: 24 },\n { numBlocks: 34, dataCodewordsPerBlock: 25 },\n ],\n },\n {\n ecCodewordsPerBlock: 30,\n ecBlocks: [\n { numBlocks: 20, dataCodewordsPerBlock: 15 },\n { numBlocks: 61, dataCodewordsPerBlock: 16 },\n ],\n },\n ],\n },\n];\n","import jsQR from '../node_modules/jsqr-es6/dist/jsQR.js';\n\nlet inversionAttempts = 'dontInvert';\nlet grayscaleWeights = {\n // weights for quick luma integer approximation (https://en.wikipedia.org/wiki/YUV#Full_swing_for_BT.601)\n red: 77,\n green: 150,\n blue: 29,\n useIntegerApproximation: true,\n};\n\nself.onmessage = event => {\n const type = event['data']['type'];\n const data = event['data']['data'];\n\n switch (type) {\n case 'decode':\n decode(data);\n break;\n case 'grayscaleWeights':\n setGrayscaleWeights(data);\n break;\n case 'inversionMode':\n setInversionMode(data);\n break;\n case 'close':\n // close after earlier messages in the event loop finished processing\n self.close();\n break;\n }\n};\n\nfunction decode(data) {\n const rgbaData = data['data'];\n const width = data['width'];\n const height = data['height'];\n const result = jsQR(rgbaData, width, height, {\n inversionAttempts: inversionAttempts,\n greyScaleWeights: grayscaleWeights,\n });\n self.postMessage({\n type: 'qrResult',\n data: result? result.data : null,\n });\n}\n\nfunction setGrayscaleWeights(data) {\n // update grayscaleWeights in a closure compiler compatible fashion\n grayscaleWeights.red = data['red'];\n grayscaleWeights.green = data['green'];\n grayscaleWeights.blue = data['blue'];\n grayscaleWeights.useIntegerApproximation = data['useIntegerApproximation'];\n}\n\nfunction setInversionMode(inversionMode) {\n switch (inversionMode) {\n case 'original':\n inversionAttempts = 'dontInvert';\n break;\n case 'invert':\n inversionAttempts = 'onlyInvert';\n break;\n case 'both':\n inversionAttempts = 'attemptBoth';\n break;\n default:\n throw new Error('Invalid inversion mode');\n }\n}\n"],"names":["decodeByte","stream","size","text","i","length","bytes","push","b","decodeURIComponent","map","substr","toString","join","decode","data","version","chunks","available","mode","ModeByte","Terminator","result","ECI","readBits","type","Mode","assignmentNumber","Numeric","num","Error","a","c","numericResult","Alphanumeric","AlphanumericCharacterCodes","charCodeAt","alphanumericResult","Byte","byteResult","Kanji","Math","floor","k","kanjiResult","StructuredAppend","currentSequence","totalSequence","parity","addOrSubtractGF","runEuclideanAlgorithm","field","R","degree","tLast","zero","t","one","r","rLast","isZero","rLastLast","q","dltInverse","addOrSubtract","buildMonomial","degreeDiff","scale","multiplyByMonomial","multiplyPoly","tLastLast","sigmaTildeAtZero","multiply","inverse","twoS","outputBytes","set","error","s","syndromeCoefficients","evaluation","syndrome","sigmaOmega","numErrors","errorLocator","getCoefficient","errorCount","evaluateAt","errorLocations","denominator","j","xiInverse","errorEvaluator","generatorBase","position","numBitsDiffering","x","y","z","bitCount","pushBit","bit","byte","readCodewords","matrix","formatInfo","dimension","setRegion","versionNumber","bitsRead","currentByte","readingUp","columnIndex","columnOffset","get","dataMask","codewords","readVersion","provisionalVersion","VERSIONS","topRightVersionBits","bottomLeftVersionBits","bestDifference","Infinity","bestVersion","infoBits","difference","readFormatInformation","topLeftFormatInfoBits","topRightBottomRightFormatInfoBits","bestFormatInfo","bits","getDataBlocks","ecLevel","totalCodewords","ecInfo","ecBlocks","forEach","block","numBlocks","dataBlocks","numDataCodewords","dataCodewordsPerBlock","ecCodewordsPerBlock","slice","shortBlockSize","dataBlock","shift","largeBlockCount","smallBlockCount","decodeMatrix","errorCorrectionLevel","resultIndex","correctedBytes","resultBytes","decodeData","squareToQuadrilateral","p1","p2","p3","p4","dx3","dy3","a11","a12","a13","a21","a22","a23","a31","a32","a33","quadrilateralToSquare","sToQ","extract","image","location","topRight","alignmentPattern","bottomLeft","qToS","sourcePixel","mappingFunction","sum","values","reduce","reorderFinderPatterns","pattern1","pattern2","pattern3","topLeft","twoThreeDistance","oneTwoDistance","oneThreeDistance","computeDimension","countBlackWhiteRun","moduleSize","topDimension","sideDimension","countBlackWhiteRunTowardsPoint","origin","end","steep","fromX","fromY","toX","toY","dx","currentPixel","xStep","realX","realY","switchPoints","dy","yStep","distances","distance","ceil","awayFromEnd","concat","middleValue","towardsEnd","scoreBlackWhiteRun","sequence","ratios","ratio","pow","averageSize","scorePattern","point","max","min","width","height","vertError","diagDownError","diagUpError","avgSize","recenterLocation","p","leftX","round","rightX","topY","bottomY","locate","activeFinderPatternQuads","activeAlignmentPatternQuads","lastBit","scans","v","abs","averageFinderPatternBlocksize","averageAlignmentPatternBlocksize","validFinderPattern","startX","endX","bottom","matchingQuads","line","top","validAlignmentPattern","finderPatternQuads","filter","alignmentPatternQuads","quad","scoredFinderPatternPositions","score","sort","otherPoint","otherPoints","finderPatternGroups","points","alignment","midTopRight","midTopLeft","midBottomLeft","centeredAlignment","findAlignmentPattern","e","correctionToTopLeft","sizeScore","expectedAlignmentPattern","scan","locations","decoded","binaryData","topRightCorner","extracted","topLeftCorner","bottomRightCorner","bottomLeftCorner","topRightFinderPattern","topLeftFinderPattern","bottomLeftFinderPattern","bottomRightAlignmentPattern","mergeObject","target","src","Object","keys","opt","jsQR","providedOptions","options","defaultOptions","shouldInvert","greyScaleWeights","canOverwriteImage","pixelCount","bufferOffset","greyscaleBuffer","Uint8ClampedArray","buffer","greyscaleWeights","useIntegerApproximation","greyscalePixels","red","green","blue","blackPointsBuffer","blackPointsCount","verticalRegionCount","verticalRegion","hortizontalRegion","horizontalRegionCount","pixelLumosity","average","blackPoints","averageNeighborBlackPoint","binarized","BitMatrix","binarizedBuffer","createEmpty","inverted","returnInverted","invertedBuffer","xRegion","yRegion","left","lum","threshold","tryInvertedFirst","inversionAttempts","Matrix","bufferSize","value","BitStream","bitOffset","byteOffset","numBits","bitsToNotRead","toRead","GenericGFPoly","coefficients","coefficientsLength","firstNonZero","other","smallerCoefficients","largerCoefficients","lengthDiff","sumDiff","scalar","product","aLength","bLength","aCoeff","bCoefficients","coefficient","GenericGF","primitive","genBase","expTable","Array","logTable","from","alignmentPatternCenters","errorCorrectionLevels","default","grayscaleWeights","self","onmessage","event","self.onmessage","postMessage","close"],"mappings":"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"} \ No newline at end of file diff --git a/apps/qrcode/qr-scanner.umd.min.js b/apps/qrcode/qr-scanner.umd.min.js new file mode 100644 index 000000000..70d61f6f1 --- /dev/null +++ b/apps/qrcode/qr-scanner.umd.min.js @@ -0,0 +1,20 @@ +'use strict';(function(d,a){"object"===typeof exports&&"undefined"!==typeof module?module.exports=a():"function"===typeof define&&define.amd?define(a):(d=d||self,d.QrScanner=a())})(this,function(){class d{static hasCamera(){return d.listCameras(!1).then(a=>!!a.length).catch(()=>!1)}static listCameras(a=!1){if(!navigator.mediaDevices)return Promise.resolve([]);let b=null;return(a?navigator.mediaDevices.getUserMedia({audio:!1,video:!0}).then(a=>b=a).catch(()=>{}):Promise.resolve()).then(()=>navigator.mediaDevices.enumerateDevices()).then(a=> +a.filter(a=>"videoinput"===a.kind).map((a,b)=>({id:a.deviceId,label:a.label||(0===b?"Default Camera":`Camera ${b+1}`)}))).finally(()=>{if(b)for(let a of b.getTracks())a.stop(),b.removeTrack(a)})}constructor(a,b,c=this._onDecodeError,f=this._calculateScanRegion,k="environment"){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;this._legacyCanvasSize=d.DEFAULT_CANVAS_SIZE;this._preferredCamera=k;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(this._legacyCanvasSize= +c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;"number"===typeof f?(this._legacyCanvasSize=f,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._calculateScanRegion=f;this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this); +a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let g=!1;a.hidden&&(a.hidden=!1,g=!0);document.body.contains(a)||(document.body.appendChild(a),g=!0);requestAnimationFrame(()=>{let b=window.getComputedStyle(a);"none"===b.display&&(a.style.setProperty("display","block","important"),g=!0);"visible"!==b.visibility&&(a.style.setProperty("visibility","visible","important"),g=!0);g&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity= +0,a.style.width=0,a.style.height=0)});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrEnginePromise=d.createQrEngine()}hasFlash(){let a=null;return(this.$video.srcObject?Promise.resolve(this.$video.srcObject.getVideoTracks()[0]):this._getCameraStream().then(({stream:b})=>{console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"); +a=b;return b.getVideoTracks()[0]})).then(a=>"torch"in a.getSettings()).catch(()=>!1).finally(()=>{if(a)for(let b of a.getTracks())b.stop(),a.removeTrack(b)})}isFlashOn(){return this._flashOn}toggleFlash(){return this._flashOn?this.turnFlashOff():this.turnFlashOn()}turnFlashOn(){if(this._flashOn)return Promise.resolve();this._flashOn=!0;return!this._active||this._paused?Promise.resolve():this.hasFlash().then(a=>a?this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:!0}]}):Promise.reject("No flash available")).catch(()=> +{this._flashOn=!1;throw e;})}turnFlashOff(){if(this._flashOn)return this._flashOn=!1,this._restartVideoStream()}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);this.stop();d._postWorkerMessage(this._qrEnginePromise,"close")}start(){if(this._active&&!this._paused)return Promise.resolve();"https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."); +this._active=!0;if(document.hidden)return Promise.resolve();this._paused=!1;return this.$video.srcObject?(this.$video.play(),Promise.resolve()):this._getCameraStream().then(({stream:a,facingMode:b})=>{this.$video.srcObject=a;this.$video.play();this._setVideoMirror(b);this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{}))}).catch(a=>{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(a=!1){this._paused=!0;if(!this._active)return Promise.resolve(!0);this.$video.pause(); +let b=()=>{const a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(const b of a)b.stop(),this.$video.srcObject.removeTrack(b);this.$video.srcObject=null};return a?(b(),Promise.resolve(!0)):(new Promise(a=>setTimeout(a,300))).then(()=>{if(!this._paused)return!1;b();return!0})}setCamera(a){if(a===this._preferredCamera)return Promise.resolve();this._preferredCamera=a;return this._restartVideoStream()}static scanImage(a,b=null,c=null,f=null,k=!1,g=!1){let h=c instanceof Worker,l=Promise.all([c|| +d.createQrEngine(),d._loadImage(a)]).then(([a,g])=>{c=a;let l;[f,l]=this._drawToCanvas(g,b,f,k);return c instanceof Worker?(h||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let k,g,h;g=f=>{"qrResult"===f.data.type&&(c.removeEventListener("message",g),c.removeEventListener("error",h),clearTimeout(k),null!==f.data.data?a(f.data.data):b(d.NO_QR_CODE_FOUND))};h=a=>{c.removeEventListener("message",g);c.removeEventListener("error",h);clearTimeout(k);b("Scanner error: "+(a?a.message|| +a:"Unknown Error"))};c.addEventListener("message",g);c.addEventListener("error",h);k=setTimeout(()=>h("timeout"),1E4);let m=l.getImageData(0,0,f.width,f.height);c.postMessage({type:"decode",data:m},[m.data.buffer])})):new Promise((a,b)=>{let k=setTimeout(()=>b("Scanner error: timeout"),1E4);c.detect(f).then(c=>{c.length?a(c[0].rawValue):b(d.NO_QR_CODE_FOUND)}).catch(a=>b("Scanner error: "+(a.message||a))).finally(()=>clearTimeout(k))})});b&&g&&(l=l.catch(()=>d.scanImage(a,null,c,f,k)));return l=l.finally(()=> +{h||d._postWorkerMessage(c,"close")})}setGrayscaleWeights(a,b,c,f=!0){d._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:f})}setInversionMode(a){d._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static createQrEngine(a=d.WORKER_PATH){return("BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats?BarcodeDetector.getSupportedFormats():Promise.resolve([])).then(b=>-1!==b.indexOf("qr_code")?new BarcodeDetector({formats:["qr_code"]}): +new Worker(a))}_onPlay(){this._scanRegion=this._calculateScanRegion(this.$video);this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video)}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_scanFrame(){if(!this._active|| +this.$video.paused||this.$video.ended)return!1;requestAnimationFrame(()=>{1>=this.$video.readyState?this._scanFrame():this._qrEnginePromise.then(a=>d.scanImage(this.$video,this._scanRegion,a,this.$canvas)).then(this._onDecode,a=>{this._active&&(-1!==(a.message||a).indexOf("service unavailable")&&(this._qrEnginePromise=d.createQrEngine()),this._onDecodeError(a))}).then(()=>this._scanFrame())})}_onDecodeError(a){a!==d.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(){if(!navigator.mediaDevices)return Promise.reject("Camera not found."); +let a="environment"===this._preferredCamera||"user"===this._preferredCamera?"facingMode":"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}];return[...b.map(b=>Object.assign({},b,{[a]:{exact:this._preferredCamera}})),...b].reduceRight((a,b)=>()=>navigator.mediaDevices.getUserMedia({video:b,audio:!1}).then(a=>({stream:a,facingMode:this._getFacingMode(a)||(b.facingMode?this._preferredCamera:"environment"===this._preferredCamera?"user":"environment")})).catch(a),()=>Promise.reject("Camera not found."))()}_restartVideoStream(){let a= +this._paused;return this.pause(!0).then(b=>{if(b&&!a&&this._active)return this.start()})}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b=null,c=null,f=!1){c=c||document.createElement("canvas");let d=b&&b.x?b.x:0,g=b&&b.y?b.y:0,h=b&&b.width?b.width:a.width||a.videoWidth,l=b&&b.height?b.height: +a.height||a.videoHeight;f||(f=b&&b.downScaledWidth?b.downScaledWidth:h,b=b&&b.downScaledHeight?b.downScaledHeight:l,c.width!==f&&(c.width=f),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,d,g,h,l,0,0,c.width,c.height);return[c,b]}static _loadImage(a){if(a instanceof HTMLCanvasElement||a instanceof HTMLVideoElement||window.ImageBitmap&&a instanceof window.ImageBitmap||window.OffscreenCanvas&&a instanceof window.OffscreenCanvas)return Promise.resolve(a); +if(a instanceof Image)return d._awaitImageLoad(a).then(()=>a);if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a;return d._awaitImageLoad(b).then(()=>{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src);return b})}return Promise.reject("Unsupported image type.")}static _awaitImageLoad(a){return new Promise((b,c)=>{if(a.complete&&0!==a.naturalWidth)b();else{let f,d;f=()=> +{a.removeEventListener("load",f);a.removeEventListener("error",d);b()};d=()=>{a.removeEventListener("load",f);a.removeEventListener("error",d);c("Image load error")};a.addEventListener("load",f);a.addEventListener("error",d)}})}static _postWorkerMessage(a,b,c){return Promise.resolve(a).then(a=>{a instanceof Worker&&a.postMessage({type:b,data:c})})}}d.DEFAULT_CANVAS_SIZE=400;d.NO_QR_CODE_FOUND="No QR code found";d.WORKER_PATH="qr-scanner-worker.min.js";return d}) +//# sourceMappingURL=qr-scanner.umd.min.js.map diff --git a/apps/qrcode/qr-scanner.umd.min.js.map b/apps/qrcode/qr-scanner.umd.min.js.map new file mode 100644 index 000000000..c4f086333 --- /dev/null +++ b/apps/qrcode/qr-scanner.umd.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"qr-scanner.umd.min.js","sources":["src/qr-scanner.js"],"sourcesContent":["export default class QrScanner {\n /* async */\n static hasCamera() {\n return QrScanner.listCameras(false)\n .then(cameras => !!cameras.length)\n .catch(() => false);\n }\n\n /* async */\n static listCameras(requestLabels = false) {\n if (!navigator.mediaDevices) return Promise.resolve([]);\n\n // Note that enumerateDevices can always be called and does not prompt the user for permission.\n // However, enumerateDevices only includes device labels if served via https and an active media stream exists\n // or permission to access the camera was given. Therefore, ask for camera permission by opening a stream, if\n // labels were requested.\n let openedStream = null;\n return (requestLabels\n ? navigator.mediaDevices.getUserMedia({ audio: false, video: true })\n .then(stream => openedStream = stream)\n // Fail gracefully, especially if the device has no camera or on mobile when the camera is already in\n // use and some browsers disallow a second stream.\n .catch(() => {})\n : Promise.resolve()\n )\n .then(() => navigator.mediaDevices.enumerateDevices())\n .then(devices => devices.filter(device => device.kind === 'videoinput').map((device, i) => ({\n id: device.deviceId,\n label: device.label || (i === 0 ? 'Default Camera' : `Camera ${i + 1}`),\n })))\n .finally(() => {\n // close the stream we just opened for getting camera access for listing the device labels\n if (!openedStream) return;\n for (const track of openedStream.getTracks()) {\n track.stop();\n openedStream.removeTrack(track);\n }\n });\n }\n\n constructor(\n video,\n onDecode,\n canvasSizeOrOnDecodeError = this._onDecodeError,\n canvasSizeOrCalculateScanRegion = this._calculateScanRegion,\n preferredCamera = 'environment'\n ) {\n this.$video = video;\n this.$canvas = document.createElement('canvas');\n this._onDecode = onDecode;\n this._legacyCanvasSize = QrScanner.DEFAULT_CANVAS_SIZE;\n this._preferredCamera = preferredCamera;\n this._active = false;\n this._paused = false;\n this._flashOn = false;\n\n if (typeof canvasSizeOrOnDecodeError === 'number') {\n // legacy function signature where the third argument is the canvas size\n this._legacyCanvasSize = canvasSizeOrOnDecodeError;\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n this._onDecodeError = canvasSizeOrOnDecodeError;\n }\n\n if (typeof canvasSizeOrCalculateScanRegion === 'number') {\n // legacy function signature where the fourth argument is the canvas size\n this._legacyCanvasSize = canvasSizeOrCalculateScanRegion;\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n this._calculateScanRegion = canvasSizeOrCalculateScanRegion;\n }\n\n this._scanRegion = this._calculateScanRegion(video);\n\n this._onPlay = this._onPlay.bind(this);\n this._onLoadedMetaData = this._onLoadedMetaData.bind(this);\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\n\n video.disablePictureInPicture = true;\n // Allow inline playback on iPhone instead of requiring full screen playback,\n // see https://webkit.org/blog/6784/new-video-policies-for-ios/\n video.playsInline = true;\n // Allow play() on iPhone without requiring a user gesture. Should not really be needed as camera stream\n // includes no audio, but just to be safe.\n video.muted = true;\n\n // Avoid Safari stopping the video stream on a hidden video.\n // See https://github.com/cozmo/jsQR/issues/185\n let shouldHideVideo = false;\n if (video.hidden) {\n video.hidden = false;\n shouldHideVideo = true;\n }\n if (!document.body.contains(video)) {\n document.body.appendChild(video);\n shouldHideVideo = true;\n }\n requestAnimationFrame(() => {\n // Checking in requestAnimationFrame which should avoid a potential additional re-flow for getComputedStyle.\n const computedStyle = window.getComputedStyle(video);\n if (computedStyle.display === 'none') {\n video.style.setProperty('display', 'block', 'important');\n shouldHideVideo = true;\n }\n if (computedStyle.visibility !== 'visible') {\n video.style.setProperty('visibility', 'visible', 'important');\n shouldHideVideo = true;\n }\n if (shouldHideVideo) {\n // Hide the video in a way that doesn't cause Safari to stop the playback.\n console.warn('QrScanner has overwritten the video hiding style to avoid Safari stopping the playback.');\n video.style.opacity = 0;\n video.style.width = 0;\n video.style.height = 0;\n }\n });\n\n video.addEventListener('play', this._onPlay);\n video.addEventListener('loadedmetadata', this._onLoadedMetaData);\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n\n this._qrEnginePromise = QrScanner.createQrEngine();\n }\n\n /* async */\n hasFlash() {\n let openedStream = null;\n return (this.$video.srcObject\n ? Promise.resolve(this.$video.srcObject.getVideoTracks()[0])\n : this._getCameraStream().then(({ stream }) => {\n console.warn('Call hasFlash after successfully starting the scanner to avoid creating '\n + 'a temporary video stream');\n openedStream = stream;\n return stream.getVideoTracks()[0];\n })\n )\n .then((track) => 'torch' in track.getSettings())\n .catch(() => false)\n .finally(() => {\n // close the stream we just opened for detecting whether it supports flash\n if (!openedStream) return;\n for (const track of openedStream.getTracks()) {\n track.stop();\n openedStream.removeTrack(track);\n }\n });\n }\n\n isFlashOn() {\n return this._flashOn;\n }\n\n /* async */\n toggleFlash() {\n if (this._flashOn) {\n return this.turnFlashOff();\n } else {\n return this.turnFlashOn();\n }\n }\n\n /* async */\n turnFlashOn() {\n if (this._flashOn) return Promise.resolve();\n this._flashOn = true;\n if (!this._active || this._paused) return Promise.resolve(); // flash will be turned on later on .start()\n return this.hasFlash().then((hasFlash) => {\n if (!hasFlash) return Promise.reject('No flash available');\n // Note that the video track is guaranteed to exist at this point\n return this.$video.srcObject.getVideoTracks()[0].applyConstraints({\n advanced: [{ torch: true }],\n });\n }).catch(() => {\n this._flashOn = false;\n throw e;\n });\n }\n\n /* async */\n turnFlashOff() {\n if (!this._flashOn) return;\n // applyConstraints with torch: false does not work to turn the flashlight off, as a stream's torch stays\n // continuously on, see https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#torch. Therefore,\n // we have to stop the stream to turn the flashlight off.\n this._flashOn = false;\n return this._restartVideoStream();\n }\n\n destroy() {\n this.$video.removeEventListener('loadedmetadata', this._onLoadedMetaData);\n this.$video.removeEventListener('play', this._onPlay);\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n\n this.stop();\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'close');\n }\n\n /* async */\n start() {\n if (this._active && !this._paused) {\n return Promise.resolve();\n }\n if (window.location.protocol !== 'https:') {\n // warn but try starting the camera anyways\n console.warn('The camera stream is only accessible if the page is transferred via https.');\n }\n this._active = true;\n if (document.hidden) {\n // camera will be started as soon as tab is in foreground\n return Promise.resolve();\n }\n this._paused = false;\n if (this.$video.srcObject) {\n // camera stream already/still set\n this.$video.play();\n return Promise.resolve();\n }\n\n return this._getCameraStream()\n .then(({ stream, facingMode }) => {\n this.$video.srcObject = stream;\n this.$video.play();\n this._setVideoMirror(facingMode);\n\n // Restart the flash if it was previously on\n if (this._flashOn) {\n this._flashOn = false; // force turnFlashOn to restart the flash\n this.turnFlashOn().catch(() => {});\n }\n })\n .catch(e => {\n this._active = false;\n throw e;\n });\n }\n\n stop() {\n this.pause();\n this._active = false;\n }\n\n /* async */\n pause(stopStreamImmediately = false) {\n this._paused = true;\n if (!this._active) {\n return Promise.resolve(true);\n }\n this.$video.pause();\n\n const stopStream = () => {\n const tracks = this.$video.srcObject ? this.$video.srcObject.getTracks() : [];\n for (const track of tracks) {\n track.stop(); // note that this will also automatically turn the flashlight off\n this.$video.srcObject.removeTrack(track);\n }\n this.$video.srcObject = null;\n };\n\n if (stopStreamImmediately) {\n stopStream();\n return Promise.resolve(true);\n }\n\n return new Promise((resolve) => setTimeout(resolve, 300))\n .then(() => {\n if (!this._paused) return false;\n stopStream();\n return true;\n });\n }\n\n /* async */\n setCamera(facingModeOrDeviceId) {\n if (facingModeOrDeviceId === this._preferredCamera) return Promise.resolve();\n this._preferredCamera = facingModeOrDeviceId;\n // Restart the scanner with the new camera which will also update the video mirror and the scan region.\n return this._restartVideoStream();\n }\n\n /* async */\n static scanImage(imageOrFileOrUrl, scanRegion=null, qrEngine=null, canvas=null, disallowCanvasResizing=false,\n alsoTryWithoutScanRegion=false) {\n const gotExternalWorker = qrEngine instanceof Worker;\n\n let promise = Promise.all([\n qrEngine || QrScanner.createQrEngine(),\n QrScanner._loadImage(imageOrFileOrUrl),\n ]).then(([engine, image]) => {\n qrEngine = engine;\n let canvasContext;\n [canvas, canvasContext] = this._drawToCanvas(image, scanRegion, canvas, disallowCanvasResizing);\n\n if (qrEngine instanceof Worker) {\n if (!gotExternalWorker) {\n // Enable scanning of inverted color qr codes. Not using _postWorkerMessage as it's async\n qrEngine.postMessage({ type: 'inversionMode', data: 'both' });\n }\n return new Promise((resolve, reject) => {\n let timeout, onMessage, onError;\n onMessage = event => {\n if (event.data.type !== 'qrResult') {\n return;\n }\n qrEngine.removeEventListener('message', onMessage);\n qrEngine.removeEventListener('error', onError);\n clearTimeout(timeout);\n if (event.data.data !== null) {\n resolve(event.data.data);\n } else {\n reject(QrScanner.NO_QR_CODE_FOUND);\n }\n };\n onError = (e) => {\n qrEngine.removeEventListener('message', onMessage);\n qrEngine.removeEventListener('error', onError);\n clearTimeout(timeout);\n const errorMessage = !e ? 'Unknown Error' : (e.message || e);\n reject('Scanner error: ' + errorMessage);\n };\n qrEngine.addEventListener('message', onMessage);\n qrEngine.addEventListener('error', onError);\n timeout = setTimeout(() => onError('timeout'), 10000);\n const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);\n qrEngine.postMessage({\n type: 'decode',\n data: imageData\n }, [imageData.data.buffer]);\n });\n } else {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => reject('Scanner error: timeout'), 10000);\n qrEngine.detect(canvas).then(scanResults => {\n if (!scanResults.length) {\n reject(QrScanner.NO_QR_CODE_FOUND);\n } else {\n resolve(scanResults[0].rawValue);\n }\n }).catch((e) => reject('Scanner error: ' + (e.message || e))).finally(() => clearTimeout(timeout));\n });\n }\n });\n\n if (scanRegion && alsoTryWithoutScanRegion) {\n promise = promise.catch(() =>\n QrScanner.scanImage(imageOrFileOrUrl, null, qrEngine, canvas, disallowCanvasResizing));\n }\n\n promise = promise.finally(() => {\n if (gotExternalWorker) return;\n QrScanner._postWorkerMessage(qrEngine, 'close');\n });\n\n return promise;\n }\n\n setGrayscaleWeights(red, green, blue, useIntegerApproximation = true) {\n // Note that for the native BarcodeDecoder, this is a no-op. However, the native implementations work also\n // well with colored qr codes.\n QrScanner._postWorkerMessage(\n this._qrEnginePromise,\n 'grayscaleWeights',\n { red, green, blue, useIntegerApproximation }\n );\n }\n\n setInversionMode(inversionMode) {\n // Note that for the native BarcodeDecoder, this is a no-op. However, the native implementations scan normal\n // and inverted qr codes by default\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'inversionMode', inversionMode);\n }\n\n /* async */\n static createQrEngine(workerPath = QrScanner.WORKER_PATH) {\n return ('BarcodeDetector' in window && BarcodeDetector.getSupportedFormats\n ? BarcodeDetector.getSupportedFormats()\n : Promise.resolve([])\n )\n .then((supportedFormats) => supportedFormats.indexOf('qr_code') !== -1\n ? new BarcodeDetector({ formats: ['qr_code'] })\n : new Worker(workerPath)\n );\n }\n\n _onPlay() {\n this._scanRegion = this._calculateScanRegion(this.$video);\n this._scanFrame();\n }\n\n _onLoadedMetaData() {\n this._scanRegion = this._calculateScanRegion(this.$video);\n }\n\n _onVisibilityChange() {\n if (document.hidden) {\n this.pause();\n } else if (this._active) {\n this.start();\n }\n }\n\n _calculateScanRegion(video) {\n // Default scan region calculation. Note that this can be overwritten in the constructor.\n const smallestDimension = Math.min(video.videoWidth, video.videoHeight);\n const scanRegionSize = Math.round(2 / 3 * smallestDimension);\n return {\n x: Math.round((video.videoWidth - scanRegionSize) / 2),\n y: Math.round((video.videoHeight - scanRegionSize) / 2),\n width: scanRegionSize,\n height: scanRegionSize,\n downScaledWidth: this._legacyCanvasSize,\n downScaledHeight: this._legacyCanvasSize,\n };\n }\n\n _scanFrame() {\n if (!this._active || this.$video.paused || this.$video.ended) return false;\n // using requestAnimationFrame to avoid scanning if tab is in background\n requestAnimationFrame(() => {\n if (this.$video.readyState <= 1) {\n // Skip scans until the video is ready as drawImage() only works correctly on a video with readyState\n // > 1, see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage#Notes.\n // This also avoids false positives for videos paused after a successful scan which remains visible on\n // the canvas until the video is started again and ready.\n this._scanFrame();\n return;\n }\n this._qrEnginePromise\n .then((qrEngine) => QrScanner.scanImage(this.$video, this._scanRegion, qrEngine, this.$canvas))\n .then(this._onDecode, (error) => {\n if (!this._active) return;\n const errorMessage = error.message || error;\n if (errorMessage.indexOf('service unavailable') !== -1) {\n // When the native BarcodeDetector crashed, create a new one\n this._qrEnginePromise = QrScanner.createQrEngine();\n }\n this._onDecodeError(error);\n })\n .then(() => this._scanFrame());\n });\n }\n\n _onDecodeError(error) {\n // default error handler; can be overwritten in the constructor\n if (error === QrScanner.NO_QR_CODE_FOUND) return;\n console.log(error);\n }\n\n /* async */\n _getCameraStream() {\n if (!navigator.mediaDevices) {\n return Promise.reject('Camera not found.');\n }\n\n const preferenceType = this._preferredCamera === 'environment' || this._preferredCamera === 'user'\n ? 'facingMode'\n : 'deviceId';\n const constraintsWithoutCamera = [{\n width: { min: 1024 }\n }, {\n width: { min: 768 }\n }, {}];\n const constraintsWithCamera = constraintsWithoutCamera.map((constraint) => Object.assign({}, constraint, {\n [preferenceType]: { exact: this._preferredCamera },\n }));\n\n // First try constraints with camera, then without camera. Using reduceRight as the Promise is build in a\n // bottom up fashion.\n return [...constraintsWithCamera, ...constraintsWithoutCamera].reduceRight((fallback, constraint) =>\n () => navigator.mediaDevices.getUserMedia({ video: constraint, audio: false })\n .then((stream) => ({\n stream,\n // Try to determine the facing mode from the stream, otherwise use a guess or 'environment' as\n // default. Note that the guess is not always accurate as Safari returns cameras of different facing\n // mode, even for exact facingMode constraints.\n facingMode: this._getFacingMode(stream)\n || (constraint.facingMode\n ? this._preferredCamera // _preferredCamera is a facing mode and we are able to fulfill it\n : (this._preferredCamera === 'environment'\n ? 'user' // switch as _preferredCamera was environment but we are not able to fulfill it\n : 'environment' // switch from unfulfilled user facingMode or default to environment\n )\n ),\n }))\n .catch(fallback),\n () => Promise.reject('Camera not found.')\n )();\n }\n\n /* async */\n _restartVideoStream() {\n // Note that we always pause the stream and not only if !this._paused as even if this._paused === true, the\n // stream might still be running, as it's by default only stopped after a delay of 300ms.\n const wasPaused = this._paused;\n return this.pause(true).then((paused) => {\n if (!paused || wasPaused || !this._active) return;\n return this.start();\n });\n }\n\n _setVideoMirror(facingMode) {\n // in user facing mode mirror the video to make it easier for the user to position the QR code\n const scaleFactor = facingMode==='user'? -1 : 1;\n this.$video.style.transform = 'scaleX(' + scaleFactor + ')';\n }\n\n _getFacingMode(videoStream) {\n const videoTrack = videoStream.getVideoTracks()[0];\n if (!videoTrack) return null; // unknown\n // inspired by https://github.com/JodusNodus/react-qr-reader/blob/master/src/getDeviceId.js#L13\n return /rear|back|environment/i.test(videoTrack.label)\n ? 'environment'\n : /front|user|face/i.test(videoTrack.label)\n ? 'user'\n : null; // unknown\n }\n\n static _drawToCanvas(image, scanRegion=null, canvas=null, disallowCanvasResizing=false) {\n canvas = canvas || document.createElement('canvas');\n const scanRegionX = scanRegion && scanRegion.x? scanRegion.x : 0;\n const scanRegionY = scanRegion && scanRegion.y? scanRegion.y : 0;\n const scanRegionWidth = scanRegion && scanRegion.width? scanRegion.width : image.width || image.videoWidth;\n const scanRegionHeight = scanRegion && scanRegion.height? scanRegion.height : image.height || image.videoHeight;\n\n if (!disallowCanvasResizing) {\n const canvasWidth = scanRegion && scanRegion.downScaledWidth\n ? scanRegion.downScaledWidth\n : scanRegionWidth;\n const canvasHeight = scanRegion && scanRegion.downScaledHeight\n ? scanRegion.downScaledHeight\n : scanRegionHeight;\n // Setting the canvas width or height clears the canvas, even if the values didn't change, therefore only\n // set them if they actually changed.\n if (canvas.width !== canvasWidth) {\n canvas.width = canvasWidth;\n }\n if (canvas.height !== canvasHeight) {\n canvas.height = canvasHeight;\n }\n }\n\n const context = canvas.getContext('2d', { alpha: false });\n context.imageSmoothingEnabled = false; // gives less blurry images\n context.drawImage(\n image,\n scanRegionX, scanRegionY, scanRegionWidth, scanRegionHeight,\n 0, 0, canvas.width, canvas.height\n );\n return [canvas, context];\n }\n\n /* async */\n static _loadImage(imageOrFileOrBlobOrUrl) {\n if (imageOrFileOrBlobOrUrl instanceof HTMLCanvasElement || imageOrFileOrBlobOrUrl instanceof HTMLVideoElement\n || window.ImageBitmap && imageOrFileOrBlobOrUrl instanceof window.ImageBitmap\n || window.OffscreenCanvas && imageOrFileOrBlobOrUrl instanceof window.OffscreenCanvas) {\n return Promise.resolve(imageOrFileOrBlobOrUrl);\n } else if (imageOrFileOrBlobOrUrl instanceof Image) {\n return QrScanner._awaitImageLoad(imageOrFileOrBlobOrUrl).then(() => imageOrFileOrBlobOrUrl);\n } else if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob\n || imageOrFileOrBlobOrUrl instanceof URL || typeof(imageOrFileOrBlobOrUrl)==='string') {\n const image = new Image();\n if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob) {\n image.src = URL.createObjectURL(imageOrFileOrBlobOrUrl);\n } else {\n image.src = imageOrFileOrBlobOrUrl;\n }\n return QrScanner._awaitImageLoad(image).then(() => {\n if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob) {\n URL.revokeObjectURL(image.src);\n }\n return image;\n });\n } else {\n return Promise.reject('Unsupported image type.');\n }\n }\n\n /* async */\n static _awaitImageLoad(image) {\n return new Promise((resolve, reject) => {\n if (image.complete && image.naturalWidth!==0) {\n // already loaded\n resolve();\n } else {\n let onLoad, onError;\n onLoad = () => {\n image.removeEventListener('load', onLoad);\n image.removeEventListener('error', onError);\n resolve();\n };\n onError = () => {\n image.removeEventListener('load', onLoad);\n image.removeEventListener('error', onError);\n reject('Image load error');\n };\n image.addEventListener('load', onLoad);\n image.addEventListener('error', onError);\n }\n });\n }\n\n /* async */\n static _postWorkerMessage(qrEngineOrQrEnginePromise, type, data) {\n return Promise.resolve(qrEngineOrQrEnginePromise).then((qrEngine) => {\n if (!(qrEngine instanceof Worker)) return;\n qrEngine.postMessage({ type, data });\n });\n }\n}\nQrScanner.DEFAULT_CANVAS_SIZE = 400;\nQrScanner.NO_QR_CODE_FOUND = 'No QR code found';\nQrScanner.WORKER_PATH = 'qr-scanner-worker.min.js';\n"],"names":["QrScanner","listCameras","then","cameras","length","catch","requestLabels","navigator","mediaDevices","Promise","resolve","openedStream","getUserMedia","audio","video","stream","enumerateDevices","devices","filter","device","kind","map","i","id","deviceId","label","finally","track","stop","removeTrack","onDecode","canvasSizeOrOnDecodeError","_onDecodeError","canvasSizeOrCalculateScanRegion","_calculateScanRegion","preferredCamera","$video","$canvas","document","createElement","_onDecode","_legacyCanvasSize","DEFAULT_CANVAS_SIZE","_preferredCamera","_flashOn","_paused","_active","console","warn","_scanRegion","_onPlay","bind","_onLoadedMetaData","_onVisibilityChange","disablePictureInPicture","playsInline","muted","shouldHideVideo","hidden","body","contains","appendChild","requestAnimationFrame","computedStyle","display","style","setProperty","visibility","opacity","width","height","addEventListener","_qrEnginePromise","createQrEngine","srcObject","getVideoTracks","_getCameraStream","getSettings","turnFlashOff","turnFlashOn","hasFlash","applyConstraints","advanced","torch","reject","e","_restartVideoStream","removeEventListener","_postWorkerMessage","window","location","protocol","play","facingMode","_setVideoMirror","pause","stopStreamImmediately","tracks","getTracks","stopStream","setTimeout","facingModeOrDeviceId","imageOrFileOrUrl","scanRegion","qrEngine","canvas","disallowCanvasResizing","alsoTryWithoutScanRegion","promise","all","_loadImage","engine","image","canvasContext","_drawToCanvas","Worker","gotExternalWorker","postMessage","type","data","timeout","onMessage","onError","event","clearTimeout","NO_QR_CODE_FOUND","imageData","buffer","detect","scanResults","rawValue","message","scanImage","red","green","blue","useIntegerApproximation","inversionMode","workerPath","WORKER_PATH","BarcodeDetector","getSupportedFormats","supportedFormats","indexOf","formats","_scanFrame","start","videoHeight","x","Math","round","videoWidth","scanRegionSize","y","downScaledWidth","downScaledHeight","paused","ended","readyState","error","log","min","constraint","preferenceType","exact","constraintsWithoutCamera","reduceRight","fallback","_getFacingMode","wasPaused","transform","videoStream","test","videoTrack","scanRegionWidth","scanRegionHeight","canvasWidth","canvasHeight","alpha","context","imageSmoothingEnabled","drawImage","scanRegionX","scanRegionY","imageOrFileOrBlobOrUrl","HTMLCanvasElement","HTMLVideoElement","ImageBitmap","OffscreenCanvas","Image","_awaitImageLoad","File","Blob","URL","src","createObjectURL","revokeObjectURL","complete","naturalWidth","onLoad","qrEngineOrQrEnginePromise"],"mappings":"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"} \ No newline at end of file diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog new file mode 100644 index 000000000..3e7094eab --- /dev/null +++ b/apps/rebble/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Fix dependancies, fix type to Purple diff --git a/apps/rebble/README.md b/apps/rebble/README.md new file mode 100644 index 000000000..712fa4e9b --- /dev/null +++ b/apps/rebble/README.md @@ -0,0 +1,26 @@ +# Rebble + + *A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion* + +* Designed specifically for Bangle 2 +* A choice of 6 different background colous through its setting menu. Goto Settings, App/Widget settings, Rebble. +* Supports the Light and Dark themes +* Low power drain, only redraws once per minute +* Has 3 sidebars that cycle including steps, day, date, sunrise, sunset +* Tap top or bottom right to instantly cycle to the next sidebar +* 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 + +![](screenshot_rebble.png) +![](screenshot_rebble2.png) +![](screenshot_rebble3.png) +![](screenshot_rebble4.png) + +## Future Enhancements + +* Support for Weather Icons in the Steps Sidebar +* Improved small font +* Improved icons + +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/rebble/rebble.app.js b/apps/rebble/rebble.app.js new file mode 100644 index 000000000..d186ea8ec --- /dev/null +++ b/apps/rebble/rebble.app.js @@ -0,0 +1,272 @@ +var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +const SETTINGS_FILE = "rebble.json"; +const LOCATION_FILE = "mylocation.json"; +let settings; +let location; + +Graphics.prototype.setFontLECO1976Regular22 = function(scale) { + // Actual height 22 (21 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/nA/+cD/5wP/nAAAAAAAAPwAA/gAD+AAPwAAAAAD+AAP4AA/gAAAAAAAAAAAAAcOAP//A//8D//wP//AHDgAcOAP//A//8D//wP//AHDgAAAAAAAAH/jgf+OB/44H/jj8OP/w4//Dj/8OPxw/4HD/gcP+Bw/4AAAAAAAP+AA/8AD/wQOHHA4c8D//wP/8A//gAD4AAfAAH/8A//wP//A84cDjhwIP/AA/8AB/wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8ABwAAAAAAAAD8AAP4AA/gAD8AAAAAAAAAAAEAAD+AB//A///v/D//gB/wABwAAAAAADgAA/wAf/4P8///wf/4AP8AAOAAAAAAAAAyAAHcAAPwAD/gAP/AA/8AA/AAH8AAMwAAAAAAAAAAAAADgAAOAAA4AAf8AD/wAP/AA/8AAOAAA4AADgAAAAAAAAAAD8AAfwAB/AAD8AAAAAAAADgAAOAAA4AADgAAOAAA4AADgAAAAAAAAAADgAAOAAA4AADgAAAAAAAAABwAB/AA/8A//gP/gA/wADwAAIAAAAAAD//wP//A//8D//wOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA4AcDgBwOAHA//8D//wP//A//8AABwAAHAAAcAAAAAAAA+f8D5/wPn/A+f8DhxwOHHA4ccDhxwP/HA/8cD/xwP/HAAAAAAAAOAHA4AcDhxwOHHA4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/wAP/AA/8AD/wAAHAAAcAABwAAHAA//8D//wP//A//8AAAAAAAA/98D/3wP/fA/98DhxwOHHA4ccDhxwOH/A4f8Dh/wOH/AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccDh/wOH/A4f8Dh/wAAAAAAAD4AAPgAA+AADgAAOAAA4AADgAAP//A//8D//wP//AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA//8D//wP//A//8AAAAAAAAOA4A4DgDgOAOA4AAAAAAAAOA/A4H8DgfwOA/AAAAAAAAB4AAPwAA/AAD8AAf4ABzgAPPAA8cAHh4AAAAAAAAAAAAHHAAccABxwAHHAAccABxwAHHAAccABxwAHHAAAAAAAAAOHAA4cADzwAPPAAf4AB/gAD8AAPwAAeAAB4AAAAAAAAA+AAD4AAPgAA+ecDh9wOH3A4fcDhwAP/AA/8AD/wAP/AAAAAAAAAP//4///j//+P//44ADjn/OOf845/zjnHOP8c4//zj//OP/84AAAAAAAP//A//8D//wP//A4cADhwAOHAA4cAD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA//8D//wP9/A/j8AAAAAAAA//8D//wP//A//8DgBwOAHA4AcDgBwOAHA4AcDgBwOAHAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA8A8D//wH/+AP/wAf+AAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4ccDhxwOAHA4AcAAAAAAAA//8D//wP//A//8DhwAOHAA4cADhwAOHAA4cADgAAOAAAAAAD//wP//A//8D//wOAHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA//8D//wP//A//8ABwAAHAAAcAABwAP//A//8D//wP//AAAAAAAAP//A//8D//wP//AAAAAAAAOAHA4AcDgBwOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA//8D//wP//A//8AHwAA/AAP8AB/wAPn/A8f8DB/wIH/AAAAAAAAP//A//8D//wP//AAAcAABwAAHAAAcAABwAAHAAAAAAAAP//A//8D//wP//Af8AAP+AAH/AAD8AAHwAD/AB/wAf8AP+AA//8D//wP//AAAAAAAAP//A//8D//wP//AfwAAfwAAfwAAfwAAfwP//A//8D//wAAAAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHAA4cADhwAOHAA/8AD/wAP/AA/8AAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//+P//4///j//+AAA4AADgAAAP//A//8D//wP//A4eADh+AOH8A4f4D/3wP/HA/8MD/wQAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA4AADgAAOAAA//8D//wP//A//8DgAAOAAA4AADgAAAAAA//8D//wP//A//8AABwAAHAAAcAABwP//A//8D//wP//AAAADAAAPgAA/wAD/4AB/8AA/8AAfwAB/AA/8Af+AP/AA/wAD4AAMAAA4AAD+AAP/gA//8AH/wAB/AAf8Af/wP/4A/4AD/gAP/4AH/8AB/wAB/AB/8D//wP/gA/gADgAAIABA4AcDwDwPw/Afn4Af+AA/wAD/AA//AH5+A/D8DwDwOAHAgAEAAAAP/AA/8AD/wAP/AAAf8AB/wAH/AAf8D/wAP/AA/8AD/wAAAAAAAADh/wOH/A4f8Dh/wOHHA4ccDhxwOHHA/8cD/xwP/HA/8cAAAAAAAAf//9///3///f//9wAA3AADcAAMAAAOAAA/gAD/wAH/8AB/8AA/wAAPAAAEAAAAHAADcAANwAB3///f//9///wAA"), 32, atob("BwYLDg4UDwYJCQwMBgkGCQ4MDg4ODg4NDg4GBgwMDA4PDg4ODg4NDg4GDQ4MEg8ODQ8ODgwODhQODg4ICQg="), 22+(scale<<8)+(1<<16)); +} + +Graphics.prototype.setFontKdamThmor = function(scale) { + // Actual height 72 (71 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAH+AAAAAAAAAAAAAP/AAAAAAAAAAAAAf/gAAAAAAAAAAAA//gAAAAAAAAAAAA//wAAAAAAAAAAAA//wAAAAAAAAAAAA//wAAAAAAAAAAAA//wAAAAAAAAAAAA//gAAAAAAAAAAAAf/gAAAAAAAAAAAAf/AAAAAAAAAAAAAP+AAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAB/AAAAAAAAAAAAAP/AAAAAAAAAAAAA//AAAAAAAAAAAAH/+AAAAAAAAAAAAf/+AAAAAAAAAAAD//+AAAAAAAAAAAf//8AAAAAAAAAAB///4AAAAAAAAAAP///wAAAAAAAAAA////AAAAAAAAAAH///4AAAAAAAAAAf///gAAAAAAAAAD///8AAAAAAAAAAf///wAAAAAAAAAB///+AAAAAAAAAAP///4AAAAAAAAAA////AAAAAAAAAAH///4AAAAAAAAAAf///gAAAAAAAAAD///8AAAAAAAAAAP///wAAAAAAAAAB///+AAAAAAAAAAP///4AAAAAAAAAA////AAAAAAAAAAD///4AAAAAAAAAAP///gAAAAAAAAAAf//8AAAAAAAAAAA///wAAAAAAAAAAA//+AAAAAAAAAAAA//4AAAAAAAAAAAA//AAAAAAAAAAAAA/4AAAAAAAAAAAAA/gAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//AAAAAAAAAAAP///+AAAAAAAAAD/////wAAAAAAAAP/////+AAAAAAAA///////gAAAAAAD///////4AAAAAAH///////8AAAAAAf///////+AAAAAA/////////gAAAAB/////////wAAAAB/////////wAAAAD/////////4AAAAH///AAA///8AAAAH//wAAAB//8AAAAP/+AAAAAP/+AAAAP/4AAAAAD/+AAAAf/gAAAAAA//AAAAf/AAAAAAAf/AAAAf+AAAAAAAP/AAAA/+AAAAAAAP/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/+AAAAAAAP/gAAAf+AAAAAAAP/AAAAf/AAAAAAAf/AAAAf/gAAAAAA//AAAAP/4AAAAAD/+AAAAP/+AAAAAP/+AAAAH//gAAAA//8AAAAH///AAAf//8AAAAD/////////4AAAAD/////////wAAAAB/////////wAAAAA/////////gAAAAAf////////AAAAAAH///////8AAAAAAD///////4AAAAAAA///////gAAAAAAAP/////+AAAAAAAAD/////4AAAAAAAAAf////AAAAAAAAAAA///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAADwAAAAAAAAAAAAAD4AAAAAAAAAAAAAH8AAAAAAAAAAAAAP+AAAAAAAAAAAAAf/AAAAAH/AAAAAA//AAAAAH/AAAAAB//AAAAAH/AAAAAB//AAAAAH/AAAAAD/+AAAAAH/AAAAAH/8AAAAAH/AAAAAP/4AAAAAH/AAAAAf/wAAAAAH/AAAAA//gAAAAAH/AAAAB//gAAAAAH/AAAAB//AAAAAAH/AAAAD/+AAAAAAH/AAAAH/8AAAAAAH/AAAAP//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAHgAAAAAD/AAAAAA/wAAAAAH/AAAAAD/wAAAAAP/AAAAAH/wAAAAAf/AAAAAP/wAAAAA//AAAAA//wAAAAB//AAAAB//wAAAAD//AAAAB//wAAAAH//AAAAD//wAAAAP//AAAAH//wAAAAf//AAAAH//gAAAA///AAAAP/+AAAAB///AAAAP/4AAAAD///AAAAf/gAAAAH///AAAAf/AAAAAP///AAAAf+AAAAAf///AAAAf+AAAAA//v/AAAA/8AAAAB//P/AAAA/8AAAAD/+P/AAAA/8AAAAH/8P/AAAA/8AAAAP/4P/AAAA/8AAAAf/wf/AAAA/8AAAA//gf/AAAA/8AAAD//Af/AAAA/8AAAH/+Af/AAAA/+AAAP/8Af/AAAA/+AAAf/4Af/AAAAf/AAB//wAf/AAAAf/gAD//gAf/AAAAf/wAf//AAf/AAAAf/+D//+AAf/AAAAP/////8AAf/AAAAP/////4AAf/AAAAH/////wAAf/AAAAH/////gAAf/AAAAD/////AAAf/AAAAB////8AAAf/AAAAA////4AAAf/AAAAAf///wAAAf/AAAAAP///AAAAf/AAAAAD//8AAAAf/AAAAAA//gAAAAP/AAAAAABwAAAAAH/AAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAfAAAAAAAAHgAAAA/wAAAAAAA/wAAAA/8AAAAAAD/wAAAB/+AAAAAAH/wAAAB//AAAAAAP/wAAAB//gAAAAA//wAAAB//wAAAAB//wAAAB//4AAAAB//wAAAB//8AAAAD//wAAAA//8AAAAH//wAAAAf/+AAAAH//gAAAAH/+AAAAP/+AAAAAB//AAAAP/4AAAAAA//AAAAf/gAAAAAAf/AAAAf/AAAAAAAf/AAAAf/AAAAAAAP/gAAAf+AAAAAAAP/gAAA/+AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAH/wAAH/gAAA/8AAH/wAAP/gAAA/+AAH/wAAP/AAAAf/AAP/4AAf/AAAAf/AAf/4AA//AAAAf/wA//8AB//AAAAf/+H//+AD/+AAAAP//////4f/+AAAAP////v////8AAAAH////v////8AAAAH////H////4AAAAD////H////wAAAAB///+D////wAAAAA///8D////gAAAAAf//4B////AAAAAAP//wA///+AAAAAAD//AAf//4AAAAAAAf4AAH//gAAAAAAAAAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAD/gAAAAAAAAAAAAP/wAAAAAAAAAAAAf/wAAAAAAAAAAAA//wAAAAAAAAAAAD//wAAAAAAAAAAAH//wAAAAAAAAAAAP//wAAAAAAAAAAA///wAAAAAAAAAAB///wAAAAAAAAAAD///wAAAAAAAAAAP///wAAAAAAAAAAf///wAAAAAAAAAA//9/wAAAAAAAAAD//x/wAAAAAAAAAH//h/wAAAAAAAAAP/+B/wAAAAAAAAA//8B/wAAAAAAAAB//4B/wAAAAAAAAD//gB/wAAAAAAAAP//AB/wAAAAAAAAf/+AB/wAAAAAAAA//4AB/wAAAAAAAD//wAB/wAAAAAAAH//AAB/wAAAAAAAP/+AAB/wAAAAAAA//8AAB/wAAAAAAB//wAAB/wAAAAAAD//gAAB/wAAAAAAP/+AAAB/wAAAAAAf/8AAAB/wAAAAAAf/5////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAPgAAAAAAAAAAAAA/wAAAAAAAAD4AAB/4AAAAAAAD/4AAB/4AAAAAAD//4AAB/8AAAAAD///8AAB/8AAAAD////8AAB/+AAAAf////8AAA/+AAAAf////+AAA/+AAAAf////+AAAf/AAAAf////8AAAf/AAAAf////8AAAP/AAAAf////8AAAP/AAAAf//4/4AAAH/gAAAf/8A/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AB/4AAAH/gAAAf+AB/4AAAH/gAAAf+AA/8AAAP/gAAAf+AA/8AAAP/AAAAf+AA/8AAAP/AAAAf+AA/+AAAf/AAAAf+AA/+AAA//AAAAf+AA//AAB/+AAAAf+AAf/gAD/+AAAAf+AAf/4Af/8AAAAf+AAf/////8AAAAf+AAP/////4AAAAf+AAP/////wAAAAf+AAH/////wAAAAf+AAD/////gAAAAf+AAD/////AAAAAf+AAB////+AAAAAf8AAA////8AAAAAf4AAAP///wAAAAAfgAAAH///AAAAAAAAAAAA//8AAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AAAAAAAAAAAB///AAAAAAAAAAAP///wAAAAAAAAAA////8AAAAAAAAAB////+AAAAAAAAAH/////AAAAAAAAAP/////gAAAAAAAA//////wAAAAAAAB//////4AAAAAAAH//////8AAAAAAAP//////8AAAAAAAf//+AP/+AAAAAAB///4AB/+AAAAAAD///wAA/+AAAAAAH///gAAf/AAAAAAf///AAAP/AAAAAA///+AAAP/AAAAAB///+AAAH/gAAAAH//3+AAAH/gAAAAP//v8AAAH/gAAAAf//P8AAAH/gAAAB//+P8AAAH/gAAAD//4f8AAAH/gAAAH//wf8AAAH/gAAAP//gf8AAAH/gAAAf//Af8AAAH/gAAAf/8Af+AAAH/gAAAf/4Af+AAAP/AAAAf/wAf+AAAP/AAAAf/gAf/AAAf/AAAAf/AAP/gAA//AAAAf8AAP/wAB/+AAAAf4AAP/4AD/+AAAAfwAAP//Af/8AAAAfgAAH/////8AAAAeAAAH/////4AAAAcAAAD/////4AAAAYAAAD/////wAAAAQAAAB/////gAAAAAAAAA/////AAAAAAAAAAf///+AAAAAAAAAAP///8AAAAAAAAAAD///wAAAAAAAAAAB///AAAAAAAAAAAAP/8AAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAABAAAAf+AAAAAAAAHAAAAf+AAAAAAAAfAAAAf+AAAAAAAB/AAAAf+AAAAAAAH/AAAAf+AAAAAAAf/AAAAf+AAAAAAB//AAAAf+AAAAAAH//AAAAf+AAAAAAf//AAAAf+AAAAAB///AAAAf+AAAAAH///AAAAf+AAAAAf///AAAAf+AAAAB///+AAAAf+AAAAH///8AAAAf+AAAAf///wAAAAf+AAAA////AAAAAf+AAAD///8AAAAAf+AAAP///wAAAAAf+AAA////AAAAAAf+AAD///8AAAAAAf+AAP///wAAAAAAf+AA////AAAAAAAf+AD///8AAAAAAAf+AP///wAAAAAAAf+A////AAAAAAAAf+D///8AAAAAAAAf+P///wAAAAAAAAf+f//+AAAAAAAAAf////4AAAAAAAAAf////gAAAAAAAAAf///+AAAAAAAAAAf///4AAAAAAAAAAf///gAAAAAAAAAAf//+AAAAAAAAAAAf//4AAAAAAAAAAAf//gAAAAAAAAAAAf/+AAAAAAAAAAAAf/4AAAAAAAAAAAAf/gAAAAAAAAAAAAf+AAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAB//gAAAAAAADwAAH//4AAAAAAA//AAf//8AAAAAAD//wA////AAAAAAP//4B////gAAAAAf//+B////wAAAAA////D////wAAAAB////H////4AAAAD////n////8AAAAH/////////8AAAAH/////////+AAAAP//////wP/+AAAAP//////AB/+AAAAf/wD//8AA//AAAAf/AA//4AAf/AAAAf+AAf/4AAP/AAAAf8AAP/wAAH/AAAA/8AAH/wAAH/gAAA/4AAH/gAAH/gAAA/4AAH/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAH/gAAH/gAAA/8AAH/wAAH/gAAAf8AAP/wAAH/gAAAf+AAP/wAAP/AAAAf/AAf/4AAf/AAAAf/wB//8AAf/AAAAP/////+AB//AAAAP//////wH/+AAAAH/////////+AAAAH/////////8AAAAD////n////8AAAAB////H////4AAAAA////D////wAAAAAf//+B////wAAAAAP//8B////gAAAAAH//wA////AAAAAAB//AAf//8AAAAAAAH4AAH//4AAAAAAAAAAAB//gAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAAAAAAAAf//AAAAAAAAAAAB///wAAAAAAAAAAH///4AAAAAAAAAAP///+AAAAAAAAAAf////AAAAAAAAAA/////AAAADAAAAB/////gAAAHAAAAD/////wAAAPAAAAH/////wAAAfAAAAH/////4AAB/AAAAP//A//4AAD/AAAAP/4AH/8AAH/AAAAf/gAD/8AAP/AAAAf/AAB/8AA//AAAAf+AAA/8AB//AAAAf+AAA/8AD//AAAA/8AAAf8AH//AAAA/8AAAf8Af//AAAA/8AAAf8A//+AAAA/8AAAf8B//+AAAA/8AAAf8D//8AAAA/8AAAf8P//wAAAA/8AAAf8f//gAAAA/8AAAf4//+AAAAA/8AAAf5//8AAAAA/8AAAf3//4AAAAAf+AAA////gAAAAAf+AAB////AAAAAAf/AAB///8AAAAAAf/gAD///4AAAAAAP/4AP///gAAAAAAP//B////AAAAAAAH//////+AAAAAAAH//////4AAAAAAAD//////wAAAAAAAB//////AAAAAAAAA/////+AAAAAAAAAf////4AAAAAAAAAP////wAAAAAAAAAH////AAAAAAAAAAB///8AAAAAAAAAAAf//gAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAD8AAAAAAAA/4AAAP+AAAAAAAB/8AAAf/AAAAAAAB/+AAAf/gAAAAAAD/+AAA//gAAAAAAD//AAA//wAAAAAAD//AAA//wAAAAAAD//AAA//wAAAAAAD//AAA//wAAAAAAD/+AAA//gAAAAAAB/+AAAf/gAAAAAAA/8AAAP/AAAAAAAAf4AAAH+AAAAAAAAHgAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("FCM0NDQ0NDQ0NDQ0GA=="), 90+(scale<<8)+(1<<16)); +} + +var boot_img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA==")); +var sunrise_img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4ACp5A/AH4A/AH4AIoEAggfcgAABD/4f/D/4f/CiNPmgfUoYIHoEAggfSoEQgYJGmAUJD5QJBgQ/IIBBKJChiVSCYR1LBZAzTICQyNICAxOICAwPD40xA4UTc5xAFiAuDiAWCAAMBc5hgHDxAgFeCKEDh//AAPwdiKDHh9PD4X0EAX0DyQ+BHoYgFh4+UDwofB/68OAAlBHw6CEQKITBDxAABMCReHUQhgSLxRgDDx9CD4g8DD4sUbqEUH5SABUB4fBDxYfKkQAFkEAiQJGAAcjgECBQ6qBAH4A9Y5wA/AH4Aw")); +var sunset_img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AH4A/AH4AMoEAggfcgAABD/4f/D/4f/CqU0D6lDBA9AgEED6VAiEDBI0wChIfKBIMCH5BAIJRIUMSqQTCOpYLIGaZASGRpAQGJxAQGB4fGmIHCibnOIAsQFwcQCwQABgLnMMA4eIEArwRQgY0DAwwARC44gC+geSORJ8PHw4KTABFBGhRAT+AzLgEPLzZgUKRhgBDx9CD50UbqARMUCBROD5MiAAsggESBIwADkcAgQKHVQIA/AHrHOAH4A/AGA")); + +var drawCount = 0; +var sideBar = 0; +var sunRise = "00:00"; +var sunSet = "00:00"; + +function log_debug(o) { + //console.log(o); +} + +// requires the myLocation app +function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; +} + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green'}; +} + +function extractTime(d){ + var h = d.getHours(), m = d.getMinutes(); + return(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2)); +} + +function updateSunRiseSunSet(lat, lon){ + // get today's sunlight times for lat/lon + var times = SunCalc.getTimes(new Date(), lat, lon); + + // format sunrise time from the Date object + sunRise = extractTime(times.sunrise); + sunSet = extractTime(times.sunset); +} + +// wrapper, makes it easier if we want to switch to a different font later +function setSmallFont() { + g.setFont('Vector', 20); +} + +// set the text color of the sidebar elements that dont change with the Theme +function setTextColor() { + // day and steps + if (settings.color == 'Blue' || settings.color == 'Red') { + g.setColor('#fff'); // white on blue or red best contrast + } else { + g.setColor('#000'); // otherwise black regardless of theme + } +} + +const h = g.getHeight(); +const w = g.getWidth(); +const ha = 2*h/5 - 8; +const h2 = 3*h/5 - 10; +const h3 = 7*h/8; +const w2 = 9*w/14; +const w3 = w2 + ((w - w2)/2); // centre line of the sidebar +const ws = w - w2; // sidebar width +const wb = 40; // battery width + +function draw() { + log_debug("draw()"); + let date = new Date(); + let da = date.toString().split(" "); + let hh = da[4].substr(0,2); + let mm = da[4].substr(3,2); + //const t = 6; + + if (drawCount % 60 == 0) + updateSunRiseSunSet(location.lat, location.lon); + + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(0, 0, w2, h); + g.setColor(settings.bg); + g.fillRect(w2, 0, w, h); + + // time + g.setColor(g.theme.fg); + g.setFontKdamThmor(); + g.setFontAlign(0, -1); + g.drawString(hh, w2/2, 10 + 0); + g.drawString(mm, w2/2, 10 + h/2); + + switch(sideBar) { + case 0: + drawSideBar1(); + break; + case 1: + drawSideBar2(); + break; + case 2: + drawSideBar3(); + break; + } + + drawCount++; + queueDraw(); +} + +function drawSideBar1() { + let date = new Date(); + let da = date.toString().split(" "); + + drawBattery(w2 + (w-w2-wb)/2, h/10, wb, 17); + + setTextColor(); + g.setFont('Vector', 20); + g.setFontAlign(0, -1); + g.drawString(E.getBattery() + '%', w3, (h/10) + 17 + 7); + + drawDateAndCalendar(w3, h/2, da[0], da[2], da[1]); +} + +function drawSideBar2() { + drawBattery(w2 + (w-w2-wb)/2, h/10, wb, 17); + + setTextColor(); + g.setFont('Vector', 20); + g.setFontAlign(0, -1); + g.drawString(E.getBattery() + '%', w3, (h/10) + 17 + 7); + + // steps + g.drawImage(boot_img, w2 + (ws - 64)/2, h/2, { scale: 1 }); + setSmallFont(); + g.setFontAlign(0, -1); + g.drawString(formatSteps(), w3, 7*h/8); +} + +// sunrise, sunset times +function drawSideBar3() { + g.setColor('#fff'); // sunrise white + g.drawImage(sunrise_img, w2 + (ws - 64)/2, 0, { scale: 1 }); + setTextColor(); + setSmallFont(); + g.setFontAlign(0, -1); + g.drawString(sunRise, w3, 64); + + g.setColor('#000'); // sunset black + g.drawImage(sunset_img, w2 + (ws - 64)/2, h/2, { scale: 1 }); + setTextColor(); + setSmallFont(); + g.setFontAlign(0, -1); + g.drawString(sunSet, w3, (h/2) + 64); +} + +function drawDateAndCalendar(x,y,dy,dd,mm) { + // day + setTextColor(); + setSmallFont(); + g.setFontAlign(0, -1); + g.drawString(dy.toUpperCase(), x, y); + + drawCalendar(x - (w/10), y + 28, w/5, 3, dd); + + // month + setTextColor(); + setSmallFont(); + g.setFontAlign(0, -1); + g.drawString(mm.toUpperCase(), x, y + 70); +} + +// at x,y width:wi thicknes:th +function drawCalendar(x,y,wi,th,str) { + g.setColor(g.theme.fg); + g.fillRect(x, y, x + wi, y + wi); + g.setColor(g.theme.bg); + g.fillRect(x + th, y + th, x + wi - th, y + wi - th); + g.setColor(g.theme.fg); + + let hook_t = 6; + // first calendar hook, one third in + g.fillRect(x + (wi/3) - (th/2), y - hook_t, x + wi/3 + th - (th/2), y + hook_t); + // second calendar hook, two thirds in + g.fillRect(x + (2*wi/3) -(th/2), y - hook_t, x + 2*wi/3 + th - (th/2), y + hook_t); + + setSmallFont(); + g.setFontAlign(0, 0); + g.drawString(str, x + wi/2 + th/2, y + wi/2 + th/2); +} + +function drawBattery(x,y,wi,hi) { + g.reset(); + g.setColor(g.theme.fg); + g.fillRect(x,y+2,x+wi-4,y+2+hi); // outer + g.clearRect(x+2,y+2+2,x+wi-4-2,y+2+hi-2); // centre + g.setColor(g.theme.fg); + g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact + g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level +} + +function getSteps() { + if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } + return '????'; +} + +// format steps so they fit in the place +function formatSteps() { + var s = getSteps(); + + if ( s == '????') { + return s; + } else if (s < 1000) { + return s + ''; + } else if (s < 10000) { + return '' + (s/1000).toFixed(1) + 'K'; + } + return Math.floor(s / 1000) + 'K'; +} + +function nextSidebar() { + if (++sideBar > 2) sideBar = 0; + log_debug("next: " + sideBar); +} + +function prevSidebar() { + if (--sideBar < 0) sideBar = 2; + log_debug("prev: " + sideBar); +} + +Bangle.setUI("clockupdown", btn=> { + if (btn<0) prevSidebar(); + if (btn>0) nextSidebar(); + 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; + nextSidebar(); + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +log_debug("starting.."); +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. + */ +for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +loadSettings(); +loadLocation(); +draw(); // queues the next draw for a minutes time diff --git a/apps/rebble/rebble.icon.js b/apps/rebble/rebble.icon.js new file mode 100644 index 000000000..4c898974e --- /dev/null +++ b/apps/rebble/rebble.icon.js @@ -0,0 +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")) diff --git a/apps/rebble/rebble.png b/apps/rebble/rebble.png new file mode 100644 index 000000000..69653015c Binary files /dev/null and b/apps/rebble/rebble.png differ diff --git a/apps/rebble/rebble.settings.js b/apps/rebble/rebble.settings.js new file mode 100644 index 000000000..db3bab878 --- /dev/null +++ b/apps/rebble/rebble.settings.js @@ -0,0 +1,38 @@ +(function(back) { + const SETTINGS_FILE = "rebble.json"; + + // initialize with default settings... + let s = {'bg': '#0f0', 'color': 'Green'} + + // ...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) || s; + const saved = settings || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; + var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; + + E.showMenu({ + '': { 'title': 'Rebble Clock' }, + '< Back': back, + 'Colour': { + value: 0 | color_options.indexOf(s.color), + min: 0, max: 5, + format: v => color_options[v], + onchange: v => { + s.color = color_options[v]; + s.bg = bg_code[v]; + save(); + }, + } + }); +}) diff --git a/apps/rebble/screenshot_rebble.png b/apps/rebble/screenshot_rebble.png new file mode 100644 index 000000000..3cbd77d8b Binary files /dev/null and b/apps/rebble/screenshot_rebble.png differ diff --git a/apps/rebble/screenshot_rebble2.png b/apps/rebble/screenshot_rebble2.png new file mode 100644 index 000000000..186b8f21c Binary files /dev/null and b/apps/rebble/screenshot_rebble2.png differ diff --git a/apps/rebble/screenshot_rebble3.png b/apps/rebble/screenshot_rebble3.png new file mode 100644 index 000000000..dca65c0fc Binary files /dev/null and b/apps/rebble/screenshot_rebble3.png differ diff --git a/apps/rebble/screenshot_rebble4.png b/apps/rebble/screenshot_rebble4.png new file mode 100644 index 000000000..b52be8be5 Binary files /dev/null and b/apps/rebble/screenshot_rebble4.png differ diff --git a/apps/rtorch/ChangeLog b/apps/rtorch/ChangeLog index 06f10fe08..13cbb6e72 100644 --- a/apps/rtorch/ChangeLog +++ b/apps/rtorch/ChangeLog @@ -1 +1,2 @@ 0.01: Cloning torch and making it red :D +0.02: Modify for setUI and Bangle 2 diff --git a/apps/rtorch/app.js b/apps/rtorch/app.js index 4f6b1d6f7..03a50ee10 100644 --- a/apps/rtorch/app.js +++ b/apps/rtorch/app.js @@ -2,21 +2,38 @@ Bangle.setLCDPower(1); Bangle.setLCDTimeout(0); g.reset(); c = 1; + function setColor(delta){ c+=delta; c = Math.max(c,0); c = Math.min(c,2); if (c<1){ g.setColor(c,0,0); + Bangle.setLCDBrightness(c >= 0.1 ? c : 0.1); }else{ g.setColor(1,c-1,c-1); + Bangle.setLCDBrightness(1); } g.fillRect(0,0,g.getWidth(),g.getHeight()); } -setColor(0) -// BTN1 light up toward white -// BTN3 light down to red -// BTN2 to reset -setWatch(()=>setColor(0.1), BTN1, { repeat:true, edge:"rising", debounce: 50 }); -setWatch(()=>load(), BTN2); -setWatch(()=>setColor(-0.1), BTN3, { repeat:true, edge:"rising", debounce: 50 }); + +function updownHandler(direction){ + if (direction == undefined){ + c=1; + setColor(0); + } else { + setColor(-direction * 0.1); + } +} + +setColor(0); + +// Bangle 1: +// BTN1: light up toward white +// BTN3: light down to red +// BTN2: reset +// Bangle 2: +// Swipe up: light up toward white +// Swipe down: light down to red +// BTN1: reset +Bangle.setUI("updown", updownHandler); diff --git a/apps/scribble/ChangeLog b/apps/scribble/ChangeLog new file mode 100644 index 000000000..af7f83942 --- /dev/null +++ b/apps/scribble/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release diff --git a/apps/scribble/README.md b/apps/scribble/README.md new file mode 100644 index 000000000..651ecfbf1 --- /dev/null +++ b/apps/scribble/README.md @@ -0,0 +1,15 @@ +# Scribble + +A tree-based keyboard, inspired by Tertiary Text on Pebble. + +![](screenshot.png) + +## Usage + +Tap a button to select text. +Swipe left to right for enter space. +Swipe right to left to delete. + +## Creator + +enricorov diff --git a/apps/scribble/add_to_apps.json b/apps/scribble/add_to_apps.json new file mode 100644 index 000000000..054f35d55 --- /dev/null +++ b/apps/scribble/add_to_apps.json @@ -0,0 +1,14 @@ +{ "id": "scribble", + "name": "Scribble", + "shortName":"Scribble", + "version":"0.01", + "description": "A keyboard on your wrist!", + "icon": "app.png", + "tags": "keyboard, text, scribble", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"scribble.app.js","url":"app.js"}, + {"name":"scribble.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/scribble/app-icon.js b/apps/scribble/app-icon.js new file mode 100644 index 000000000..740706094 --- /dev/null +++ b/apps/scribble/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwMB/4A2/IFE+IFE+YFE84FE44FE54SEz/jAocfDAk/54EC/1/x4FC/l/z4FDCQJGD/wFD+IYBIwYSBIwf4IwhfEIwuPIwkPIwMAj//g/P/gFCkOP/AEB/8wj5+Dn0/Aoc8n/4JAU4v/8gYFBaYWAJ4MHAoPwEgMPOgUfLogJCBYQFE+AFD8BHB/EAAAV/AoYyCB4IKBc6QA==")) \ No newline at end of file diff --git a/apps/scribble/app.js b/apps/scribble/app.js new file mode 100644 index 000000000..99ee3f717 --- /dev/null +++ b/apps/scribble/app.js @@ -0,0 +1,469 @@ +const black = "#000000"; +const white = "#ffffff"; +const gray1 = "#444444"; +const gray2 = "#888888"; +const gray3 = "#bbbbbb"; + +const red = "#FF0000"; +const green = "#00FF00"; +const blue = "#0000FF"; + +const transp = -1; +const abc = "abcdefghijklmnopqrstuvwxyz1234567890"; +// const abc_up = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; +const uppercase = 1; +var last_layer = false; // set to true at the last layer of the tree +let chunk_size = 6; + +const font_height = 2; +const global_font = "Dennis8"; +require("FontDennis8").add(Graphics); + +const editable_buf = "Scribble"; + +const left = 3; +const _screen_mid = g.getWidth() / 2; +const right = 176 - 4; + +const box_size = { + w: _screen_mid - 6, + h: 46, +}; + +const spacing = 4; +const border = 4; +const top_start = 25; + +const pos_y = [ + top_start, + top_start + (box_size.h + spacing), + top_start + (box_size.h + spacing) * 2, +]; + +// list of points to render +const points = { + "3x2": [{ x: left, y: pos_y[0] }, + { x: left, y: pos_y[1] }, + { x: left, y: pos_y[2] }, + { x: _screen_mid + 2, y: pos_y[0] }, + { x: _screen_mid + 2, y: pos_y[1] }, + { x: _screen_mid + 2, y: pos_y[2] }, + ] +}; + +g.theme = { + fg: white, + bg: black, + fg2: white, + bg2: black, + fgH: black, + bgH: red, + dark: false, +}; + +const maxX = g.getWidth(); +const maxY = g.getHeight(); +const fontSize = g.getWidth() > 200 ? 2 : 1; +const rowN = 7; +const colN = 7; +const headerH = maxY / 7; +const rowH = (maxY - headerH) / rowN; +const colW = maxX / colN; + +function getRndInteger(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +class Window { + constructor(label, bgCol) { + this.label = "win_" + this.label += (typeof label !== "undefined") ? label : "Unset"; + console.log(`Constructing Window ${this.label}, args: ${arguments}`) + + this.bgCol = bgCol; + this.layers = []; + } + + push(layer) { + layer.label=`${this.layers.length}_${layer.label}`; + this.layers.push(layer); + } + pop() { + this.layers.pop(); + } + + top_layer() { + return this.layers[this.layers.length - 1]; + } + + render() { + + if (this.bgCol !== transp) { + console.log(`${this.label}: filling bg in ${this.bgCol}`); + g.setColor(this.bgCol); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + } + + + let i = 0; + this.layers.forEach((lyr) => { + // console.log(`Rendering Layer ${i} ${lyr.label}`) + i++; + lyr.render(); + }); + } +} + +class Layer { + constructor(label) { + + this.label = "lyr_" + + this.label += (typeof label !== "undefined") ? label : "Unset"; + console.log(`Constructing Layer ${this.label}, args: ${arguments}`) + this.items = []; + // console.log(`bg is ${bg} type ${typeof bg}`) + + } + + push(button) { + this.items.push(button); + } + + setLabel(label) { + this.label = label; + } + + parseTaps(xy) { + this.items.forEach(item => { + // // print(item) + if (item.was_tapped(xy)) { + // pass parent layer to the tapped button + item.callback(this); + } + }); + } + + render() { + + this.items.forEach((item) => { + + item.render(); + }); + } +} + +class BTN_layer extends Layer { + + constructor(label, layout) { + super(); + Layer.call(this, label) + + this.alphabet = (uppercase) ? abc.toUpperCase() : abc; + console.log(`Constructing BTN_Layer ${this.label}, layout ${this.layout}`) + + if (layout in points) { + + this.create_layout(layout); + + } + else { + throw `Invalid layout passed ->[${layout}]`; + } + + // // print(this); + + } + + render() { + + Layer.prototype.render.call(this); + } + + create_layout(layout) { + + console.log(`Creating layout ${layout}`); + + let start_p = 0; + + this.items = this.push_buttons(points[layout], this.alphabet, start_p, chunk_size) + + } + + push_buttons(points, in_string, start_p) { + + items = []; + spacer = "" // char interposed b/w the two halves of text per button + + for (let i = 0; i < points.length; i++) { + substr = `${in_string.substring( + start_p, + start_p + chunk_size / 2 + )}${spacer}${in_string.substring(start_p + chunk_size / 2, start_p + chunk_size)}`; + + btn_label = + uppercase === 1 + ? substr.toUpperCase() + : substr; + + start_p += chunk_size; + + items.push( + new Button( + i, // ID of button + points[i].x, // left + points[i].y, // top + btn_label, // text to render in the button + box_size.w, // width + box_size.h, // height + g.theme.bg, // box bg + white, // box fill + black // text col + ) + ); + } + + return items; + } + + update_labels(in_string, start_p, chk_size) { + // print(`Updating labels | in_string ${in_string} start_p ${start_p} chk_size ${chk_size}`); + in_string.replace('\n', ''); // remove newlines just in case + + spacer = "" // char interposed b/w the two halves of text per button + + for (let i = 0; i < this.items.length; i++) { + + item = this.items[i]; + substr = (chk_size < 3) + ? in_string.substring(start_p + chk_size * i, start_p + (chk_size * (i + 1))) + : `${in_string.substring( + start_p + chk_size * i, + start_p + chk_size * i + chk_size / 2 + )}${spacer}${in_string.substring(start_p + chk_size * i + chk_size / 2, start_p + chk_size * i + chk_size)}`; + // // print(`(chk_size > 3): ${(chk_size > 3)}`) + // print(`Label ${i} -> ${substr}`); + item.setLabel(substr); + } + + } + + zoom_in(id) { + let start_p = id * chunk_size; + // print(`Zooming in | start_p ${start_p}`) + if (chunk_size % this.items.length !== 0) { + throw `Chunk size [${chunk_size}] does not fit #btns [${this.items.length}]` + } + subchunk_size = chunk_size / this.items.length; + + substr = this.alphabet.substring(start_p, start_p + chunk_size); + // print(`substr ${substr}`); + // print(`subchunk_size ${subchunk_size}`); + this.update_labels(substr, 0, subchunk_size); + + } + +} + +class Button { + constructor(id, x, y, text, w, h, col, bgCol, txtCol, font) { + this.id = id; + this.label = `btn_${this.id}`; + + this.text = text; + this.x1 = x; + this.y1 = y; + this.w = w; + this.h = h; + this.col = typeof col !== "undefined" ? col : black; + this.bgCol = typeof bgCol !== "undefined" ? bgCol : gray2; + this.txtCol = typeof txtCol !== "undefined" ? txtCol : black; + // this.font = font; + + this.x2 = this.x1 + this.w; + this.y2 = this.y1 + this.h; + this.center = { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; + + + console.log(`Constructed button `) + // // print(this); + } + + render() { + // console.log( + // `Button ${this.text} -> P1: (${this.x1}, ${this.y1}) | P2: (${this.x2}, ${this.y2})` + // ); + + g.setColor(this.bgCol); + g.fillRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(this.col); + g.drawRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(this.txtCol); + + g.setFontAlign(0, 0).setFont(global_font, font_height); + g.drawString(this.text, this.center.x, this.center.y); + } + + // short tap callback func + callback(parent_layer) { + // print(`Tapped button ${this.id}`); + + // this.highlight(); // TODO set up highlighting + if (last_layer) { + l_text.items[0].text += this.text; + // print(`Updated buffer to ${l_text.items[0].text}`) + parent_layer.update_labels(parent_layer.alphabet, 0, chunk_size); + last_layer = false; + } + else { + parent_layer.zoom_in(this.id); + last_layer = true; + } + } + + was_tapped(xy) { + var x = xy.x; + var y = xy.y; + + if ((x > this.x1 && x < this.x2) && (y > this.y1 && y < this.y2)) { + return true; + } + else { + return false; + } + } + + setLabel(lbl) { + // // print(`Button ${this.id}, updating label ${this.text} with ${lbl}`); + this.text = lbl; + } + + getLabel(lbl) { + + return this.label; + } + + highlight() { + + g.setColor(g.theme.bgH); + g.fillRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(g.theme.fgH); + g.drawRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(this.fg); + + g.setFontAlign(0, 0).setFont(global_font, font_height); + g.drawString(this.text, this.center.x, this.center.y); + + } + +} + +class TextBox { + + constructor(x, y, text, col) { + + // x and y are the center points + this.x = x; + this.y = y; + this.text = (typeof text !== undefined) ? text : "Default"; + this.col = (typeof col !== undefined) ? col : red; + + // console.log(`Constr TextBox ${this.text} -> Center: (${this.x}, ${this.y}) | Col ${this.col}`); + } + + render() { + // console.log(`Rendering TextBox`) + + var align_center = (0, 1); + var align_right = (0, 0); + alignment = (g.stringWidth(this.text) < g.getWidth()) ? align_center : align_right; + // coords = (g.stringWidth(this.text) < g.getWidth()- 20) ? {x:this.x, y:this.y} : {x:g.getWidth()-border, y:this.y} + coords = { x: this.x, y: this.y }; + g.setColor(this.col); + g.setFontAlign(0, 0).setFont(global_font, font_height); + g.drawString(this.text, coords.x, coords.y); + + } +} + +/* Screen refresh *************************************/ + +function draw(obj) { + console.log("draw()"); + obj.render(); +} + +let tickTimer; + +function clearTickTimer() { + if (tickTimer) { + clearTimeout(tickTimer); + tickTimer = undefined; + } +} + +function queueNextTick() { + clearTickTimer(); + tickTimer = setTimeout(tick, 5000); +} + +function tick() { + console.log("tick"); + draw(window); + // queueNextTick(); +} + +/* Init **********************************************/ + +var window = new Window("abc", red); + +var l_btns = new BTN_layer("btns", "3x2"); + +var l_text = new Layer("text"); // black + +var box = new TextBox( + _screen_mid, + 12, + editable_buf, + white +); + +l_text.push(box); + +window.push(l_text); +window.push(l_btns); + +// Set up callbacks for touches + +Bangle.on('touch', function (button, xy) { + + window.top_layer().parseTaps(xy); + window.render(); + +}); + +Bangle.on('swipe', function (direction) { + + console.log(`Swipe dir ${direction}`); + + if (direction === -1) { // left + + l_text.items[0].text = l_text.items[0].text.slice(0, -1); + + } else if (direction == 1) { // right + + l_text.items[0].text += ' '; + + } + window.render(); + +}); + +// Clear the screen once, at startup +g.clear(); + +// Start ticking +tick(); diff --git a/apps/scribble/app.png b/apps/scribble/app.png new file mode 100644 index 000000000..01a6acb72 Binary files /dev/null and b/apps/scribble/app.png differ diff --git a/apps/scribble/screenshot.png b/apps/scribble/screenshot.png new file mode 100644 index 000000000..a3dc0cff2 Binary files /dev/null and b/apps/scribble/screenshot.png differ diff --git a/apps/sensible/ChangeLog b/apps/sensible/ChangeLog index ba597a22f..c50431f51 100644 --- a/apps/sensible/ChangeLog +++ b/apps/sensible/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Corrected variable initialisation +0.03: Advertise app name, added screenshots +0.04: Advertise bar, GPS, HRM and mag services diff --git a/apps/sensible/README.md b/apps/sensible/README.md index f79b61aea..fcff3b0f9 100644 --- a/apps/sensible/README.md +++ b/apps/sensible/README.md @@ -17,7 +17,7 @@ Currently implements: - Heart Rate Monitor - Magnetometer -in the menu display but NOT YET in Bluetooth Low Energy advertising (which will be implemented in a subsequent version). +in the menu display, and broadcasts all sensor data readings _except_ acceleration in Bluetooth Low Energy advertising packets as GATT characteristic services. ## Controls diff --git a/apps/sensible/screenshot-acc.png b/apps/sensible/screenshot-acc.png new file mode 100644 index 000000000..b286d1ed5 Binary files /dev/null and b/apps/sensible/screenshot-acc.png differ diff --git a/apps/sensible/screenshot-bar.png b/apps/sensible/screenshot-bar.png new file mode 100644 index 000000000..781ddbaa6 Binary files /dev/null and b/apps/sensible/screenshot-bar.png differ diff --git a/apps/sensible/screenshot-gps.png b/apps/sensible/screenshot-gps.png new file mode 100644 index 000000000..3fd1229e3 Binary files /dev/null and b/apps/sensible/screenshot-gps.png differ diff --git a/apps/sensible/screenshot-hrm.png b/apps/sensible/screenshot-hrm.png new file mode 100644 index 000000000..aa6a0574f Binary files /dev/null and b/apps/sensible/screenshot-hrm.png differ diff --git a/apps/sensible/screenshot-mag.png b/apps/sensible/screenshot-mag.png new file mode 100644 index 000000000..829ac6727 Binary files /dev/null and b/apps/sensible/screenshot-mag.png differ diff --git a/apps/sensible/screenshot-top.png b/apps/sensible/screenshot-top.png new file mode 100644 index 000000000..e485933f0 Binary files /dev/null and b/apps/sensible/screenshot-top.png differ diff --git a/apps/sensible/sensible.js b/apps/sensible/sensible.js index c569ff720..3da39998e 100644 --- a/apps/sensible/sensible.js +++ b/apps/sensible/sensible.js @@ -6,6 +6,10 @@ // Non-user-configurable constants const APP_ID = 'sensible'; +const ESPRUINO_COMPANY_CODE = 0x0590; +const APP_ADVERTISING_DATA = [ 0x12, 0xff, 0x90, 0x05, 0x7b, 0x6e, 0x61, 0x6d, + 0x65, 0x3a, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x62, + 0x6c, 0x65, 0x7d ]; // Global variables @@ -19,6 +23,12 @@ let isBarEnabled = true; let isGpsEnabled = true; let isHrmEnabled = true; let isMagEnabled = true; +let isNewAccData = false; +let isNewBarData = false; +let isNewGpsData = false; +let isNewHrmData = false; +let isNewMagData = false; + // Menus @@ -90,9 +100,121 @@ let magMenu = { }; +// Check for new sensor data and update the advertising sequence +function transmitUpdatedSensorData() { + let data = [ APP_ADVERTISING_DATA ]; // Always advertise at least app name + + if(isNewBarData) { + data.push(encodeBarServiceData()); + isNewBarData = false; + } + + if(isNewGpsData && gps.lat && gps.lon) { + data.push(encodeGpsServiceData()); + isNewGpsData = false; + } + + if(isNewHrmData) { + data.push({ 0x2a37: [ 0, hrm.bpm ] }); + isNewHrmData = false; + } + + if(isNewMagData) { + data.push(encodeMagServiceData()); + isNewMagData = false; + } + + NRF.setAdvertising(data, { showName: false, interval: 200 }); +} + + +// Encode the bar service data to fit in a Bluetooth PDU +function encodeBarServiceData() { + let tEncoded = Math.round(bar.temperature * 100); + let pEncoded = Math.round(bar.pressure * 100); + let eEncoded = Math.round(bar.altitude * 100); + + if(bar.temperature < 0) { + tEncoded += 0x10000; + } + if(bar.altitude < 0) { + eEncoded += 0x1000000; + } + + let t = [ tEncoded & 0xff, (tEncoded >> 8) & 0xff ]; + let p = [ pEncoded & 0xff, (pEncoded >> 8) & 0xff, (pEncoded >> 16) & 0xff, + (pEncoded >> 24) & 0xff ]; + let e = [ eEncoded & 0xff, (eEncoded >> 8) & 0xff, (eEncoded >> 16) & 0xff ]; + + return [ + 0x02, 0x01, 0x06, // Flags + 0x05, 0x16, 0x6e, 0x2a, t[0], t[1], // Temperature + 0x07, 0x16, 0x6d, 0x2a, p[0], p[1], p[2], p[3], // Pressure + 0x06, 0x16, 0x6c, 0x2a, e[0], e[1], e[2] // Elevation + ]; +} + + +// Encode the GPS service data using the Location and Speed characteristic +function encodeGpsServiceData() { + let latEncoded = Math.round(gps.lat * 10000000); + let lonEncoded = Math.round(gps.lon * 10000000); + let hEncoded = Math.round(gps.course * 100); + let sEncoded = Math.round(1000 * gps.speed / 36); + + if(gps.lat < 0) { + latEncoded += 0x100000000; + } + if(gps.lon < 0) { + lonEncoded += 0x100000000; + } + + let s = [ sEncoded & 0xff, (sEncoded >> 8) & 0xff ]; + let lat = [ latEncoded & 0xff, (latEncoded >> 8) & 0xff, + (latEncoded >> 16) & 0xff, (latEncoded >> 24) & 0xff ]; + let lon = [ lonEncoded & 0xff, (lonEncoded >> 8) & 0xff, + (lonEncoded >> 16) & 0xff, (lonEncoded >> 24) & 0xff ]; + let h = [ hEncoded & 0xff, (hEncoded >> 8) & 0xff ]; + + return [ + 0x02, 0x01, 0x06, // Flags + 0x11, 0x16, 0x67, 0x2a, 0x95, 0x02, s[0], s[1], lat[0], lat[1], lat[2], + lat[3], lon[0], lon[1], lon[2], lon[3], h[0], h[1] // Location and Speed + ]; +} + + +// Encode the mag service data using the magnetic flux density 3D characteristic +function encodeMagServiceData() { + let xEncoded = mag.x; // TODO: units??? + let yEncoded = mag.y; + let zEncoded = mag.z; + + if(xEncoded < 0) { + xEncoded += 0x10000; + } + if(yEncoded < 0) { + yEncoded += 0x10000; + } + if(yEncoded < 0) { + yEncoded += 0x10000; + } + + let x = [ xEncoded & 0xff, (xEncoded >> 8) & 0xff ]; + let y = [ yEncoded & 0xff, (yEncoded >> 8) & 0xff ]; + let z = [ zEncoded & 0xff, (zEncoded >> 8) & 0xff ]; + + return [ + 0x02, 0x01, 0x06, // Flags + 0x09, 0x16, 0xa1, 0x2a, x[0], x[1], y[0], y[1], z[0], z[1] // Mag 3D + ]; +} + + // Update acceleration Bangle.on('accel', function(newAcc) { acc = newAcc; + isNewAccData = true; if(isAccMenu) { accMenu.x.value = acc.x.toFixed(2); @@ -105,6 +227,7 @@ Bangle.on('accel', function(newAcc) { // Update barometer Bangle.on('pressure', function(newBar) { bar = newBar; + isNewBarData = true; if(isBarMenu) { barMenu.Altitude.value = bar.altitude.toFixed(1) + 'm'; @@ -117,6 +240,7 @@ Bangle.on('pressure', function(newBar) { // Update GPS Bangle.on('GPS', function(newGps) { gps = newGps; + isNewGpsData = true; if(isGpsMenu) { gpsMenu.Lat.value = gps.lat.toFixed(4); @@ -131,6 +255,7 @@ Bangle.on('GPS', function(newGps) { // Update heart rate monitor Bangle.on('HRM', function(newHrm) { hrm = newHrm; + isNewHrmData = true; if(isHrmMenu) { hrmMenu.BPM.value = hrm.bpm; @@ -142,6 +267,7 @@ Bangle.on('HRM', function(newHrm) { // Update magnetometer Bangle.on('mag', function(newMag) { mag = newMag; + isNewMagData = true; if(isMagMenu) { magMenu.x.value = mag.x; @@ -159,4 +285,5 @@ Bangle.setBarometerPower(isBarEnabled, APP_ID); Bangle.setGPSPower(isGpsEnabled, APP_ID); Bangle.setHRMPower(isHrmEnabled, APP_ID); Bangle.setCompassPower(isMagEnabled, APP_ID); -E.showMenu(mainMenu); \ No newline at end of file +E.showMenu(mainMenu); +setInterval(transmitUpdatedSensorData, 1000); \ No newline at end of file diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index faa50405f..64844dcbc 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -36,3 +36,7 @@ 0.31: Remove Bangle 1 settings when running on Bangle 2 0.32: Fix 'beep' menu on Bangle.js 2 0.33: Really fix 'beep' menu on Bangle.js 2 this time +0.34: Remove Quiet Mode LCD settings: now handled by Quiet Mode Schedule app +0.35: Change App/Widget settings to 'App Settings' so it fits on Bangle screen +0.36: Added 'Utils' menu with helpful utilities for restoring Bangle.js +0.37: Going into passkey menu now saves settings with passkey diff --git a/apps/setting/README.md b/apps/setting/README.md index 1875fc3b0..305c0b610 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -2,27 +2,26 @@ This is Bangle.js's settings menu -* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up) * **App/Widget Settings** settings specific to installed applications * **BLE** Bluetooth Settings menu - see below. -* **Debug Info** should debug info be shown on the watch's screen or not? * **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected * **Vibration** enable/disable the vibration motor * **Quiet Mode** prevent notifications/alarms from vibrating/beeping/turning the screen on - see below * **Locale** set time zone/whether the clock is 12/24 hour (for supported clocks) * **Select Clock** if you have more than one clock face, select the default one -* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device. - * **NOTE:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps. * **Set Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader * **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below. * **Theme** Adjust the colour scheme -* **Reset Settings** Reset the settings to defaults +* **Utils** Utilities - including resetting settings (see below) * **Turn Off** Turn Bangle.js off ## BLE - Bluetooth Settings +* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up) * **BLE** is Bluetooth LE enabled and the watch connectable? * **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps? As long as your watch firmware is up to date, Gadgetbridge will work even with `Programmable` set to `Off`. +* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device. + * **NOTE:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps. * **Passkey BETA** allows you to set a passkey that is required to connect and pair to Bangle.js. **Note:** This is Beta and you will almost certainly encounter issues connecting with Web Bluetooth using this option. * **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app. * **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again. @@ -44,6 +43,16 @@ The exact effects depend on the app. In general the watch will not wake up by i - Off: Normal operation - Alarms: Stops notifications, but "alarm" apps will still work - Silent: Blocks even alarms -* **LCD Brightness**, **LCD Timeout**, **Wake on X**: - Override default settings while Quit Mode is active (either as *Alarms* or *Silent*) - \ No newline at end of file + +## Utils + + +* **Debug Info** should debug info be shown on the watch's screen or not? + * `Hide` (default) do not show debug information + * `Show` Show on the Bangle's screen (when not connected to Bluetooth or `Programmable:off`) + * `Log` Show on the Bangle's screen **and** write to a file called `log.txt` on Storage (when not connected to Bluetooth or `Programmable:off`). Warning - this file is appended to so may grow to be large if this is left enabled. +* **Compact Storage** Removes deleted/old files from Storage - this will speed up your Bangle.js +* **Rewrite Settings** Should not normally be required, but if `.boot0` has been deleted/corrupted (and so no settings are being loaded) this will fix it. +* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours. +* **Reset Settings** Reset the settings (as set in this app) to defaults. Does not reset settings for other apps. +* **Factory Reset** (not available on Bangle.js 1) - wipe **everything** and return to a factory state diff --git a/apps/setting/settings.js b/apps/setting/settings.js index fcf651b6f..9cba09d6c 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -7,17 +7,12 @@ let settings; function updateSettings() { //storage.erase('setting.json'); // - not needed, just causes extra writes if settings were the same - if (Object.keys(settings.qmOptions).length === 0) delete settings.qmOptions; storage.write('setting.json', settings); - if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in this file } function updateOptions() { updateSettings(); Bangle.setOptions(settings.options) - if (settings.quiet) { - Bangle.setOptions(settings.qmOptions) - } } function gToInternal(g) { @@ -56,20 +51,14 @@ function resetSettings() { twistMaxY: -800, twistTimeout: 1000 }, - // Quiet Mode options: - // we only set these if we want to override the default value - // qmOptions: {}, - // qmBrightness: undefined, - // qmTimeout: undefined, }; updateSettings(); } settings = storage.readJSON('setting.json', 1); if (!settings) resetSettings(); -if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in here -const boolFormat = v => v ? "On" : "Off"; +const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; function showMainMenu() { var beepMenuItem; @@ -88,7 +77,7 @@ function showMainMenu() { }; } else { // Bangle.js 1 var beepV = [false, true, "vib"]; - var beepN = ["Off", "Piezo", "Vibrate"]; + var beepN = [/*LANG*/"Off", /*LANG*/"Piezo", /*LANG*/"Vibrate"]; beepMenuItem = { value: Math.max(0 | beepV.indexOf(settings.beep),0), min: 0, max: beepV.length-1, @@ -106,19 +95,10 @@ function showMainMenu() { const mainmenu = { '': { 'title': 'Settings' }, '< Back': ()=>load(), - 'Make Connectable': ()=>makeConnectable(), - 'App/Widget Settings': ()=>showAppSettingsMenu(), - 'BLE': ()=>showBLEMenu(), - 'Debug Info': { - value: settings.log, - format: v => v ? "Show" : "Hide", - onchange: () => { - settings.log = !settings.log; - updateSettings(); - } - }, - 'Beep': beepMenuItem, - 'Vibration': { + /*LANG*/'App Settings': ()=>showAppSettingsMenu(), + /*LANG*/'BLE': ()=>showBLEMenu(), + /*LANG*/'Beep': beepMenuItem, + /*LANG*/'Vibration': { value: settings.vibrate, format: boolFormat, onchange: () => { @@ -130,14 +110,23 @@ function showMainMenu() { } } }, - "Quiet Mode": ()=>showQuietModeMenu(), - 'Locale': ()=>showLocaleMenu(), - 'Select Clock': ()=>showClockMenu(), - 'Set Time': ()=>showSetTimeMenu(), - 'LCD': ()=>showLCDMenu(), - 'Theme': ()=>showThemeMenu(), - 'Reset Settings': ()=>showResetMenu(), - 'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }, + /*LANG*/"Quiet Mode": { + value: settings.quiet|0, + format: v => ["Off", "Alarms", "Silent"][v%3], + onchange: v => { + settings.quiet = v%3; + updateSettings(); + updateOptions(); + if ("qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); + }, + }, + /*LANG*/'Locale': ()=>showLocaleMenu(), + /*LANG*/'Select Clock': ()=>showClockMenu(), + /*LANG*/'Set Time': ()=>showSetTimeMenu(), + /*LANG*/'LCD': ()=>showLCDMenu(), + /*LANG*/'Theme': ()=>showThemeMenu(), + /*LANG*/'Utils': ()=>showUtilMenu(), + /*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }, }; return E.showMenu(mainmenu); @@ -148,6 +137,7 @@ function showBLEMenu() { var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"]; E.showMenu({ '< Back': ()=>showMainMenu(), + 'Make Connectable': ()=>makeConnectable(), 'BLE': { value: settings.ble, format: boolFormat, @@ -286,8 +276,10 @@ function showPasskeyMenu() { showBLEMenu(); } }; - if (!settings.passkey || settings.passkey.length!=6) + if (!settings.passkey || settings.passkey.length!=6) { settings.passkey = "123456"; + updateSettings(); + } for (var i=0;i<6;i++) (function(i){ menu[`Digit ${i+1}`] = { value : 0|settings.passkey[i], @@ -352,9 +344,7 @@ function showLCDMenu() { onchange: v => { settings.brightness = v || 1; updateSettings(); - if (!(settings.quiet && "qmBrightness" in settings)) { - Bangle.setLCDBrightness(settings.brightness); - } + Bangle.setLCDBrightness(settings.brightness); } }, 'LCD Timeout': { @@ -365,9 +355,7 @@ function showLCDMenu() { onchange: v => { settings.timeout = 0 | v; updateSettings(); - if (!(settings.quiet && "qmTimeout" in settings)) { - Bangle.setLCDTimeout(settings.timeout); - } + Bangle.setLCDTimeout(settings.timeout); } }, 'Wake on BTN1': { @@ -455,105 +443,6 @@ function showLCDMenu() { }); return E.showMenu(lcdMenu) } -function showQuietModeMenu() { - // we always keep settings.quiet and settings.qmOptions - // other qm values are deleted when not set - const modes = ["Off", "Alarms", "Silent"]; - const qmDisabledFormat = v => v ? "Off" : "-"; - const qmMenu = { - "": {"title": "Quiet Mode"}, - "< Back": () => showMainMenu(), - "Quiet Mode": { - value: settings.quiet|0, - format: v => modes[v%3], - onchange: v => { - settings.quiet = v%3; - updateSettings(); - updateOptions(); - if ("qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} - }, - }, - "LCD Brightness": { - value: settings.qmBrightness || 0, - min: 0, // 0 = use default - max: 1, - step: 0.1, - format: v => (v>0.05) ? v : "-", - onchange: v => { - if (v>0.05) { // prevent v=0.000000000000001 bugs - settings.qmBrightness = v; - } else { - delete settings.qmBrightness; - } - updateSettings(); - if (settings.qmBrightness) { // show result, even if not quiet right now - Bangle.setLCDBrightness(v); - } else { - Bangle.setLCDBrightness(settings.brightness); - } - }, - }, - "LCD Timeout": { - value: settings.qmTimeout || 0, - min: 0, // 0 = use default (no constant on for quiet mode) - max: 60, - step: 5, - format: v => v>1 ? v : "-", - onchange: v => { - if (v>1) { - settings.qmTimeout = v; - } else { - delete settings.qmTimeout; - } - updateSettings(); - if (settings.quiet && v>1) { - Bangle.setLCDTimeout(v); - } else { - Bangle.setLCDTimeout(settings.timeout); - } - }, - }, - // we disable wakeOn* events by overwriting them as false in qmOptions - // not disabled = not present in qmOptions at all - "Wake on FaceUp": { - value: "wakeOnFaceUp" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnFaceUp" in settings.qmOptions) { - delete settings.qmOptions.wakeOnFaceUp; - } else { - settings.qmOptions.wakeOnFaceUp = false; - } - updateOptions(); - }, - }, - "Wake on Touch": { - value: "wakeOnTouch" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnTouch" in settings.qmOptions) { - delete settings.qmOptions.wakeOnTouch; - } else { - settings.qmOptions.wakeOnTouch = false; - } - updateOptions(); - }, - }, - "Wake on Twist": { - value: "wakeOnTwist" in settings.qmOptions, - format: qmDisabledFormat, - onchange: () => { - if ("wakeOnTwist" in settings.qmOptions) { - delete settings.qmOptions.wakeOnTwist; - } else { - settings.qmOptions.wakeOnTwist = false; - } - updateOptions(); - }, - }, - }; - return E.showMenu(qmMenu); -} function showLocaleMenu() { const localemenu = { @@ -581,21 +470,63 @@ function showLocaleMenu() { return E.showMenu(localemenu); } -function showResetMenu() { - const resetmenu = { - '': { 'title': 'Reset' }, +function showUtilMenu() { + var menu = { + '': { 'title': 'Utilities' }, '< Back': ()=>showMainMenu(), + 'Debug Info': { + value: E.clip(0|settings.log,0,2), + format: v => ["Hide","Show","Log"][E.clip(0|v,0,2)], + onchange: v => { + settings.log = v; + updateSettings(); + } + }, + 'Compact Storage': () => { + E.showMessage("Compacting...\nTakes approx\n1 minute",{title:"Storage"}); + require("Storage").compact(); + showUtilMenu(); + }, + '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.'); + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat"); + if (Bangle.setHRMPower) Bangle.setHRMPower(1,"flat"); + if (Bangle.setCompassPower) Bangle.setCompassPower(1,"flat"); + if (Bangle.setBarometerPower) Bangle.setBarometerPower(1,"flat"); + if (Bangle.setHRMPower) Bangle.setGPSPower(1,"flat"); + setInterval(function() { + var i=1000;while (i--); + }, 1); + }, 'Reset Settings': () => { - E.showPrompt('Reset Settings?').then((v) => { + E.showPrompt('Reset to Defaults?',{title:"Settings"}).then((v) => { if (v) { E.showMessage('Resetting'); resetSettings(); - } - setTimeout(showMainMenu, 50); + setTimeout(showMainMenu, 50); + } else showUtilMenu(); }); } }; - return E.showMenu(resetmenu); + if (Bangle.factoryReset) { + menu['Factory Reset'] = ()=>{ + E.showPrompt('This will remove everything!',{title:"Factory Reset"}).then((v) => { + if (v) { + E.showMessage(); + Terminal.setConsole(); + Bangle.factoryReset(); + } else showUtilMenu(); + }); + } + } + + return E.showMenu(menu); } function makeConnectable() { diff --git a/apps/showimg/README.md b/apps/showimg/README.md new file mode 100644 index 000000000..9d7c0067a --- /dev/null +++ b/apps/showimg/README.md @@ -0,0 +1,3 @@ +Displays an image. I use this app to show my vaccination certificate. +The image is read from the file "showimage.user.img". +Returns to watch face after 60s/button push. diff --git a/apps/showimg/app-icon.js b/apps/showimg/app-icon.js new file mode 100644 index 000000000..1c73a2f72 --- /dev/null +++ b/apps/showimg/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAf////AHP/////AH//////AP/8AAAHAP4AAAAHAOAAMAAHAOAAeAAHAOAA+cAHAOAA/+AHAOAA/+AHAOAf/+AHAOA//+AHgOA///AHgOA///gDgOA/z/gDgOAfz/gDgOA///gDgOA///gDgOA//uADgOAf3+ADgOAP/+ADgOAD/8ADwOAB+4ADwOAA8AABwOAAAABBwOA8DgPxwOB/Dg/xwOB/jh/xwOB3zj5xwOB57nzxwOA4/njhwOA4/vHhwOA8f+PB4OAef+PB4OAef8eB4OAPP58A4OAHv/4A4OAH//wA4OAD//AA4OAA/8fn4PDgP///4P//////4P////9/wD///4AAA")) diff --git a/apps/showimg/app.js b/apps/showimg/app.js new file mode 100644 index 000000000..e00385bd7 --- /dev/null +++ b/apps/showimg/app.js @@ -0,0 +1,16 @@ +g.reset(); +g.clear(); +g.drawImage(require("Storage").read("showimg.user.img"),0,0); +drawTimeout = setTimeout(function() { + load(); +}, 60000); +setWatch(function() { + load(); +}, BTN, { repeat:false, edge:'falling' }); +var savedOptions=Bangle.getOptions(); +Bangle.setLCDBrightness(1); +var newOptions={ + lockTimeout:60000, + backlightTimeout:60000 +}; +Bangle.setOptions(newOptions); diff --git a/apps/showimg/app.png b/apps/showimg/app.png new file mode 100644 index 000000000..306db9b42 Binary files /dev/null and b/apps/showimg/app.png differ diff --git a/apps/snaky/README.md b/apps/snaky/README.md new file mode 100644 index 000000000..03ef3be2b --- /dev/null +++ b/apps/snaky/README.md @@ -0,0 +1,7 @@ +# Snaky + +Eat apples and don't bite your tail. + +## Controls +Use the touch screen, drag up, down, right or left. + diff --git a/apps/snaky/snaky-icon.js b/apps/snaky/snaky-icon.js new file mode 100644 index 000000000..85e81eadf --- /dev/null +++ b/apps/snaky/snaky-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC7u7u7u7AAAJmZmZmZmZmZkAAAAAAAAAu7u7u7u7uwAJmZmZmZmZmZkAAAAAAAALuyu7u7u7uwAJmZmZmZmZmZkAAAAAAAALIiIru7IiKwAJmZmZmZmZmZkAAAAAAAALISIru7IiK7AJmZmZmZmZmZkAAAAAAAALsiK7u7IjK7AJmZmZmZmZmZkAAAAAAAALu7u7u7u7u7AJmZmZmZmZmZkAAAAAAAALu7u7u7u7u7AAAAAAAJmZmZkAAAAAAAALsru7u7siu7AAAAAAAAmZmZkAAAAAAAALuxEiIiIruwAAAAAAAJmZmZkAAAAAAAAAu7IiIiu7uwAAAAAAAJmZmZkAAAAAAAAAu7u7u7u7sAAAAAAAAJmZmZkAAAAAAAAAALuqqqqwAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAAmZmZkAAAAAAAAAAAmZmZmQAAAAAAAAAJmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAmZmZmZmZmZmZmZmZmZmZkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")) diff --git a/apps/snaky/snaky.js b/apps/snaky/snaky.js new file mode 100644 index 000000000..7ce032fcf --- /dev/null +++ b/apps/snaky/snaky.js @@ -0,0 +1,185 @@ +//Bangle.setLCDMode("176x176"); +Bangle.setLCDTimeout(0); + +const H = g.getWidth(); +const W = g.getHeight(); +let running = true; +let score = 0; +let d; +const gridSize = 29; +const tileSize = 6; +let nextX = 0; +let nextY = 0; +const defaultTailSize = 3; +let tailSize = defaultTailSize; +const snakeTrail = []; +const snake = { x: 10, y: 10 }; +const apple = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; + +function drawBackground(){ + g.setColor("#000000"); + g.fillRect(0, 0, H, W); +} + +function drawBackgroundSuccess(){ + g.setColor("#00FFFF"); + g.fillRect(0, 0, H, W); +} + +function drawApple(){ + g.setColor("#FF0000"); + g.fillCircle((apple.x * tileSize) + tileSize/2, (apple.y * tileSize) + tileSize/2, tileSize/2); +} + +function drawSnake(){ + g.setColor("#008000"); + for (let i = 0; i < snakeTrail.length; i++) { + g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize); + + //snake bites it's tail + if (snakeTrail[i].x === snake.x && snakeTrail[i].y === snake.y && tailSize > defaultTailSize) { + Bangle.buzz(1000); + gameOver(); + } + } + g.setColor("#FFFFFF"); + g.fillRect(snake.x*tileSize, snake.y*tileSize, snake.x*tileSize+ tileSize, snake.y*tileSize + tileSize); + + g.setColor("#0000ff"); + g.fillRect((snake.x*tileSize)+1, (snake.y*tileSize)+2, (snake.x*tileSize)+2, (snake.y*tileSize)+4); + + g.setColor("#0000ff"); + g.fillRect((snake.x*tileSize)+tileSize-1, (snake.y*tileSize)+2, (snake.x*tileSize)+tileSize-2, (snake.y*tileSize)+4); + +} + +function drawScore(){ + g.setColor("#555555"); + g.setFont("Vector20"); + g.setFontAlign(0, 0); + g.drawString("Score:" + score, W / 2, 10); +} + +function gameStart() { + running = true; + score = 0; +} + +function gameOver() { + g.clear(); + g.setColor("#000000"); + g.setFont("Vector12"); + g.drawString("GAME OVER!", W / 2, H / 2 - 20); + g.drawString("Score: " + score, W / 2, H / 2 - 10); + g.drawString("Tap to Restart", W / 2, H / 2 + 10); + running = false; + tailSize = defaultTailSize; +} + +function draw() { + if (!running) { + return; + } + + g.clear(); + + // move snake in next pos + snake.x += nextX; + snake.y += nextY; + + // snake over game world + if (snake.x < 0) { + snake.x = gridSize - 1; + } + if (snake.x > gridSize - 1) { + snake.x = 0; + } + + if (snake.y < 0) { + snake.y = gridSize - 1; + } + if (snake.y > gridSize - 1) { + snake.y = 0; + } + + //snake bite apple + if (snake.x === apple.x && snake.y === apple.y) { + Bangle.beep(20); + drawBackgroundSuccess(); + tailSize++; + score++; + + apple.x = Math.floor(Math.random() * gridSize); + apple.y = Math.floor(Math.random() * gridSize); + drawApple(); + } + + drawBackground(); + drawApple(); + drawSnake(); + drawScore(); + + //set snake trail + snakeTrail.push({ x: snake.x, y: snake.y }); + while (snakeTrail.length > tailSize) { + snakeTrail.shift(); + } + + g.flip(); +} + +let dDiff = 10; + +Bangle.on('drag', function(a) { + + if (a.dx > dDiff ) { // right + if (d !== 'l') + { + nextX = 1; + nextY = 0; + d = 'r'; + } + } + + if (a.dx < -dDiff ) { // left + if (d !== 'r') + { + nextX = -1; + nextY = 0; + d = 'l'; + } + } + + if (a.dy < -dDiff) { // Up + if (d !== 'd') { + nextX = 0; + nextY = -1; + d = 'u'; + } + } + + + if (a.dy > dDiff) { // Down + if (d !== 'u') + { + nextX = 0; + nextY = 1; + d = 'd'; + } + } + +}); + + + + +Bangle.on('touch', button => { + if (!running) { + gameStart(); + } +}); + + +// render X times per second +const x = 5; +setInterval(draw, 1000 / x); diff --git a/apps/snaky/snaky.png b/apps/snaky/snaky.png new file mode 100644 index 000000000..388c32126 Binary files /dev/null and b/apps/snaky/snaky.png differ diff --git a/apps/snek/ChangeLog b/apps/snek/ChangeLog new file mode 100644 index 000000000..7c93db451 --- /dev/null +++ b/apps/snek/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Fixed snek.png and snek.icon.js to 64x64 to display in launcher, added screenshots, updated apps.json diff --git a/apps/snek/screenshot_snek.png b/apps/snek/screenshot_snek.png new file mode 100644 index 000000000..01a15bb27 Binary files /dev/null and b/apps/snek/screenshot_snek.png differ diff --git a/apps/snek/screenshot_snek2.png b/apps/snek/screenshot_snek2.png new file mode 100644 index 000000000..d2cb938bc Binary files /dev/null and b/apps/snek/screenshot_snek2.png differ diff --git a/apps/snek/snek.icon.js b/apps/snek/snek.icon.js new file mode 100644 index 000000000..b820ffcf7 --- /dev/null +++ b/apps/snek/snek.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("oFA4X/AAOJksvr2rmokYgWqB7sq/2AB5krgYPMgW8ioPc1X9i/oLplVqv+1BdK1OV//q9QPMv4PL1eqy/q1SRK3tVu+AgWCFxP96t+Vhn9qoPLgWr/+//wFBSBEq3/qlW+JwJ/I3eXDQIOBB5OrB5sC3xMD1WAH4+r6xsOtSpKLoYPN1fV1bpKTYf+RJAeDytXFxoPOdQYPNPpkCy1VtQPc6wvO62Vu+CbhfVN4P//+q//uMgwPH9QPH3tqqtpqoABv4wHfoOpBoP/6tVUg7uBFwIvB3xlIB4v+OpJsC1WA1fVQpiGCB52+uzlMB58A31XB5sqy4PNlYPfH50rywPN3++BxgPPgW9V5kCZ4L/HBwmq/tX1APM/4PMBwNVvxuKgW/tP/HxUq1X+1eqFxQPRAAKsLB4KqNAFY=")) diff --git a/apps/snek/snek.js b/apps/snek/snek.js new file mode 100644 index 000000000..4c3aec73e --- /dev/null +++ b/apps/snek/snek.js @@ -0,0 +1,469 @@ +function init() { + this.titleScreen = true; + this.min = 0; + this.max = 160; + this.step = 20; + this.scoreMultiplier = 25; + this.totalGrid = this.max / this.step; + + if (g.theme.dark) { + this.textColor = 1; + } else { + this.textColor = 0; + } + + this.getNewPosistion = () => { + let newPos; + while (!newPos) { + // random bonus points for bad luck / lag + if (currentPosition.length > 10) { + this.score += 1; + } + const x = Math.floor(Math.random() * this.totalGrid + 1) * this.step; + const y = Math.floor(Math.random() * this.totalGrid + 1) * this.step; + const found = currentPosition.find(pos => { + return pos.x === x && pos.y === y; + }); + if (!found) { + newPos = {x: x, y: y}; + } + } + return newPos; + }; + + this.restart = () => { + g.clear(); + this.titleScreen = false; + this.score = 0; + this.paused = false; + this.currentPosition = [{x: 2 * step, y: 3 * step},{x: 1 * step, y: 3 * step}]; + this.death = false; + this.gameSpeed = 200; + this.directionSet = null; + this.direction = 1; + this.createApple(); + }; + + const game = () => { + if (this.death && !this.paused) { + g.clear(); + this.showDeathScreen(); + } else if (this.titleScreen && !this.paused) { + this.showTitleScreen(); + } else if (!this.paused) { + g.clear(); + this.drawApple(); + this.drawSnake(); + this.boundries(); + + } + + setTimeout(() => { + game(); + }, this.gameSpeed); + }; + + this.increaseDifficulity = () => { + if (gameSpeed > 59) { + gameSpeed -= 10; + } + }; + + this.createApple = () => { + this.applePosition = getNewPosistion(); + }; + + this.drawApple = () => { + g.setColor(0, 1, 0); + + g.drawImage(this.appleLeaf, this.applePosition.x - 15, this.applePosition.y - 10); + g.setColor(1, 0, 0); + + g.drawImage(this.apple, this.applePosition.x - 15, this.applePosition.y - 2); + }; + + this.checkmax = (x) => { + if (x > this.max) { + return this.min; + } else if (x < this.min) { + return this.max; + } + return x; + }; + + this.movement = (lastItem) => { + let newPosition; + switch(this.direction) { + case 3: + newPosition = { + x: checkmax(lastItem.x + this.step), + y: lastItem.y + }; + break; + case 1: + newPosition = { + x: checkmax(lastItem.x - this.step), + y: lastItem.y + }; + break; + case 2: + newPosition = { + x: lastItem.x, + y: checkmax(lastItem.y + this.step) + }; + break; + case 0: + newPosition = { + x: lastItem.x, + y: checkmax(lastItem.y - this.step) + }; + break; + } + this.directionSet = false; + this.checkDeath(newPosition); + this.currentPosition.push(newPosition); + + }; + + this.snakeHead = (props) => { + switch (this.direction) { + case 0: + return [this.snakeUp, props.x - 9, props.y - 12]; + case 1: + return [this.snakeLeft, props.x - 20, props.y - 10]; + case 3: + return [this.snakeRight, props.x - 12, props.y - 12]; + case 2: + return [this.snakeDown, props.x - 12, props.y - 7]; + default: + return [this.snakeDown, props.x - 12, props.y - 7]; + } + }; + + this.drawSnake = () => { + const totalItems = this.currentPosition.length - 1; + g.setColor(0, 1, 0); + this.movement(this.currentPosition[totalItems]); + this.currentPosition.forEach((props, index) => { + if (index-1 === totalItems) { + const head = this.snakeHead(props); + + g.drawImage(head[0], head[1], head[2]); + } else { + g.fillCircle(props.x, props.y, 10); + } + }); + if (this.currentPosition[totalItems].x === this.applePosition.x && this.currentPosition[totalItems].y === this.applePosition.y) { + this.createApple(); + this.increaseDifficulity(); + } else { + this.currentPosition.shift(); + } + }; + + this.checkDeath = (newPos) => { + + const found = this.currentPosition.find((oldPos) => { + return newPos.x === oldPos.x && newPos.y === oldPos.y; + }); + if (found) { + Bangle.buzz(); + g.clear(); + this.death = true; + } + }; + + this.boundries = () => { + if (this.currentPosition.x >= this.maxPx) { + this.currentPosition.x = this.maxPx; + } + else if (this.currentPosition.x < 10) { + this.currentPosition.x = 10; + } + + if ( this.currentPosition.y >= this.maxPy) { + this.currentPosition.y = this.maxPy; + } else if (this.currentPosition.y < 10) { + this.currentPosition.y = 10; + } + }; + + this.creatTopScrore = () => { + require("Storage").writeJSON("snek_jd", { + topScore: this.calculateScore() + }); + }; + + this.calculateScore = () => { + return currentPosition.length * this.scoreMultiplier + this.score; + }; + + this.showDeathScreen = () => { + this.paused = true; + g.setFont('Vector', 25); + g.setColor(1, 0, 0); + g.drawString("GAME OVER",15, 50, "solid"); + g.setFont('Vector', 15); + g.setColor(this.textColor, this.textColor, this.textColor); + g.drawString("Score : " + this.calculateScore(), 50, 78, "solid"); + + let storage = require("Storage").readJSON("snek_jd"); + if (storage && storage.topScore) { + if (storage.topScore < this.calculateScore()) { + g.setColor(0, 1, 1); + g.drawString("New top score!", 20, 95, "solid"); + g.setFont('Vector', 22); + g.drawString(this.calculateScore(), 20, 115, "solid"); + + this.creatTopScrore(); + } else { + g.setColor(this.textColor, this.textColor, this.textColor); + g.drawString("Top score : " + storage.topScore, 20, 95, "solid"); + } + } else { + this.creatTopScrore(); + } + g.setFont('Vector', 25); + }; + + /* Events */ + Bangle.on('tap', (data) => { + Bangle.setLCDPower(true); + if (this.death) { + this.showTitleScreen(); + } else if (this.titleScreen || this.paused) { + this.restart(); + } + }); + + Bangle.on('accel', (xyz) => { + if (Math.abs(xyz.x) > Math.abs(xyz.y)) { + if (xyz.x < 0) { + if (!this.directionSet && this.direction !== 1) { + Bangle.setLCDPower(true); + this.direction = 3; + } + } else { + if (!this.directionSet && this.direction !== 3) { + Bangle.setLCDPower(true); + this.direction = 1; + } + } + } else { + if (xyz.y < 0) { + if (!this.directionSet && this.direction !== 0) { + Bangle.setLCDPower(true); + this.direction = 2; + } + } else { + if (!this.directionSet && this.direction !== 2) { + Bangle.setLCDPower(true); + this.direction = 0; + } + } + } + this.directionSet = true; + }); + + this.showTitleScreen = () => { + this.death = false; + g.clear(); + g.setColor(0, 1, 0); + g.setFont('Vector', 50); + g.drawString("nek", 70, 15, "solid"); + g.drawImage(this.titleScreenImg, 20, 20); + g.fillPoly([ + 15, 66, + 152, 70, + 159, 79, + 21, 71 ]); + g.setColor(this.textColor, this.textColor, this.textColor); + g.setFont('Vector', 15); + g.drawString("Tilt to turn", 20, 100, "solid"); + g.drawString("Tap to start", 20, 120, "solid"); + + g.setColor(0, 1, 0); + + g.setFont('4x6', 3); + g.drawString("Jason de Belle", 5, 145, "solid"); + + + + }; + +/* Graphics */ + this.snakeUp = Graphics.createImage(` + XX XX + xx xx + xx xx + xx + xx + xx + xx + xx + xxxxxxxx + xxxx xxxx + xxxxxx xxxxxxx + xxxxxxxxxxxxxxxx + xxxxx xXXx xxxxx + xxxxx XX xxxxx + xxxxx XX xxxxx + xxxxxx xxxx xxxxx + xxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxx + `); + this.snakeDown = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxx xxxx xxxxxx + xxxxxx xxxx xxxxxx + xxxx XX xxxxx + xxxx XX xxxxx + xxxx xXXx xxxx + xxxx xXXx xxxx + xXxxxxxxxxxxxx + xXxxxxxxxxxxxx + xxx xxx + xxxx xxxx + xxxx + xx + xx + xx + xx + x x + xx xx + xx xx + `); + + this.snakeRight = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xXxxxxxx xxxx + xXxxxxxx xxxx + xxxxxxxX xxx + xxxxxxxX xxx xxxx +xxxxxxxxxxXX xxxxxx xxxx +xxxxxxxxxxXX xxxxxx xx +xxxxxxxxxxXXxxxxx xxxxxxxx +xxxxxxxxxxXXxxxxx xxxxxxxx +xxxxxxxxxxxx xxxxx xx +xxxxxxxxxxxx xxxxx xxx + xxxxxxxx xx xxxx + xxxxxxxx xx + xxxxxxxx xxx + xxxxxxxx xxx + xxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxxxx + `); + this.snakeLeft = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xXxxxxxxxxxxxx + xX xxxxxxxxxx + x xx xXxxxxxxxx + xx xx xXxxxxxxxx + xx xx xxxx xxXXxxxxxxxx + xxxxxxx xxxxxXXxxxxxxxx + xxxxxxx xxxxxXXxxxxxxxx + xx xx xxxx xxXXxxxxxxxx + xx xxx xxxxxxxxxx + x xxx xxxxxxxxx + xxxx xxxxxxxxx + xxxx xxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxx + xxxxxxxxx + `); + + this.apple = Graphics.createImage(` + xxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxx + `); + + this.appleLeaf = Graphics.createImage(` + xxxxxx + xxxxxx + xxxx + XXxxxxxxxx + xx + xx + xx + xx + xx + `); + + +this.titleScreenImg = Graphics.createImage(` + sxxxxxxxs + xxsxxx xxxxxs + xxxxxxxxsxx xxxxsx + xxxxxxxxxxxxxxxsxxs xxxxsxx + xxxxxxxxxxxxxxxxxxxxxsxxxsssxxxxxxsxxxx + xxxxxxxxxxxxxxxxxxxsxxxxsxxs xxsxxx + xxxxxxxxxxxxxxxxxxxxxxxxxsxx xxxsxx + xxxxxxxxxxxxxxxxxxxxx sxxx ssxxsx + xxxxxxxxxxxxxxx xxxxxxs + xxxxxxxxxxxx ssss + xxxxxxxxxxxxx +xxxxxxxxxxxx +xxxxxxxxxxx +xxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxxxx + xxxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxxxx + xxxxxxxxxx +xxxxxxx +xxx +`); + + game(); +} +init(); \ No newline at end of file diff --git a/apps/snek/snek.png b/apps/snek/snek.png new file mode 100644 index 000000000..6e31cfcbb Binary files /dev/null and b/apps/snek/snek.png differ diff --git a/apps/thermom/ChangeLog b/apps/thermom/ChangeLog index 6ab6ba8e5..6183ac17b 100644 --- a/apps/thermom/ChangeLog +++ b/apps/thermom/ChangeLog @@ -1,2 +1,3 @@ 0.02: New App! 0.03: Improved messages and added Celsius sign +0.04: Make temperature value readable on smaller screens diff --git a/apps/thermom/app.js b/apps/thermom/app.js index 7eae9b3d4..145854765 100644 --- a/apps/thermom/app.js +++ b/apps/thermom/app.js @@ -4,7 +4,7 @@ function onTemperature(p) { var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; g.drawString("Temperature:", x, y - 45); - g.setFontVector(70).setFontAlign(0,0); + g.setFontVector(g.getWidth() > 200 ? 70 : 40).setFontAlign(0,0); g.drawString(p.temperature.toFixed(1) + " °C", x, y); } diff --git a/apps/vectorclock/Changelog b/apps/vectorclock/Changelog index 43190331b..c2a6fbcf4 100644 --- a/apps/vectorclock/Changelog +++ b/apps/vectorclock/Changelog @@ -1,2 +1,3 @@ 0.1: New watch face 0.2: Use Bangle.setUI for button/launcher handling +0.3: Bangle.js 2 support \ No newline at end of file diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js index a98c9f97b..78c17b3d4 100644 --- a/apps/vectorclock/app.js +++ b/apps/vectorclock/app.js @@ -5,9 +5,10 @@ function padNum(n, l) { return ("0".repeat(l)+n).substr(-l); } -let rects = {}; -let rectsToClear = {}; -let commands = []; +var rects = {}; +var rectsToClear = {}; +var commands = []; +var showSeconds = true; function pushCommand(command) { let hash = E.CRC32(E.toJS(arguments)); @@ -20,17 +21,20 @@ function executeCommands() { "ram"; for (let hash in rectsToClear) delete rects[hash]; for (let r of rectsToClear) if (r) g.clearRect(r.x1, r.y1, r.x2, r.y2); - g.getModified(true); - for (let c of commands) { - c.command(); - rects[c.hash] = g.getModified(true); - } + for (let c of commands) rects[c.hash] = c.command(); rectsToClear = Object.assign({}, rects); commands = []; } function drawVectorText(text, size, x, y, alignX, alignY) { g.setFont("Vector", size).setFontAlign(alignX, alignY).drawString(text, x, y); + var m = g.stringMetrics(text); + return { + x1: x - m.width * (alignX / 2 + 0.5), + y1: y - m.height * (alignY / 2 + 0.5), + x2: x - m.width * (alignX / 2 - 0.5), + y2: y - m.height * (alignY / 2 - 0.5) + }; } function draw() { @@ -43,11 +47,12 @@ function draw() { let secondsText = padNum(d.getSeconds(), 2); let dowText = locale.dow(d); let dateText = locale.date(d, true); + let width = g.getWidth() - 2; g.setFont("Vector", 256); - let timeFontSize = g.getWidth() / ((g.stringWidth(timeText) / 256) + (Math.max(g.stringWidth(meridian), g.stringWidth(secondsText)) / 512 * 9 / 10)); - let dowFontSize = g.getWidth() / (g.stringWidth(dowText) / 256); - let dateFontSize = g.getWidth() / (g.stringWidth(dateText) / 256); + let timeFontSize = width / ((g.stringWidth(timeText) / 256) + (Math.max(g.stringWidth(meridian), g.stringWidth(secondsText)) / 512 * 9 / 10)); + let dowFontSize = width / (g.stringWidth(dowText) / 256); + let dateFontSize = width / (g.stringWidth(dateText) / 256); let timeHeight = g.setFont("Vector", timeFontSize).getFontHeight() * 9 / 10; let dowHeight = g.setFont("Vector", dowFontSize).getFontHeight(); @@ -56,26 +61,28 @@ function draw() { let remainingHeight = g.getHeight() - 24 - timeHeight - dowHeight - dateHeight; let spacer = remainingHeight / 4; + let x = 2; let y = 24 + spacer; - pushCommand(drawVectorText, timeText, timeFontSize, 0, y, -1, -1); - pushCommand(drawVectorText, meridian, timeFontSize*9/20, g.getWidth(), y, 1, -1); - pushCommand(drawVectorText, secondsText, timeFontSize*9/20, g.getWidth(), y + timeHeight, 1, 1); + pushCommand(drawVectorText, timeText, timeFontSize, x, y, -1, -1); + pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1); + if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1); y += timeHeight + spacer; - pushCommand(drawVectorText, dowText, dowFontSize, g.getWidth()/2, y, 0, -1); + pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1); y += dowHeight + spacer; - pushCommand(drawVectorText, dateText, dateFontSize, g.getWidth()/2, y, 0, -1); + pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1); executeCommands(); } -let timeout; +var timeout; function tick() { draw(); - timeout = setTimeout(tick, 1000 - getTime() % 1 * 1000); + var period = showSeconds ? 1000 : 60 * 1000; + timeout = setTimeout(tick, period - getTime() * 1000 % period); } Bangle.on('lcdPower', function(on) { @@ -84,6 +91,13 @@ Bangle.on('lcdPower', function(on) { if (on) tick(); }); +Bangle.on('lock', function(locked) { + if (timeout) clearTimeout(timeout); + timeout = null; + showSeconds = !locked; + tick(); +}); + g.clear(); tick(); Bangle.loadWidgets(); diff --git a/apps/vectorclock/bangle2-vector-clock-screenshot.png b/apps/vectorclock/bangle2-vector-clock-screenshot.png new file mode 100644 index 000000000..30d40c864 Binary files /dev/null and b/apps/vectorclock/bangle2-vector-clock-screenshot.png differ diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index c1a0504a4..fb6b28bf6 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -8,3 +8,5 @@ 0.09: Fix crash when weather.json is absent. 0.10: Use new Layout library 0.11: Bangle.js 2 support +0.12: Allow hiding the widget +0.13: Tweak Bangle.js 2 light theme colors diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 76ed2aaa4..7cb9a9f9b 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -54,14 +54,62 @@ exports.get = function() { scheduleExpiry(storage.readJSON('weather.json')||{}); exports.drawIcon = function(cond, x, y, r) { + var palette; + + if (B2) { + if (g.theme.dark) { + palette = { + sun: '#FF0', + cloud: '#FFF', + bgCloud: '#777', // dithers on B2, but that's ok + rain: '#0FF', + lightning: '#FF0', + snow: '#FFF', + mist: '#FFF' + }; + } else { + palette = { + sun: '#FF0', + cloud: '#777', // dithers on B2, but that's ok + bgCloud: '#000', + rain: '#00F', + lightning: '#FF0', + snow: '#0FF', + mist: '#0FF' + }; + } + } else { + if (g.theme.dark) { + palette = { + sun: '#FE0', + cloud: '#BBB', + bgCloud: '#777', + rain: '#0CF', + lightning: '#FE0', + snow: '#FFF', + mist: '#FFF' + }; + } else { + palette = { + sun: '#FC0', + cloud: '#000', + bgCloud: '#777', + rain: '#07F', + lightning: '#FC0', + snow: '#CCC', + mist: '#CCC' + }; + } + } + function drawSun(x, y, r) { - g.setColor(B2 ? '#FF0' : (g.theme.dark ? "#FE0" : "#FC0")); + g.setColor(palette.sun); g.fillCircle(x, y, r); } function drawCloud(x, y, r, c) { const u = r/12; - if (c==null) c = B2 ? '#FFF': (g.theme.dark ? "#BBB" : "#AAA"); + if (c==null) c = palette.cloud; g.setColor(c); g.fillCircle(x-8*u, y+3*u, 4*u); g.fillCircle(x-4*u, y-2*u, 5*u); @@ -78,7 +126,7 @@ exports.drawIcon = function(cond, x, y, r) { } function drawBrokenClouds(x, y, r) { - drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777"); // dithers on B2, but that's ok + drawCloud(x+1/8*r, y-1/8*r, 7/8*r, palette.bgCloud); drawCloud(x-1/8*r, y+1/8*r, 7/8*r); } @@ -88,7 +136,7 @@ exports.drawIcon = function(cond, x, y, r) { } function drawRainLines(x, y, r) { - g.setColor(B2 ? '#0FF' : (g.theme.dark ? "#0CF" : "#07F")); + g.setColor(palette.rain); const y1 = y+1/2*r; const y2 = y+1*r; const poly = g.fillPolyAA ? p => g.fillPolyAA(p) : p => g.fillPoly(p); @@ -124,7 +172,7 @@ exports.drawIcon = function(cond, x, y, r) { function drawThunderstorm(x, y, r) { function drawLightning(x, y, r) { - g.setColor(B2 ? '#FF0' : (g.theme.dark ? "#FE0" : "#FC0")); + g.setColor(palette.lightning); g.fillPoly([ x-2/6*r, y-r, x-4/6*r, y+1/6*r, @@ -152,7 +200,7 @@ exports.drawIcon = function(cond, x, y, r) { } } - g.setColor(B2 ? '#FFF' : (g.theme.dark ? "#FFF" : "#CCC")); + g.setColor(palette.snow); const w = 1/12*r; for(let i = 0; i<=6; ++i) { const points = [ @@ -187,7 +235,7 @@ exports.drawIcon = function(cond, x, y, r) { [-0.2, 0.3], ]; - g.setColor(B2 ? '#FFF' : (g.theme.dark ? "#FFF" : "#CCC")); + g.setColor(palette.mist); for(let i = 0; i<5; ++i) { g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r, y+(0.4*i-0.7)*r-1); @@ -197,7 +245,7 @@ exports.drawIcon = function(cond, x, y, r) { } function drawUnknown(x, y, r) { - drawCloud(x, y, r, "#777"); // dithers on B2, but that's ok + drawCloud(x, y, r, palette.bgCloud); g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); } diff --git a/apps/weather/readme.md b/apps/weather/readme.md index e2fb886b7..6d0ea04a5 100644 --- a/apps/weather/readme.md +++ b/apps/weather/readme.md @@ -11,6 +11,12 @@ You can view the full report through the app: 1. Install [Gadgetbridge for Android](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) on your phone. 2. Set up [Gadgetbridge weather reporting](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather). +## Settings + +* Expiration timespan can be set after which the local weather data is considered as invalid +* Widget can be hidden + ## Controls -BTN2: opens the launcher +BTN2: opens the launcher (Bangle.js 1) +BTN: opens the launcher (Bangle.js 2) diff --git a/apps/weather/settings.js b/apps/weather/settings.js index 1cc097e3a..7e2c043b9 100644 --- a/apps/weather/settings.js +++ b/apps/weather/settings.js @@ -19,6 +19,14 @@ }, onchange: x => save('expiry', x), }, + 'Hide Widget': { + value: "hide" in settings ? settings.hide : false, + format: () => (settings.hide ? 'Yes' : 'No'), + onchange: () => { + settings.hide = !settings.hide + save('hide', settings.hide); + }, + }, '< Back': back, }); }) diff --git a/apps/weather/widget.js b/apps/weather/widget.js index 4871ceda4..f2ddf0b5b 100644 --- a/apps/weather/widget.js +++ b/apps/weather/widget.js @@ -1,9 +1,25 @@ (() => { const weather = require('weather'); - + var dirty = false; + + let settings; + + function loadSettings() { + settings = require('Storage').readJSON('weather.json', 1) || {}; + } + + function setting(key) { + if (!settings) { loadSettings(); } + const DEFAULTS = { + 'expiry': 2*3600000, + 'hide': false + }; + return (key in settings) ? settings[key] : DEFAULTS[key]; + } weather.on("update", w => { + if (setting('hide')) return; if (w) { if (!WIDGETS["weather"].width) { WIDGETS["weather"].width = 20; @@ -21,7 +37,7 @@ }); Bangle.on('lcdPower', on => { - if (on && dirty) { + if (on && dirty && !setting('hide')) { WIDGETS["weather"].draw(); dirty = false; } @@ -29,8 +45,9 @@ WIDGETS["weather"] = { area: "tl", - width: weather.get() ? 20 : 0, + width: weather.get() && !setting('hide') ? 20 : 0, draw: function() { + if (setting('hide')) return; const w = weather.get(); if (!w) return; g.reset(); @@ -47,5 +64,9 @@ g.drawString(t, this.x+10, this.y+24); } }, + reload:function() { + loadSettings(); + WIDGETS["weather"].redraw(); + }, }; })(); diff --git a/apps/weatherClock/ChangeLog b/apps/weatherClock/ChangeLog new file mode 100644 index 000000000..72b53ebbf --- /dev/null +++ b/apps/weatherClock/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Minor layout format tweak so it uses less memory and draws ok on Bangle.js 1 (#1012) +0.03: Minor layout extra spaces. +0.04: Layout now compatible with Bangle.js 2 diff --git a/apps/weatherClock/README.md b/apps/weatherClock/README.md new file mode 100644 index 000000000..f1f146440 --- /dev/null +++ b/apps/weatherClock/README.md @@ -0,0 +1,19 @@ +# Weather Clock + +A clock which displays the current weather conditions. Temperature, wind speed, and an icon indicating the weather conditions are displayed. + +Standard widgets are displayed. + +## Requirements + +**This clock requires Gadgetbridge and the weather app in order to get weather data!** + +See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather. + +![Screenshot](screens/screen1.png) + +![Screenshot2](screens/screen2.png) + +## Creator + +James Gough diff --git a/apps/weatherClock/app-icon.js b/apps/weatherClock/app-icon.js new file mode 100644 index 000000000..e289f6c8b --- /dev/null +++ b/apps/weatherClock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/ADEzAgk/AogACn/zAgYLBmMACAQEBgMvAwUQgAABj8wAgUD//yAgIQFBQIXFl4XFmETC4QcBn8xgQXDGgYACmAXBmQMBC4UB//zAQJoBC4JmE/8gj4kDC48Qn8QJAIxEBIIXFCgM/+IXELIIGBBwYcEAYcBCARHCiczNIINB+JrDC4inBFAjKCLQZ2CC4zRCHYgXFkBkFKAUSPQYABkJEBVQZ2DmUigEikfzgESiA7BkU/mEBkJfCOwjdFL4QnBEwL8CHIJ2F+ciAAI6CmUjAYRmCAwQSBbgoA/AG3zXoMvkMv+Uj+U/mcxBYP/kciTQMimKRCBYPyj/xXIMydYMvmKqBUYIWBkcyC4bmBmMT+P/C4Uzn/xEYXziYXBkYnBbAczmfyC4MT+bWBkMiiQKBiUyAoMhj6RzMgYASn3dmazIAYYmG/ve93dC48iCYMzAYQXF93u9oXIl4uBAQIAFCwIAB74XKkYvGIwIXJYQIDCC43+I4QKFAoh6Cn4IEO4RfF+QtDGQcTMQn97oABC4si+cRkcikIUBAQQAE6a8GiXzkUykUiZgMjj7gNEwMzXoLWCL4oANCSQAl")) \ No newline at end of file diff --git a/apps/weatherClock/app.js b/apps/weatherClock/app.js new file mode 100644 index 000000000..799f9abc6 --- /dev/null +++ b/apps/weatherClock/app.js @@ -0,0 +1,128 @@ +const Layout = require("Layout"); +const storage = require('Storage'); +const locale = require("locale"); + +// weather icons from https://icons8.com/icon/set/weather/color +var sunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQC6vd7ouVC4IwUCwIwUFwQwQCYgAHDZQXc9wACC6QWDDAgXN7wXF9oXPCwowDC5guGGAYXMCw4wCC5RGJJAZGTJBiNISIylQVJrLCC5owGF65fXR7AwBC5jvhC7JIILxapDFxAXOGAy9KC4owGBAQXODAgHDC54AHC8T0FAAQSOGg4qPGA4WUGAIuVC7AA/AH4AEA=")); + +var partSunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AY6AWVhvdC6vd7owUFwIABFiYAFGR4Xa93u9oXTCwIYDC6HeC4fuC56MBC4ySOIwpIQXYQXHmYABRpwXECwQYKF5HjC4kwL5gQCAYYwO7wqFAAowK7wWKJBgXLJBPd6YX/AAoVMAAM/Cw0DC5yRHCx5JGFyAwGCyIwFC/4XyR4inXa64wRFwowQCw4A/AH4AkA")); + +var cloudIcon = require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4AtgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAH4A/AH4A/ADg=")); + +var snowIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AhxGAC9YUBC4QZRhAVBAIWIC6QAEI6IYEI5cIBgwWOC64NCKohHPNox3RBgqnQEo7XPHpKONR5AXYAH4ASLa4XWXILiBC6r5LDBgWWDBRrKC5hsCEacIHawvMCIwvQC5QvQFAROEfZ5ADLJ4YGCywvVI7CPGC9IA/AH4AF")); + +var rainIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AFgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAGeIBJEIwAVJhGIC5AJBC5QMJEJQMEC44JBC6QSCC54FHLxgNBBgYSEDgKpPMhQXneSwuUAH4A/AA4=")); + +var stormIcon = require("heatshrink").decompress(atob("mEwwhC/AFEzmcwCyoYUgYXDmYuVGAY0OFwocHC6pNLCxYXYJBQXuCxhhJRpgYKCyBKFFyIXFCyJIFC/4XaO66nU3eza6k7C4IWFGBwXBCwwwO3ewC5AZMC6RaCIxZiI3e7AYYwRCQIIBC4QwPIQIpDC5owDhYREIxgAEFIouNC4orDFyBGBGAcLC6BaFhYWRLSRIFISQXcCyqhRAH4Az")); + +// err icon - https://icons8.com/icons/set/error +var errIcon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4AZUAIWUiAXBWqgXXdIYuVGCgXBgICCIyYXCJCQTDC6QrEMCQSEJCQRFC6ApGJCCiDDQSpQFAYXEJBqNGJCA/EC4ZIOEwgXFJBgNEAhKlNAgxIKBgoXEJBjsLC5TsIeRycMBhRrMMBKzQEozjOBxAgHGww+IA6wfSH4hnIC47OMSJqlRIJAXCACIXaGoQARPwwuTAH4A/ABw")); + +/** +Choose weather icon to display based on condition. +Based on function from the Bangle weather app so it should handle all of the conditions +sent from gadget bridge. +*/ +function chooseIcon(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm")) return stormIcon; + if (condition.includes("freezing")||condition.includes("snow")|| + condition.includes("sleet")) { + return snowIcon; + } + if (condition.includes("drizzle")|| + condition.includes("shower")) { + return rainIcon; + } + if (condition.includes("rain")) return rainIcon; + if (condition.includes("clear")) return sunIcon; + if (condition.includes("few clouds")) return partSunIcon; + if (condition.includes("scattered clouds")) return cloudIcon; + if (condition.includes("clouds")) return cloudIcon; + 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")) { + return cloudIcon; + } + return cloudIcon; +} + +/** +Get weather stored in json file by weather app. +*/ +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} + +var clockLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"35%", halign: 0, fillx:1, pad: 8, label:"00:00", id:"time" }, + {type: "h", fillx: 1, c: [ + {type:"txt", font:"10%", label:"THU", id:"dow" }, + {type:"txt", font:"10%", label:"01/01/1970", id:"date" } + ] + }, + {type: "h", valign : 1, fillx:1, c: [ + {type: "img", filly: 1, id: "weatherIcon", src: sunIcon}, + {type: "v", fillx:1, c: [ + {type: "h", c: [ + {type: "txt", font: "10%", id: "temp", label: "000 °C"}, + ]}, + {type: "h", c: [ + {type: "txt", font: "10%", id: "wind", label: "00 km/h"}, + ]} + ] + }, + ]}] +}); + +// 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)); +} + +function draw() { + var date = new Date(); + clockLayout.time.label = locale.time(date, 1); + clockLayout.date.label = locale.date(date, 1).toUpperCase(); + clockLayout.dow.label = locale.dow(date, 1).toUpperCase() + " "; + var weatherJson = getWeather(); + if(weatherJson && weatherJson.weather){ + var currentWeather = weatherJson.weather; + const temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/); + clockLayout.temp.label = temp[1] + " " + temp[2]; + clockLayout.weatherIcon.src = chooseIcon(currentWeather.txt); + const wind = locale.speed(currentWeather.wind).match(/^(\D*\d*)(.*)$/); + clockLayout.wind.label = wind[1] + " " + wind[2] + " " + (currentWeather.wrose||'').toUpperCase(); + } + else{ + clockLayout.temp.label = "Err"; + clockLayout.wind.label = "No Data"; + clockLayout.weatherIcon.src = errIcon; + } + clockLayout.clear(); + clockLayout.render(); + // queue draw in one minute + queueDraw(); +} + +g.clear(); +Bangle.setUI("clock"); // Show launcher when middle button pressed +Bangle.loadWidgets(); +Bangle.drawWidgets(); +clockLayout.render(); +draw(); diff --git a/apps/weatherClock/app.png b/apps/weatherClock/app.png new file mode 100644 index 000000000..434811541 Binary files /dev/null and b/apps/weatherClock/app.png differ diff --git a/apps/weatherClock/icons/icons8-cloud-lightning-48.png b/apps/weatherClock/icons/icons8-cloud-lightning-48.png new file mode 100644 index 000000000..7ae0cd4a9 Binary files /dev/null and b/apps/weatherClock/icons/icons8-cloud-lightning-48.png differ diff --git a/apps/weatherClock/icons/icons8-clouds-48.png b/apps/weatherClock/icons/icons8-clouds-48.png new file mode 100644 index 000000000..7b7533c51 Binary files /dev/null and b/apps/weatherClock/icons/icons8-clouds-48.png differ diff --git a/apps/weatherClock/icons/icons8-error-48.png b/apps/weatherClock/icons/icons8-error-48.png new file mode 100644 index 000000000..b45640f31 Binary files /dev/null and b/apps/weatherClock/icons/icons8-error-48.png differ diff --git a/apps/weatherClock/icons/icons8-partly-cloudy-day-48.png b/apps/weatherClock/icons/icons8-partly-cloudy-day-48.png new file mode 100644 index 000000000..e83f55148 Binary files /dev/null and b/apps/weatherClock/icons/icons8-partly-cloudy-day-48.png differ diff --git a/apps/weatherClock/icons/icons8-rain-48.png b/apps/weatherClock/icons/icons8-rain-48.png new file mode 100644 index 000000000..690efd82c Binary files /dev/null and b/apps/weatherClock/icons/icons8-rain-48.png differ diff --git a/apps/weatherClock/icons/icons8-snow-storm-48.png b/apps/weatherClock/icons/icons8-snow-storm-48.png new file mode 100644 index 000000000..e5ff7dd11 Binary files /dev/null and b/apps/weatherClock/icons/icons8-snow-storm-48.png differ diff --git a/apps/weatherClock/icons/icons8-sun-48.png b/apps/weatherClock/icons/icons8-sun-48.png new file mode 100644 index 000000000..4933d6721 Binary files /dev/null and b/apps/weatherClock/icons/icons8-sun-48.png differ diff --git a/apps/weatherClock/screens/screen1.png b/apps/weatherClock/screens/screen1.png new file mode 100644 index 000000000..bb54e4ef7 Binary files /dev/null and b/apps/weatherClock/screens/screen1.png differ diff --git a/apps/weatherClock/screens/screen2.png b/apps/weatherClock/screens/screen2.png new file mode 100644 index 000000000..21b814dc7 Binary files /dev/null and b/apps/weatherClock/screens/screen2.png differ diff --git a/apps/welcome/app-bangle2.js b/apps/welcome/app-bangle2.js index 93d1c5657..41d051148 100644 --- a/apps/welcome/app-bangle2.js +++ b/apps/welcome/app-bangle2.js @@ -244,5 +244,6 @@ setWatch(()=>{ }, BTN1, {repeat:true}); Bangle.setLCDTimeout(0); +Bangle.setLocked(0); Bangle.setLCDPower(1); move(0); diff --git a/apps/widChargingStatus/ChangeLog b/apps/widChargingStatus/ChangeLog new file mode 100644 index 000000000..d3175e1ab --- /dev/null +++ b/apps/widChargingStatus/ChangeLog @@ -0,0 +1 @@ +0.1: First release. \ No newline at end of file diff --git a/apps/widChargingStatus/widget.js b/apps/widChargingStatus/widget.js new file mode 100644 index 000000000..90f9199fa --- /dev/null +++ b/apps/widChargingStatus/widget.js @@ -0,0 +1,31 @@ +(() => { + const icon = require("heatshrink").decompress(atob("ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA")); + const iconWidth = 18; + + function draw() { + g.reset(); + if (Bangle.isCharging()) { + g.setColor("#FD0"); + g.drawImage(icon, this.x + 1, this.y + 1, { + scale: 0.6875 + }); + } + } + + WIDGETS.chargingStatus = { + area: 'tr', + width: Bangle.isCharging() ? iconWidth : 0, + draw: draw, + }; + + Bangle.on('charging', (charging) => { + if (charging) { + Bangle.buzz(); + WIDGETS.chargingStatus.width = iconWidth; + } else { + WIDGETS.chargingStatus.width = 0; + } + Bangle.drawWidgets(); // re-layout widgets + g.flip(); + }); +})(); \ No newline at end of file diff --git a/apps/widChargingStatus/widget.png b/apps/widChargingStatus/widget.png new file mode 100644 index 000000000..0097d45ef Binary files /dev/null and b/apps/widChargingStatus/widget.png differ diff --git a/apps/wid_a_battery_widget/ChangeLog b/apps/wid_a_battery_widget/ChangeLog index 9b0649c27..b04824ae8 100644 --- a/apps/wid_a_battery_widget/ChangeLog +++ b/apps/wid_a_battery_widget/ChangeLog @@ -1,2 +1,3 @@ 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 diff --git a/apps/wid_a_battery_widget/widget.js b/apps/wid_a_battery_widget/widget.js index 9fb06e320..8ab644ab3 100644 --- a/apps/wid_a_battery_widget/widget.js +++ b/apps/wid_a_battery_widget/widget.js @@ -1,9 +1,9 @@ (function(){ let COLORS = { - 'white': "#fff", - 'black': "#000", + 'white': g.theme.dark ? "#000" : "#fff", + 'black': g.theme.dark ? "#fff" : "#000", 'charging': "#08f", - 'high': "#000", + 'high': g.theme.dark ? "#fff" : "#000", 'low': "#f00", }; diff --git a/apps/widbars/ChangeLog b/apps/widbars/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/widbars/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/widbars/README.md b/apps/widbars/README.md new file mode 100644 index 000000000..c1cb73a96 --- /dev/null +++ b/apps/widbars/README.md @@ -0,0 +1,15 @@ +# Bars Widget + +A simple widget that display several measurements as vertical bars. + +![Screenshot](screenshot.png) + +## Measurements from left to right: + +- Flash storage space used (*blue/cyan*) +- Memory usage (*magenta*) +- Battery charge (*green*) \ No newline at end of file diff --git a/apps/widbars/icon.png b/apps/widbars/icon.png new file mode 100644 index 000000000..3d6fcb053 Binary files /dev/null and b/apps/widbars/icon.png differ diff --git a/apps/widbars/screenshot.png b/apps/widbars/screenshot.png new file mode 100644 index 000000000..ae85e42f5 Binary files /dev/null and b/apps/widbars/screenshot.png differ diff --git a/apps/widbars/widget.js b/apps/widbars/widget.js new file mode 100644 index 000000000..a1134f31f --- /dev/null +++ b/apps/widbars/widget.js @@ -0,0 +1,67 @@ +(() => { + const h=24, // widget height + w=3, // width of single bar + bars=3; // number of bars + + // Note: HRM/temperature are commented out (they didn't seem very useful) + // If re-adding them, also adjust `bars` + + // ==HRM start== + // // We show HRM if available, but don't turn it on + // let bpm,rst,con=10; // always ignore HRM with confidence below 10% + // function noHrm() { // last value is no longer valid + // if (rst) clearTimeout(rst); + // rst=bpm=undefined; con=10; + // WIDGETS["bars"].draw(); + // } + // Bangle.on('HRM', hrm=>{ + // if (hrm.confidence>con || hrm.confidence>=80) { + // bpm=hrm.confidence; + // con=hrm.confidence; + // WIDGETS["bars"].draw(); + // if (rst) clearTimeout(rst); + // rst = setTimeout(noHrm, 10*60*1000); // forget HRM after 10 minutes + // } + // }); + // ==HRM end== + + /** + * Draw a bar + * + * @param {int} x left + * @param {int} y top (of full bar) + * @param {string} col Color + * @param {number} f Fraction of bar to draw + */ + function bar(x,y, col,f) { + if (!f) f = 0; // for f=NaN: set it to 0 -> don't even draw the bottom pixel + if (f>1) f = 1; + if (f<0) f = 0; + const top = Math.round((h-1)*(1-f)); + // use Math.min/max to make sure we stay within widget boundaries for f=0/f=1 + if (top) g .clearRect(x,y, x+w-1,y+top-1); // erase above bar + if (f) g.setColor(col).fillRect(x,y+top, x+w-1,y+h-1); // even for f=0.001 this is still 1 pixel high + } + function draw() { + g.reset(); + const x = this.x, y = this.y, + m = process.memory(); + let b=0; + // ==HRM== bar(x+(w*b++),y,'#f00'/*red */,bpm/200); // >200 seems very unhealthy; if we have no valid bpm this will just be empty space + // ==Temperature== bar(x+(w*b++),y,'#ff0'/*yellow */,E.getTemperature()/50); // you really don't want to wear a watch that's hotter than 50°C + bar(x+(w*b++),y,g.theme.dark?'#0ff':'#00f'/*cyan/blue*/,1-(require('Storage').getFree() / process.env.STORAGE)); + bar(x+(w*b++),y,'#f0f'/*magenta*/,m.usage/m.total); + bar(x+(w*b++),y,'#0f0'/*green */,E.getBattery()/100); + } + + let redraw; + Bangle.on('lcdPower', on => { + if (redraw) clearInterval(redraw) + redraw = undefined; + if (on) { + WIDGETS["bars"].draw(); + redraw = setInterval(()=>WIDGETS["bars"].draw, 10*1000); // redraw every 10 seconds + } + }); + WIDGETS["bars"]={area:"tr",width: bars*w,draw:draw}; +})() diff --git a/apps/widbata/ChangeLog b/apps/widbata/ChangeLog new file mode 100644 index 000000000..51575d3b4 --- /dev/null +++ b/apps/widbata/ChangeLog @@ -0,0 +1 @@ +0.01: Created diff --git a/apps/widbata/README.md b/apps/widbata/README.md new file mode 100644 index 000000000..6c3012793 --- /dev/null +++ b/apps/widbata/README.md @@ -0,0 +1,14 @@ +# Battery Level Widget (Themed) + +Shows the current battery level status in the top right using the clocks colour theme + +* Works with Bangle 2 +* Simple design, no settings +* 27 pixels wide +* Uses current colour theme to match clock + +![](screenshot_widbata_1.png) +![](screenshot_widbata_2.png) +![](screenshot_widbata_3.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/widbata/screenshot_widbata_1.png b/apps/widbata/screenshot_widbata_1.png new file mode 100644 index 000000000..5fdc3ac3d Binary files /dev/null and b/apps/widbata/screenshot_widbata_1.png differ diff --git a/apps/widbata/screenshot_widbata_2.png b/apps/widbata/screenshot_widbata_2.png new file mode 100644 index 000000000..6a6ec8581 Binary files /dev/null and b/apps/widbata/screenshot_widbata_2.png differ diff --git a/apps/widbata/screenshot_widbata_3.png b/apps/widbata/screenshot_widbata_3.png new file mode 100644 index 000000000..824309702 Binary files /dev/null and b/apps/widbata/screenshot_widbata_3.png differ diff --git a/apps/widbata/widbata.png b/apps/widbata/widbata.png new file mode 100644 index 000000000..877af1707 Binary files /dev/null and b/apps/widbata/widbata.png differ diff --git a/apps/widbata/widbata.wid.js b/apps/widbata/widbata.wid.js new file mode 100644 index 000000000..1c04bf8ae --- /dev/null +++ b/apps/widbata/widbata.wid.js @@ -0,0 +1,16 @@ +setInterval(()=>WIDGETS["bata"].draw(), 60000); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["bata"].draw(); +}); +WIDGETS["bata"]={area:"tr",width:27,draw:function() { + var s = 26; + var t = 13; // thickness + var x = this.x, y = this.y; + g.reset(); + g.setColor(g.theme.fg); + g.fillRect(x,y+2,x+s-4,y+2+t); // outer + g.clearRect(x+2,y+2+2,x+s-4-2,y+2+t-2); // centre + g.setColor(g.theme.fg); + g.fillRect(x+s-3,y+2+(((t - 1)/2)-1),x+s-2,y+2+(((t - 1)/2)-1)+4); // contact + g.fillRect(x+3, y+5, x +4 + E.getBattery()*(s-12)/100, y+t-1); // the level +}}; diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 09e4fabf4..99822b5a9 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -10,3 +10,4 @@ 0.11: Don't overwrite existing settings on app update 0.12: Fixed for Bangle 2 0.13: Fillbar setting added, see README +0.14: Fix drawing the bar when charging diff --git a/apps/widbatpc/README.md b/apps/widbatpc/README.md index c75154f72..48c6070f4 100644 --- a/apps/widbatpc/README.md +++ b/apps/widbatpc/README.md @@ -5,12 +5,12 @@ Show the current battery level and charging status in the top right of the clock Works with Bangle 1 and Bangle 2 When the fillbar setting is on the level colour will fill the entire -bar. This makes for an easier to read dsiplay when the charge is +bar. This makes for an easier to read display when the charge is below 50%. ![](widbatpc.full.jpg) -When the fillbar setting is off the level colour will follow the battry percentage +When the fillbar setting is off the level colour will follow the battery percentage ![](widbatpc.part.jpg) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index caecf8ae4..3e5ff47b4 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -79,20 +79,20 @@ // else... var s = 39; var x = this.x, y = this.y; - const l = E.getBattery(); - let xl = x+4+l*(s-12)/100; + const l = E.getBattery(), + c = levelColor(l); - // show bar full in the level color, as you cant see the color if the bar is too small - if (setting('fillbar')) - xl = x+4+100*(s-12)/100; - - c = levelColor(l); - if (Bangle.isCharging() && setting('charger')) { g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); x+=16; } + + let xl = x+4+l*(s-12)/100; + // show bar full in the level color, as you can't see the color if the bar is too small + if (setting('fillbar')) + xl = x+4+100*(s-12)/100; + g.setColor(g.theme.fg); g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); diff --git a/apps/widmpsh/ChangeLog b/apps/widmpsh/ChangeLog new file mode 100644 index 000000000..e432f82e5 --- /dev/null +++ b/apps/widmpsh/ChangeLog @@ -0,0 +1 @@ +0.01: Copied from widmp and flipped the phase directions! diff --git a/apps/widmpsh/widget.js b/apps/widmpsh/widget.js new file mode 100644 index 000000000..9115a4719 --- /dev/null +++ b/apps/widmpsh/widget.js @@ -0,0 +1,25 @@ +WIDGETS["widmoonsh"] = { area: "tr", width: 24, draw: function() { + const MC = 29.5305882, NM = 694039.09; + var r = 11, mx = this.x + 12; my = this.y + 12; + + function moonPhase(d) { + var tmp, month = d.getMonth(), year = d.getFullYear(), day = d.getDate(); + if (month < 3) {year--; month += 12;} + tmp = ((365.25 * year + 30.6 * ++month + day - NM) / MC); + return Math.round(((tmp - (tmp | 0)) * 7)+1); + } + + const BLACK = g.theme.bg, MOON = 0x41f; + var moon = { + 0: () => { g.reset().setColor(BLACK).fillRect(mx - r, my - r, mx + r, my + r);}, + 1: () => { moon[0](); g.setColor(MOON).drawCircle(mx, my, r);}, + 2: () => { moon[3](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, + 3: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx, my - r, mx + r, my + r);}, + 4: () => { moon[3](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, + 5: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r);}, + 6: () => { moon[7](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, + 7: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx - r, my - r, mx, my + r);}, + 8: () => { moon[7](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);} + }; + moon[moonPhase(Date())](); +} }; diff --git a/apps/widmpsh/widget.png b/apps/widmpsh/widget.png new file mode 100644 index 000000000..aeaadea4b Binary files /dev/null and b/apps/widmpsh/widget.png differ diff --git a/apps/widviz/ChangeLog b/apps/widviz/ChangeLog index e1958b429..9785f4d84 100644 --- a/apps/widviz/ChangeLog +++ b/apps/widviz/ChangeLog @@ -1,3 +1,3 @@ - 0.01: New Widget - 0.02: swipe left,right update - +0.01: New Widget +0.02: swipe left,right update +0.03: Fix widget visibility code to the top bar isn't cleared by drawWidgets diff --git a/apps/widviz/widget.js b/apps/widviz/widget.js index 241dabf61..1490cf11a 100644 --- a/apps/widviz/widget.js +++ b/apps/widviz/widget.js @@ -6,16 +6,21 @@ if (!Bangle.isLCDOn() || saved) return; saved = []; for (var wd of WIDGETS) { - saved.push(wd.draw); + saved.push({d:wd.draw,a:wd.area}); wd.draw=()=>{}; + wd.area=""; } g.setColor(0,0,0); - g.fillRect(0,0,239,23); + g.fillRect(0,0,g.getWidth(),23); } function reveal(){ if (!Bangle.isLCDOn() || !saved) return; - for (var wd of WIDGETS) wd.draw = saved.shift(); + for (var wd of WIDGETS) { + var o = saved.shift(); + wd.draw = o.d; + wd.area = o.a; + } Bangle.drawWidgets(); saved=null; } diff --git a/bin/language_scan.js b/bin/language_scan.js new file mode 100755 index 000000000..2a92fded1 --- /dev/null +++ b/bin/language_scan.js @@ -0,0 +1,69 @@ +#!/usr/bin/nodejs +/* Scans for strings that may be in English in each app, and +outputs a list of strings that have been found. + +Early work towards internationalisation. +See https://github.com/espruino/BangleApps/issues/136 +*/ + +var BASEDIR = __dirname+"/../"; +Espruino = require(BASEDIR+"core/lib/espruinotools.js"); +var fs = require("fs"); + +var APPSDIR = BASEDIR+"apps/"; +function ERROR(s) { + console.error("ERROR: "+s); + process.exit(1); +} +function WARN(s) { + console.log("Warning: "+s); +} + +var appsFile, apps; +try { + appsFile = fs.readFileSync(BASEDIR+"apps.json").toString(); +} catch (e) { + ERROR("apps.json not found"); +} +try{ + apps = JSON.parse(appsFile); +} catch (e) { + ERROR("apps.json not valid JSON"); +} + +// Given a string value, work out if it's obviously not a text string +function isNotString(s) { + if (s.length<2) return true; // too short + if (s.length>40) return true; // too long + if (s[0]=="#") return true; // a color + if (s.endsWith(".json") || s.endsWith(".img")) return true; // a filename + if (s.endsWith("=")) return true; // probably base64 + if (s.startsWith("BTN")) return true; // button name + return false; +} + +var textStrings = []; + +console.log("Scanning..."); +apps.forEach((app,appIdx) => { + var appDir = APPSDIR+app.id+"/"; + app.storage.forEach((file) => { + if (!file.url || !file.name.endsWith(".js")) return; + var fileContents = fs.readFileSync(appDir+file.url).toString(); + var lex = Espruino.Core.Utils.getLexer(fileContents); + var tok = lex.next(); + while (tok!==undefined) { + if (tok.type=="STRING") { + if (!isNotString(tok.value)) { + //console.log(tok.str); + if (!textStrings.includes(tok.value)) + textStrings.push(tok.value); + } + } + tok = lex.next(); + } + }); +}); +console.log("Done"); +textStrings.sort(); +console.log(textStrings.join("\n")); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index ea45dc19b..572364224 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -207,10 +207,10 @@ apps.forEach((app,appIdx) => { } }); // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?) - if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json")) + /* if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json")) WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`) else if (dataNames.includes(app.id+".settings.json")) - WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`) + WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`)*/ // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?) if (fileNames.includes(app.id+".settings.json")) WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`) diff --git a/core b/core index 50aa45f13..2a8e872ec 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 50aa45f13f06cc2f40684971d4c0a68d061b1f3c +Subproject commit 2a8e872ecb143a10e53273b4d3473164e104e1d3 diff --git a/lang/de_DE.json b/lang/de_DE.json new file mode 100644 index 000000000..80d0e74bb --- /dev/null +++ b/lang/de_DE.json @@ -0,0 +1,23 @@ +{ + "//":"German language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarm" : "Wecker", + "Hours" : "Stunden", + "Minutes" : "Minuten", + "Enabled" : "Aktiviert", + "Settings" : "Einstellungen", + "Save" : "Speichern", + "Back" : "Zurück", + "Repeat" : "Wiederholen", + "Delete" : "Löschen", + "Sleep" : "Schlummern", + "Alarms" : "Wecker", + "New Alarm" : "Neuer Wecker", + "ALARM!" : "ALARM!" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "Wdh." + } +} diff --git a/lang/en_GB.json b/lang/en_GB.json new file mode 100644 index 000000000..e85fe8029 --- /dev/null +++ b/lang/en_GB.json @@ -0,0 +1,9 @@ +{ + "//":"British English language translations - the default strings in apps are all english anyway, so no need to have translations for most things", + "GLOBAL": { + "//":"Translations that apply for all apps", + }, + "alarm": { + "//":"App-specific overrides", + } +} diff --git a/lang/es_ES.json b/lang/es_ES.json new file mode 100644 index 000000000..0671c4ab8 --- /dev/null +++ b/lang/es_ES.json @@ -0,0 +1,21 @@ +{ + "//":"Spanish language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Alarmas", + "Hours" : "Horas", + "Minutes" : "Minutos", + "Enabled" : "Activados", + "New Alarm" : "Alarma nueva", + "Save" : "Grabar", + "Back" : "Atrás", + "Repeat" : "Repetición", + "Delete" : "Borrar", + "ALARM!" : "ALARM", + "Sleep" : "Dormir" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "rep." + } +} diff --git a/lang/fi_FI.json b/lang/fi_FI.json new file mode 100644 index 000000000..eb1d826d8 --- /dev/null +++ b/lang/fi_FI.json @@ -0,0 +1,21 @@ +{ + "//":"Finnish language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Hälytykset", + "Hours" : "Tunnit", + "Minutes" : "Minuutit", + "Enabled" : "Aktivoitu", + "New Alarm" : "Uusi hälytys", + "Save" : "Tallenna", + "Back" : "Paluu", + "Repeat" : "Toista", + "Delete" : "Poista", + "ALARM!" : "ALARM", + "Sleep" : "Nukkuminen" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "toistaa" + } +} diff --git a/lang/fr_FR.json b/lang/fr_FR.json new file mode 100644 index 000000000..209574424 --- /dev/null +++ b/lang/fr_FR.json @@ -0,0 +1,21 @@ +{ + "//":"French language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Réveils", + "Hours" : "Heures", + "Minutes" : "Minutes", + "Enabled" : "Activé", + "New Alarm" : "Nouveau Réveil", + "Save" : "Sauvegarder", + "Back" : "Retour", + "Repeat" : "Répétition", + "Delete" : "Supprimer", + "ALARM!" : "ALARM!", + "Sleep" : "Sommeil" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "rép." + } +} diff --git a/lang/hu_HU.json b/lang/hu_HU.json new file mode 100644 index 000000000..8e5df6ed7 --- /dev/null +++ b/lang/hu_HU.json @@ -0,0 +1,21 @@ +{ + "//":"Spanish language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Riasztások", + "Hours" : "Óra", + "Minutes" : "Perc", + "Enabled" : "Aktiválva", + "New Alarm" : "Új riasztás", + "Save" : "Mentés", + "Back" : "Vissza", + "Repeat" : "Ismétlés", + "Delete" : "Törlés", + "ALARM!" : "ALARM!", + "Sleep" : "Alvás" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "ismétlés" + } +} diff --git a/lang/index.json b/lang/index.json new file mode 100644 index 000000000..f17bf5e03 --- /dev/null +++ b/lang/index.json @@ -0,0 +1,12 @@ +[ + {"code":"en_GB","name":"British English","url":"en_GB.json"}, + {"code":"de_DE","name":"German","url":"de_DE.json"}, + {"code":"es_ES","name":"Spanish","url":"es_ES.json"}, + {"code":"fi_FI","name":"Finnish","url":"fi_FI.json"}, + {"code":"fr_FR","name":"French","url":"fr_FR.json"}, + {"code":"hu_HU","name":"Hungarian","url":"hu_HU.json"}, + {"code":"it_IT","name":"Italian","url":"it_IT.json"}, + {"code":"nl_NL","name":"Dutch","url":"nl_NL.json"}, + {"code":"sv_SE","name":"Swedish","url":"sv_SE.json"}, + {"code":"tr_TR","name":"Turkish","url":"tr_TR.json"} +] diff --git a/lang/it_IT.json b/lang/it_IT.json new file mode 100644 index 000000000..184c80238 --- /dev/null +++ b/lang/it_IT.json @@ -0,0 +1,21 @@ +{ + "//":"Italian language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Allarmi", + "Hours" : "Ore", + "Minutes" : "Minuti", + "Enabled" : "Attivato", + "New Alarm" : "Nuovo allarme", + "Save" : "Salvare", + "Back" : "Indietro", + "Repeat" : "Ripetere", + "Delete" : "Cancellare", + "ALARM!" : "ALARM!", + "Sleep" : "Dormire" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "ripetere" + } +} diff --git a/lang/nl_NL.json b/lang/nl_NL.json new file mode 100644 index 000000000..a04e46928 --- /dev/null +++ b/lang/nl_NL.json @@ -0,0 +1,21 @@ +{ + "//":"Dutch language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Alarmen", + "Hours" : "Uren", + "Minutes" : "Minuten", + "Enabled" : "Geactiveerd", + "New Alarm" : "Nieuw alarm", + "Save" : "Opslaan", + "Back" : "Terug", + "Repeat" : "Herhalen", + "Delete" : "Verwijderen", + "ALARM!" : "ALARV.", + "Sleep" : "Stand-by" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "herhalen" + } +} diff --git a/lang/sv_SE.json b/lang/sv_SE.json new file mode 100644 index 000000000..3a006c2bf --- /dev/null +++ b/lang/sv_SE.json @@ -0,0 +1,21 @@ +{ + "//":"Swedish language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Larm", + "Hours" : "Timmar", + "Minutes" : "Minuter", + "Enabled" : "Aktiverad", + "New Alarm" : "Ny alarm", + "Save" : "Spara", + "Back" : "Tillbaka", + "Repeat" : "Upprepning", + "Delete" : "Radera", + "ALARM!" : "ALURH!", + "Sleep" : "Sömn" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "uppr." + } +} diff --git a/lang/tr_TR.json b/lang/tr_TR.json new file mode 100644 index 000000000..c59bc7d6b --- /dev/null +++ b/lang/tr_TR.json @@ -0,0 +1,21 @@ +{ + "//":"Turkish language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Alarmlar", + "Hours" : "Saat", + "Minutes" : "Dakika", + "Enabled" : "Etkinleştirildi", + "New Alarm" : "Yeni alarm", + "Save" : "Sakla", + "Back" : "Geriye", + "Repeat" : "Yineleme", + "Delete" : "Sil", + "ALARM!" : "ALARM!", + "Sleep" : "Uyku" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "yineleme" + } +} diff --git a/modules/Settings.js b/modules/Settings.js deleted file mode 100644 index 0828b4655..000000000 --- a/modules/Settings.js +++ /dev/null @@ -1,101 +0,0 @@ -/* -- Read/write app settings, stored in .json -- Read/write global settings (stored in setting.json) - -Usage: -``` -// read a single app setting -value = require('Settings').get(appid, key, default); -// omit key to read all app settings -value = require('Settings').get(appid); -// write a single app setting -require('Settings').set(appid, key, value) -// omit key and pass an object as values to overwrite all settings -require('Settings').set(appid, values) - -// read Bangle settings by passing the Bangle object instead of an app name -value = require('Settings').get(Bangle, key, default); -// read all global settings -values = require('Settings').get(Bangle); -// write a global setting -require('Settings').set(Bangle, key, value) -``` - -For example: -``` -require('Settings').set('test', 'foo', 123); // writes to 'test.json' -require('Settings').set('test', 'bar', 456); // updates 'test.json' -// 'test.json' now contains {baz:123,bam:456} -baz = require('Settings').get('test', 'foo'); // baz = 123 -def = require('Settings').get('test', 'jkl', 789); // def = 789 -all = require('Settings').get('test'); // all = {foo: 123, bar: 456} -baz = require('Settings').get('test', 'baz'); // baz = undefined - -// read global setting -vibrate = require('Settings').get(Bangle, 'vibrate', true); - -// Hint: if your app reads multiple settings, you can create a helper function: -function s(key, def) { return require('Settings').get('myapp', key, def); } -var foo = s('foo setting', 'default value'), bar = s('bar setting'); -``` - -*/ - -/** - * Read setting value from file - * - * @param {string} file Settings file - * @param {string} key Setting to get, omit to get all settings as object - * @param {*} def Default value - * @return {*} Setting value (or default if not found) - */ -function get(file, key, def) { - var s = require("Storage").readJSON(file); - if (def===undefined && ["object", "undefined"].includes(typeof key)) { - // get(file) or get(file, def): get all settings - return (s!==undefined) ? s : key; - } - return ((typeof s==="object") && (key in s)) ? s[key] : def; -} - -/** - * Write setting value to file - * - * @param {string} file Settings file - * @param {string} key Setting to change, omit to replace all settings - * @param {*} value Value to store - */ -function set(file, key, value) { - if (value===undefined && typeof key==="object") { - // set(file, value): overwrite settings completely - require("Storage").writeJSON(file, key); - return; - } - var s = require("Storage").readJSON(file, 1); - if (typeof s!=="object") s = {}; - s[key] = value; - require("Storage").write(file, s); -} - -/** - * Read setting value - * - * @param {string|object} app App name or Bangle - * @param {string} key Setting to get, omit to get all settings as object - * @param {*} def Default value - * @return {*} Setting value (or default if not found) - */ -exports.get = function(app, key, def) { - return get((app===Bangle) ? 'setting.json' : app+".json", key, def); -}; - -/** - * Write setting value - * - * @param {string|object} app App name or Bangle - * @param {string} key Setting to change, omit to replace all settings - * @param {*} val Value to store - */ -exports.set = function(app, key, val) { - set((app===Bangle) ? 'setting.json' : app+".json", key, val); -}; diff --git a/testing/map/README.md b/testing/map/README.md deleted file mode 100644 index 03705019a..000000000 --- a/testing/map/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This code can take an image file, split it into tiles, and then render those tiles on the watch - making them fit with the GPS data. - -Problem is right now I can't automate getting the rendered area of map, so can't turn it into a very useful tool for BangleApps. diff --git a/testing/map/espruinomap.js b/testing/map/espruinomap.js deleted file mode 100644 index cea770600..000000000 --- a/testing/map/espruinomap.js +++ /dev/null @@ -1,83 +0,0 @@ -require("Storage").write('+map',{ - name:"Map", - icon:"*map", - src:"-map" -}); -require("Storage").write('*map',require("heatshrink").decompress(atob("mEwghC/AH4AWh//mcwBZIWI/4ABmYABBZAgIC4oyDBYggIC4wABBYoX/C90imcykYXUkYBB+YyDC5E/F5EykQXKHwYVCL4YXNkQ+BC4wICHgIvJ+QVBC4oYBkUvO5QXCU4wXBF5INCCwqMDAYTXUC6xHNC5Z3LI5UyF6oADF9ZfL+fTAIIUCkUjR5397s9C4LxBC4MykfzDYYvI7vdC4cyDIciO5c97s/C4QABF4IBBC5QvEAAk/+ZdBC5JfEX6XzmaPEa7oX8+AGBgYXHBYQXHBAoXFCowXCEA4yCBZIA/AH4AO"))); - -require("Storage").write("-map",` -var s = require("Storage"); -var hs = require("heatshrink"); -var map = { - imgx : 831, - imgy : 656, - tilesize : 64, - scale : 20000, - lat : 51.7075, - lon : -1.2948 -}; - - -map.center = Bangle.project({lat:map.lat,lon:map.lon}); -var lat = map.lat, lon = map.lon; -var fix = {}; - - -function redraw() { - var cx = g.getWidth()/2; - var cy = g.getHeight()/2; - var p = Bangle.project({lat:lat,lon:lon}); - var ix = (p.x-map.center.x)*4096/map.scale + (map.imgx/2) - cx; - var iy = (map.center.y-p.y)*4096/map.scale + (map.imgy/2) - cy; - //console.log(ix,iy); - var tx = 0|(ix/map.tilesize); - var ty = 0|(iy/map.tilesize); - var ox = (tx*map.tilesize)-ix; - var oy = (ty*map.tilesize)-iy; - for (var x=ox,ttx=tx;x - - - - - - - -

An online map loader for Espruino...

- Scale
- Geo URI
-
-
-
- - -

-
-  
-
-   
- 
-