diff --git a/.gitignore b/.gitignore index 47233d1f5..523dc5f20 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ appdates.csv _config.yml tests/Layout/bin/tmp.* tests/Layout/testresult.bmp +apps.local.json \ No newline at end of file diff --git a/README.md b/README.md index 20ae8afb2..985d5ac5a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ and that it is not licensed in another way that would make this impossible. ## How does it work? -* A list of apps is in `apps.json` +* A list of apps is in `apps.json` (this is auto-generated from all the `apps/yourapp/metadata.json` using Jekyll or `bin/create_apps_json.sh`) * Each element references an app in `apps/` which is uploaded * When it starts, BangleAppLoader checks the JSON and compares it with the files it sees in the watch's storage. @@ -53,10 +53,10 @@ easily distinguish between file types, we use the following: is limited to 28 char filenames and appends a file extension (eg `.js`) so please try and keep filenames short to avoid overflowing the buffer. * Create a folder called `apps/`, lets assume `apps/myappid` -* We'd recommend that you copy files from 'Example Applications' (below) as a base, or... +* We'd recommend that you copy files from one of the Examples in `apps/_example_*` (see below), or... * `apps/myappid/app.png` should be a 48px icon * Use http://www.espruino.com/Image+Converter to create `apps/myappid/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String" -* Create an entry in `apps.json` as follows: +* Create/modify `apps/myappid/metadata.json` as follows: ``` { "id": "myappid", @@ -116,8 +116,7 @@ and set it to `Load default application`. To make the process easier we've come up with some example applications that you can use as a base when creating your own. Just come up with a unique name (ideally lowercase, under 20 chars), copy `apps/_example_app` -or `apps/_example_widget` to `apps/myappid`, and add `apps/_example_X/add_to_apps.json` to -`apps.json`. +or `apps/_example_widget` to `apps/myappid`, and edit `apps/myappid/metadata.json` accordingly. **Note:** the max filename length is 28 chars, so we suggest an app ID of under 20 so that when `.app.js`/etc gets added to the end the filename isn't cropped. @@ -131,7 +130,7 @@ The app example is available in [`apps/_example_app`](apps/_example_app) Apps are listed in the Bangle.js menu, accessible from a clock app via the middle button. -* `add_to_apps.json` - insert into `apps.json`, describes the app to bootloader and loader +* `metadata.json` - describes the app to bootloader and loader * `app.png` - app icon - 48x48px * `app-icon.js` - JS version of the icon (made with http://www.espruino.com/Image+Converter) for use in Bangle.js's menu * `app.js` - app code @@ -144,11 +143,11 @@ Use the Espruino [image converter](https://www.espruino.com/Image+Converter) and Follow this steps to create a readable icon as image string. -1. upload a png file +1. upload a 48x48 png file - THE IMAGE SHOULD BE 48x48 OR LESS 2. set _X_ Use Compression 3. set _X_ Transparency (optional) 4. set Diffusion: _flat_ -5. set Colours: _1 bit_, _4 bit_ or _8 bit Web Palette_ +5. set Colours: _1 bit_, any of the Optimised options, or _8 bit Web Palette_ are best 6. set Output as: _Image String_ Replace this line with the image converter output: @@ -157,6 +156,8 @@ Replace this line with the image converter output: require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) ``` +**Do not add a trailing semicolon** + You can also use this converter for creating images you like to draw with `g.drawImage()` with your app. Apps that need widgets can call `Bangle.loadWidgets()` **once** at startup to load @@ -167,17 +168,18 @@ has call to completely clear the screen. Widgets themselves will update as and w The widget example is available in [`apps/_example_widget`](apps/_example_widget) -* `add_to_apps.json` - insert into `apps.json`, describes the widget to bootloader and loader +* `metadata.json` - describes the widget to bootloader and loader * `widget.js` - widget code Widgets are just small bits of code that run whenever an app that supports them calls `Bangle.loadWidgets()`. If they want to display something in the 24px high -widget bars at the top and bottom of the screen they can add themselves to -the global `WIDGETS` array with: +widget bar at the top of the screen they can add themselves to the global +`WIDGETS` array with: ``` WIDGETS["mywidget"]={ - area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + area:"tl", // tl (top left), tr (top right) + sortorder:0, // (Optional) determines order of widgets in the same corner width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout draw:draw // called to draw the widget }; @@ -202,7 +204,7 @@ and which gives information about the app for the Launcher. // if it's 'clock' then it'll be loaded by default at boot time // if this is 'bootloader' then it's code that is run at boot time, but is not in a menu "version":"1.23", - // added by BangleApps loader on upload based on apps.json + // added by BangleApps loader on upload based on metadata.json "files:"file1,file2,file3", // added by BangleApps loader on upload - lists all files // that belong to the app so it can be deleted @@ -214,7 +216,7 @@ and which gives information about the app for the Launcher. } ``` -### `apps.json` format +### `metadata.json` format ``` { "id": "appid", // 7 character app id @@ -293,9 +295,9 @@ and which gives information about the app for the Launcher. * storage is used to identify the app files and how to handle them * data is used to clean up files when the app is uninstalled -### `apps.json`: `custom` element +### `metadata.json`: `custom` element -Apps that can be customised need to define a `custom` element in `apps.json`, +Apps that can be customised need to define a `custom` element in `metadata.json`, which names an HTML file in that app's folder. When `custom` is defined, the 'upload' button is replaced by a customize @@ -303,7 +305,7 @@ button, and when clicked it opens the HTML page specified in an iframe. In that HTML file you're then responsible for handling a button press and calling `sendCustomizedApp` with your own customised -version of what's in `apps.json`: +version of what's in `metadata.json`: ``` @@ -335,9 +337,9 @@ for a clean example. and will never be loaded. This is so the app loader can tell if it's a JavaScript file based on the extension, and if so it can minify and pretokenise it. -### `apps.json`: `interface` element +### `metadata.json`: `interface` element -Apps that create data that can be read back can define a `interface` element in `apps.json`, +Apps that create data that can be read back can define a `interface` element in `metadata.json`, which names an HTML file in that app's folder. When `interface` is defined, a `Download from App` button is added to @@ -384,20 +386,24 @@ 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) }) ``` -In this example the app needs to add `myappid.settings.js` to `storage` in `apps.json`. +In this example the app needs to add `myappid.settings.js` to `storage` in `metadata.json`. It should also add `myappid.json` to `data`, to make sure it is cleaned up when the app is uninstalled. ```json { "id": "myappid", @@ -457,16 +463,13 @@ The screen is parted in a widget and app area for lcd mode `direct`(default). | areas | as rectangle or point | | :-:| :-: | | Widget | (0,0,239,23) | -| Widget bottom bar (optional) | (0,216,239,239) | -| Apps | (0,24,239,239) (see below) | +| Apps | (0,24,239,239) | | BTN1 | (230, 55) | | BTN2 | (230, 140) | | BTN3 | (230, 210) | | BTN4 | (0,0,119, 239)| | BTN5 | (120,0,239,239) | -- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215) - - Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`. - For BTN4-5 the touch area is named diff --git a/apps.json b/apps.json index 1c614c592..537a4f697 100644 --- a/apps.json +++ b/apps.json @@ -1,4530 +1,38 @@ +--- +# ================================================================= +# ALL THE INFORMATION INSIDE APPS.JSON HAS NOW BEEN MOVED +# +# You'll find it inside a file called apps/yourapp/metadata.json +# +# Otherwise nothing has changed. GitHub Pages will automatically +# create apps.json as your site is hosted, or if you're hosting +# yourself you can run bin/create_apps_json.sh +# +# If you serve the store from localhost for development/testing, +# the loader looks for apps.local.json instead, you can run +# `bin/create_apps_json.sh apps.local.json` to create that file. +# ================================================================= + +# Uncomment the following line if you only want explicitly listed +# apps to be available on your site + +# restricted: ["boot", "launch", "antonclk", "health", "setting", "about", "widbat", "widbt", "widlock", "widid"] +--- +{%- if page.restricted == nil -%} + {%- assign apps = site.static_files | where: "name", "metadata.json" | map: "path" -%} +{%- else -%} + {%- capture temp -%} + {%- for app in page.restricted %} /apps/{{app}}/metadata.json {%- endfor -%} + {%- endcapture -%} + {%- assign apps = temp | strip | split: " " -%} +{%- endif -%} + [ - { - "id": "fwupdate", - "name": "Firmware Update (BETA)", - "version": "0.01", - "description": "Uploads new Espruino firmwares to Bangle.js 2", - "icon": "app.png", - "type": "RAM", - "tags": "tools,system", - "supports": ["BANGLEJS2"], - "custom": "custom.html", - "customConnect": true, - "storage": [], - "sortorder": -20 - }, - { - "id": "boot", - "name": "Bootloader", - "version": "0.36", - "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", - "icon": "bootloader.png", - "type": "bootloader", - "tags": "tool,system", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":".boot0","url":"boot0.js"}, - {"name":".bootcde","url":"bootloader.js"}, - {"name":"bootupdate.js","url":"bootupdate.js"} - ], - "sortorder": -10 - }, - { - "id": "messages", - "name": "Messages", - "version": "0.07", - "description": "App to display notifications from iOS and Gadgetbridge", - "icon": "app.png", - "type": "app", - "tags": "tool,system", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"messages.app.js","url":"app.js"}, - {"name":"messages.settings.js","url":"settings.js"}, - {"name":"messages.img","url":"app-icon.js","evaluate":true}, - {"name":"messages.wid.js","url":"widget.js"}, - {"name":"messages","url":"lib.js"} - ], - "data": [{"name":"messages.json"},{"name":"messages.settings.json"}], - "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.", - "icon": "app.png", - "tags": "tool,system,messages,notifications", - "dependencies": {"messages":"app"}, - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"android.app.js","url":"app.js"}, - {"name":"android.settings.js","url":"settings.js"}, - {"name":"android.img","url":"app-icon.js","evaluate":true}, - {"name":"android.boot.js","url":"boot.js"} - ], - "sortorder": -8 - }, - { - "id": "ios", - "name": "iOS Integration", - "version": "0.03", - "description": "(BETA) App to display notifications from iOS devices", - "icon": "app.png", - "tags": "tool,system,ios,apple,messages,notifications", - "dependencies": {"messages":"app"}, - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"ios.app.js","url":"app.js"}, - {"name":"ios.img","url":"app-icon.js","evaluate":true}, - {"name":"ios.boot.js","url":"boot.js"} - ], - "sortorder": -8 - }, - { - "id": "health", - "name": "Health Tracking", - "version": "0.08", - "description": "Logs health data and provides an app to view it (BETA - requires firmware 2v11)", - "icon": "app.png", - "tags": "tool,system,health", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"health.app.js","url":"app.js"}, - {"name":"health.img","url":"app-icon.js","evaluate":true}, - {"name":"health.boot.js","url":"boot.js"}, - {"name":"health","url":"lib.js"} - ] - }, - { - "id": "launch", - "name": "Launcher", - "shortName": "Launcher", - "version": "0.08", - "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", - "tags": "tool,system,launcher", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"launch.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]}, - {"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]} - ], - "sortorder": -10 - }, - { - "id": "setting", - "name": "Settings", - "version": "0.33", - "description": "A menu for setting up Bangle.js", - "icon": "settings.png", - "tags": "tool,system", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"setting.app.js","url":"settings.js"}, - {"name":"setting.img","url":"settings-icon.js","evaluate":true} - ], - "data": [{"name":"setting.json","url":"settings.min.json","evaluate":true}], - "sortorder": -5 - }, - { - "id": "about", - "name": "About", - "version": "0.11", - "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", - "supports": ["BANGLEJS","BANGLEJS2"], - "screenshots": [{"url":"bangle1-about-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"about.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, - {"name":"about.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]}, - {"name":"about.img","url":"app-icon.js","evaluate":true} - ], - "sortorder": -4 - }, - { - "id": "alarm", - "name": "Default Alarm & Timer", - "shortName": "Alarms", - "version": "0.14", - "description": "Set and respond to alarms and timers", - "icon": "app.png", - "tags": "tool,alarm,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"alarm.app.js","url":"app.js"}, - {"name":"alarm.boot.js","url":"boot.js"}, - {"name":"alarm.js","url":"alarm.js"}, - {"name":"alarm.img","url":"app-icon.js","evaluate":true}, - {"name":"alarm.wid.js","url":"widget.js"} - ], - "data": [{"name":"alarm.json"}] - }, - { - "id": "locale", - "name": "Languages", - "version": "0.10", - "description": "Translations for different countries", - "icon": "locale.png", - "type": "locale", - "tags": "tool,system,locale,translate", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "custom": "locale.html", - "storage": [ - {"name":"locale"} - ], - "sortorder": -10 - }, - { - "id": "notify", - "name": "Notifications (default)", - "shortName": "Notifications", - "version": "0.11", - "description": "Provides the default `notify` module used by applications to display notifications in a bar at the top of the screen. This module is installed by default by client applications such as the Gadgetbridge app. Installing `Fullscreen Notifications` replaces this module with a version that displays the notifications using the full screen", - "icon": "notify.png", - "type": "notify", - "tags": "widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"notify","url":"notify.js"} - ] - }, - { - "id": "notifyfs", - "name": "Fullscreen Notifications", - "shortName": "Notifications", - "version": "0.12", - "description": "Provides a replacement for the `Notifications (default)` `notify` module. This version is used by applications to display notifications fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notify module.", - "icon": "notify.png", - "type": "notify", - "tags": "widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"notify","url":"notify.js"} - ] - }, - { - "id": "welcome", - "name": "Welcome", - "shortName": "Welcome", - "version": "0.14", - "description": "Appears at first boot and explains how to use Bangle.js", - "icon": "app.png", - "screenshots": [{"url":"screenshot_welcome.png"}], - "tags": "start,welcome", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"welcome.boot.js","url":"boot.js"}, - {"name":"welcome.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, - {"name":"welcome.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]}, - {"name":"welcome.settings.js","url":"settings.js"}, - {"name":"welcome.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"welcome.json"}] - }, - { - "id": "mywelcome", - "name": "Customised Welcome", - "shortName": "My Welcome", - "version": "0.12", - "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"], - "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.settings.js","url":"settings.js"}, - {"name":"mywelcome.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"mywelcome.json"}] - }, - { - "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.", - "icon": "app.png", - "type": "widget", - "tags": "tool,system,android,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "dependencies": {"notify":"type"}, - "readme": "README.md", - "storage": [ - {"name":"gbridge.settings.js","url":"settings.js"}, - {"name":"gbridge.img","url":"app-icon.js","evaluate":true}, - {"name":"gbridge.wid.js","url":"widget.js"} - ], - "data": [{"name":"gbridge.json"}] - }, - { - "id": "mclock", - "name": "Morphing Clock", - "version": "0.07", - "description": "7 segment clock that morphs between minutes and hours", - "icon": "clock-morphing.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-morphing-clock-screenshot.png"}], - "storage": [ - {"name":"mclock.app.js","url":"clock-morphing.js"}, - {"name":"mclock.img","url":"clock-morphing-icon.js","evaluate":true} - ], - "sortorder": -9 - }, - { - "id": "moonphase", - "name": "Moonphase", - "version": "0.02", - "description": "Shows current moon phase. Now with GPS function.", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-moon-phase-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"moonphase.app.js","url":"app.js"}, - {"name":"moonphase.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "daysl", - "name": "Days left", - "version": "0.03", - "description": "Shows you the days left until a certain date. Date can be set with a settings app and is written to a file.", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "allow_emulator": false, - "storage": [ - {"name":"daysl.app.js","url":"app.js"}, - {"name":"daysl.img","url":"app-icon.js","evaluate":true}, - {"name":"daysl.wid.js","url":"widget.js"} - ] - }, - { - "id": "wclock", - "name": "Word Clock", - "version": "0.03", - "description": "Display Time as Text", - "icon": "clock-word.png", - "screenshots": [{"url":"screenshot_word.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"wclock.app.js","url":"clock-word.js"}, - {"name":"wclock.img","url":"clock-word-icon.js","evaluate":true} - ] - }, - { - "id": "fontclock", - "name": "Font Clock", - "version": "0.01", - "description": "Choose the font and design of clock face from a library of available designs", - "icon": "fontclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "custom": "custom.html", - "allow_emulator": false, - "storage": [ - {"name":"fontclock.app.js","url":"fontclock.js"}, - {"name":"fontclock.img","url":"fontclock-icon.js","evaluate":true}, - {"name":"fontclock.hand.js","url":"fontclock.hand.js"}, - {"name":"fontclock.thinhand.js","url":"fontclock.thinhand.js"}, - {"name":"fontclock.thickhand.js","url":"fontclock.thickhand.js"}, - {"name":"fontclock.hourscriber.js","url":"fontclock.hourscriber.js"}, - {"name":"fontclock.font.js","url":"fontclock.font.js"}, - {"name":"fontclock.font.abril_ff50.js","url":"fontclock.font.abril_ff50.js"}, - {"name":"fontclock.font.cpstc58.js","url":"fontclock.font.cpstc58.js"}, - {"name":"fontclock.font.mntn25.js","url":"fontclock.font.mntn25.js"}, - {"name":"fontclock.font.mntn50.js","url":"fontclock.font.mntn50.js"}, - {"name":"fontclock.font.vector25.js","url":"fontclock.font.vector25.js"}, - {"name":"fontclock.font.vector50.js","url":"fontclock.font.vector50.js"} - ] - }, - { - "id": "slidingtext", - "name": "Sliding Clock", - "version": "0.07", - "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported", - "icon": "slidingtext.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "custom": "custom.html", - "allow_emulator": false, - "storage": [ - {"name":"slidingtext.app.js","url":"slidingtext.js"}, - {"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true}, - {"name":"slidingtext.locale.en.js","url":"slidingtext.locale.en.js"}, - {"name":"slidingtext.locale.en2.js","url":"slidingtext.locale.en2.js"}, - {"name":"slidingtext.utils.en.js","url":"slidingtext.utils.en.js"}, - {"name":"slidingtext.locale.es.js","url":"slidingtext.locale.es.js"}, - {"name":"slidingtext.locale.fr.js","url":"slidingtext.locale.fr.js"}, - {"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"}, - {"name":"slidingtext.locale.de.js","url":"slidingtext.locale.de.js"}, - {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} - ] - }, - { - "id": "solarclock", - "name": "Solar Clock", - "version": "0.02", - "description": "Using your current or chosen location the solar watch face shows the Sun's sky position, time and date. Also allows you to wind backwards and forwards in time to see the sun's position", - "icon": "solar_clock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "custom": "custom.html", - "allow_emulator": false, - "storage": [ - {"name":"solarclock.app.js","url":"solar_clock.js"}, - {"name":"solarclock.img","url":"solar_clock-icon.js","evaluate":true}, - {"name":"solar_colors.js","url":"solar_colors.js"}, - {"name":"solar_controller.js","url":"solar_controller.js"}, - {"name":"solar_date_utils.js","url":"solar_date_utils.js"}, - {"name":"solar_graphic_utils.js","url":"solar_graphic_utils.js"}, - {"name":"solar_location.js","url":"solar_location.js"}, - {"name":"solar_math_utils.js","url":"solar_math_utils.js"}, - {"name":"solar_loc.Reykjavik.json","url":"solar_loc.Reykjavik.json"}, - {"name":"solar_loc.Hong_Kong.json","url":"solar_loc.Hong_Kong.json"}, - {"name":"solar_loc.Honolulu.json","url":"solar_loc.Honolulu.json"}, - {"name":"solar_loc.Rio.json","url":"solar_loc.Rio.json"}, - {"name":"solar_loc.Tokyo.json","url":"solar_loc.Tokyo.json"}, - {"name":"solar_loc.Seoul.json","url":"solar_loc.Seoul.json"} - ] - }, - { - "id": "sweepclock", - "name": "Sweep Clock", - "version": "0.04", - "description": "Smooth sweep secondhand with single hour numeral. Use button 1 to toggle the numeral font, button 3 to change the colour theme and button 4 to change the date placement", - "icon": "sweepclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-sweep-clock-screenshot.png"}], - "storage": [ - {"name":"sweepclock.app.js","url":"sweepclock.js"}, - {"name":"sweepclock.img","url":"sweepclock-icon.js","evaluate":true} - ] - }, - { - "id": "matrixclock", - "name": "Matrix Clock", - "version": "0.02", - "description": "inspired by The Matrix, a clock of the same style", - "icon": "matrixclock.png", - "screenshots": [{"url":"screenshot_matrix.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"matrixclock.app.js","url":"matrixclock.js"}, - {"name":"matrixclock.img","url":"matrixclock-icon.js","evaluate":true} - ] - }, - { - "id": "mandlebrotclock", - "name": "Mandlebrot Clock", - "version": "0.01", - "description": "A mandlebrot set themed clock cool", - "icon": "mandlebrotclock.png", - "screenshots": [{ "url": "screenshot_mandlebrotclock.png" }], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - { "name": "mandlebrotclock.app.js", "url": "mandlebrotclock.js" }, - { - "name": "mandlebrotclock.img", - "url": "mandlebrotclock-icon.js", - "evaluate": true - } - ] - }, - { - "id": "imgclock", - "name": "Image background clock", - "shortName": "Image Clock", - "version": "0.08", - "description": "A clock with an image as a background", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "custom": "custom.html", - "storage": [ - {"name":"imgclock.app.js","url":"app.js"}, - {"name":"imgclock.img","url":"app-icon.js","evaluate":true}, - {"name":"imgclock.face.img"}, - {"name":"imgclock.face.json"}, - {"name":"imgclock.face.bg","content":""} - ] - }, - { - "id": "impwclock", - "name": "Imprecise Word Clock", - "version": "0.03", - "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", - "icon": "clock-impword.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-impercise-word-clock-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"impwclock.app.js","url":"clock-impword.js"}, - {"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true} - ] - }, - { - "id": "aclock", - "name": "Analog Clock", - "version": "0.15", - "description": "An Analog Clock", - "icon": "clock-analog.png", - "screenshots": [{"url":"screenshot_analog.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"aclock.app.js","url":"clock-analog.js"}, - {"name":"aclock.img","url":"clock-analog-icon.js","evaluate":true} - ] - }, - { - "id": "clock2x3", - "name": "2x3 Pixel Clock", - "version": "0.05", - "description": "This is a simple clock using minimalist 2x3 pixel numerical digits", - "icon": "clock2x3.png", - "screenshots": [{"url":"screenshot_pixel.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"clock2x3.app.js","url":"clock2x3-app.js"}, - {"name":"clock2x3.img","url":"clock2x3-icon.js","evaluate":true} - ] - }, - { - "id": "geissclk", - "name": "Geiss Clock", - "version": "0.03", - "description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation", - "icon": "clock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"geissclk.app.js","url":"clock.js"}, - {"name":"geissclk.precompute.js","url":"precompute.js"}, - {"name":"geissclk.img","url":"clock-icon.js","evaluate":true} - ], - "data": [{"name":"geissclk.0.map"},{"name":"geissclk.1.map"},{"name":"geissclk.2.map"},{"name":"geissclk.3.map"},{"name":"geissclk.4.map"},{"name":"geissclk.5.map"},{"name":"geissclk.0.pal"},{"name":"geissclk.1.pal"},{"name":"geissclk.2.pal"}] - }, - { - "id": "trex", - "name": "T-Rex", - "version": "0.04", - "description": "T-Rex game in the style of Chrome's offline game", - "icon": "trex.png", - "screenshots": [{"url":"screenshot_trex.png"}], - "tags": "game", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"trex.app.js","url":"trex.js"}, - {"name":"trex.img","url":"trex-icon.js","evaluate":true}, - {"name":"trex.settings.js","url":"settings.js"} - ], - "data": [{"name":"trex.score","storageFile":true}] - }, - { - "id": "cubescramble", - "name": "Cube Scramble", - "version":"0.04", - "description": "A random scramble generator for the 3x3 Rubik's cube with a basic timer", - "icon": "cube-scramble.png", - "tags": "", - "supports" : ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle2-cube-scramble-screenshot.png"},{"url":"bangle1-cube-scramble-screenshot.png"}], - "storage": [ - {"name":"cubescramble.app.js","url":"cube-scramble.js"}, - {"name":"cubescramble.img","url":"cube-scramble-icon.js","evaluate":true} - ] - }, - { - "id": "astroid", - "name": "Asteroids!", - "version": "0.03", - "description": "Retro asteroids game", - "icon": "asteroids.png", - "screenshots": [{"url":"screenshot_asteroids.png"}], - "tags": "game", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"astroid.app.js","url":"asteroids.js"}, - {"name":"astroid.img","url":"asteroids-icon.js","evaluate":true} - ] - }, - { - "id": "clickms", - "name": "Click Master", - "version": "0.01", - "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", - "icon": "click-master.png", - "tags": "game", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"clickms.app.js","url":"click-master.js"}, - {"name":"clickms.img","url":"click-master-icon.js","evaluate":true} - ] - }, - { - "id": "horsey", - "name": "Horse Race!", - "version": "0.01", - "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", - "icon": "horse-race.png", - "tags": "game", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"horsey.app.js","url":"horse-race.js"}, - {"name":"horsey.img","url":"horse-race-icon.js","evaluate":true} - ] - }, - { - "id": "compass", - "name": "Compass", - "version": "0.05", - "description": "Simple compass that points North", - "icon": "compass.png", - "screenshots": [{"url":"screenshot_compass.png"}], - "tags": "tool,outdoors", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"compass.app.js","url":"compass.js"}, - {"name":"compass.img","url":"compass-icon.js","evaluate":true} - ] - }, - { - "id": "gpstime", - "name": "GPS Time", - "version": "0.05", - "description": "Update the Bangle.js's clock based on the time from the GPS receiver", - "icon": "gpstime.png", - "tags": "tool,gps", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"gpstime.app.js","url":"gpstime.js"}, - {"name":"gpstime.img","url":"gpstime-icon.js","evaluate":true} - ] - }, - { - "id": "openloc", - "name": "Open Location / Plus Codes", - "shortName": "Open Location", - "version": "0.01", - "description": "Convert your current GPS location to a series of characters", - "icon": "app.png", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"openloc.app.js","url":"app.js"}, - {"name":"openloc.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "speedo", - "name": "Speedo", - "version": "0.05", - "description": "Show the current speed according to the GPS", - "icon": "speedo.png", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"speedo.app.js","url":"speedo.js"}, - {"name":"speedo.img","url":"speedo-icon.js","evaluate":true} - ] - }, - { - "id": "gpsrec", - "name": "GPS Recorder", - "version": "0.26", - "description": "Application that allows you to record a GPS track. Can run in background", - "icon": "app.png", - "tags": "tool,outdoors,gps,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"gpsrec.app.js","url":"app.js"}, - {"name":"gpsrec.img","url":"app-icon.js","evaluate":true}, - {"name":"gpsrec.wid.js","url":"widget.js"}, - {"name":"gpsrec.settings.js","url":"settings.js"} - ], - "data": [{"name":"gpsrec.json"},{"wildcard":".gpsrc?","storageFile":true}] - }, - { - "id": "recorder", - "name": "Recorder (BETA)", - "shortName": "Recorder", - "version": "0.04", - "description": "Record GPS position, heart rate and more in the background, then download to your PC.", - "icon": "app.png", - "tags": "tool,outdoors,gps,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"recorder.app.js","url":"app.js"}, - {"name":"recorder.img","url":"app-icon.js","evaluate":true}, - {"name":"recorder.wid.js","url":"widget.js"}, - {"name":"recorder.settings.js","url":"settings.js"} - ], - "data": [{"name":"recorder.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] - }, - { - "id": "gpsnav", - "name": "GPS Navigation", - "version": "0.05", - "description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor", - "icon": "icon.png", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "waypoints.html", - "storage": [ - {"name":"gpsnav.app.js","url":"app.min.js"}, - {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] - }, - { - "id": "heart", - "name": "Heart Rate Recorder", - "shortName": "HRM Record", - "version": "0.07", - "description": "Application that allows you to record your heart rate. Can run in background", - "icon": "app.png", - "tags": "tool,health,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "interface": "interface.html", - "storage": [ - {"name":"heart.app.js","url":"app.js"}, - {"name":"heart.img","url":"app-icon.js","evaluate":true}, - {"name":"heart.wid.js","url":"widget.js"} - ], - "data": [{"name":"heart.json"},{"wildcard":".heart?","storageFile":true}] - }, - { - "id": "slevel", - "name": "Spirit Level", - "version": "0.02", - "description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat", - "icon": "spiritlevel.png", - "tags": "tool", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"slevel.app.js","url":"spiritlevel.js"}, - {"name":"slevel.img","url":"spiritlevel-icon.js","evaluate":true} - ] - }, - { - "id": "files", - "name": "App Manager", - "version": "0.07", - "description": "Show currently installed apps, free space, and allow their deletion from the watch", - "icon": "files.png", - "tags": "tool,system,files", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"files.app.js","url":"files.js"}, - {"name":"files.img","url":"files-icon.js","evaluate":true} - ] - }, - { - "id": "weather", - "name": "Weather", - "version": "0.11", - "description": "Show Gadgetbridge weather report", - "icon": "icon.png", - "screenshots": [{"url":"screenshot.png"}], - "tags": "widget,outdoors", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "readme.md", - "storage": [ - {"name":"weather.app.js","url":"app.js"}, - {"name":"weather.wid.js","url":"widget.js"}, - {"name":"weather","url":"lib.js"}, - {"name":"weather.img","url":"icon.js","evaluate":true}, - {"name":"weather.settings.js","url":"settings.js"} - ], - "data": [{"name":"weather.json"}] - }, - { - "id": "chargeanim", - "name": "Charge Animation", - "version": "0.02", - "description": "When charging, show a sideways charging animation and keep the screen on. When removed from the charger load the clock again.", - "icon": "icon.png", - "tags": "battery", - "supports": ["BANGLEJS", "BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle2-charge-animation-screenshot.png"},{"url":"bangle-charge-animation-screenshot.png"}], - "storage": [ - {"name":"chargeanim.app.js","url":"app.js"}, - {"name":"chargeanim.boot.js","url":"boot.js"}, - {"name":"chargeanim.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "bluetoothdock", - "name": "Bluetooth Dock", - "shortName": "Dock", - "version": "0.01", - "description": "When charging shows the time, scans Bluetooth for known devices (eg temperature) and shows them on the screen", - "icon": "app.png", - "tags": "bluetooth", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"bluetoothdock.app.js","url":"app.js"}, - {"name":"bluetoothdock.boot.js","url":"boot.js"}, - {"name":"bluetoothdock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widbat", - "name": "Battery Level Widget", - "version": "0.09", - "description": "Show the current battery level and charging status in the top right of the clock", - "icon": "widget.png", - "type": "widget", - "tags": "widget,battery", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widbat.wid.js","url":"widget.js"} - ] - }, - { - "id": "widbatv", - "name": "Battery Level Widget (Vertical)", - "version": "0.01", - "description": "Slim, vertical battery widget that only takes up 14px", - "icon": "widget.png", - "type": "widget", - "tags": "widget,battery", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widbatv.wid.js","url":"widget.js"} - ] - }, - { - "id": "widlock", - "name": "Lock Widget", - "version": "0.03", - "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", - "icon": "widget.png", - "type": "widget", - "tags": "widget,lock", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widlock.wid.js","url":"widget.js"} - ] - }, - { - "id": "widbatpc", - "name": "Battery Level Widget (with percentage)", - "shortName": "Battery Widget", - "version": "0.13", - "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", - "icon": "widget.png", - "type": "widget", - "tags": "widget,battery", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"widbatpc.wid.js","url":"widget.js"}, - {"name":"widbatpc.settings.js","url":"settings.js"} - ], - "data": [{"name":"widbatpc.json"}] - }, - { - "id": "widbatwarn", - "name": "Battery Warning", - "shortName": "Battery Warning", - "version": "0.02", - "description": "Show a warning when the battery runs low.", - "icon": "widget.png", - "screenshots": [{"url":"screenshot.png"}], - "type": "widget", - "tags": "tool,battery", - "supports": ["BANGLEJS"], - "dependencies": {"notify":"type"}, - "readme": "README.md", - "storage": [ - {"name":"widbatwarn.wid.js","url":"widget.js"}, - {"name":"widbatwarn.settings.js","url":"settings.js"} - ], - "data": [{"name":"widbatwarn.json"}] - }, - { - "id": "widbt", - "name": "Bluetooth Widget", - "version": "0.07", - "description": "Show the current Bluetooth connection status in the top right of the clock", - "icon": "widget.png", - "type": "widget", - "tags": "widget,bluetooth", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widbt.wid.js","url":"widget.js"} - ] - }, - { - "id": "widchime", - "name": "Hour Chime", - "version": "0.02", - "description": "Buzz or beep on every whole hour.", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widchime.wid.js","url":"widget.js"}, - {"name":"widchime.settings.js","url":"settings.js"} - ], - "data": [{"name":"widchime.json"}] - }, - { - "id": "widram", - "name": "RAM Widget", - "shortName": "RAM Widget", - "version": "0.01", - "description": "Display your Bangle's available RAM percentage in a widget", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widram.wid.js","url":"widget.js"} - ] - }, - { - "id": "hrm", - "name": "Heart Rate Monitor", - "version": "0.06", - "description": "Measure your heart rate and see live sensor data", - "icon": "heartrate.png", - "tags": "health", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"hrm.app.js","url":"heartrate.js"}, - {"name":"hrm.img","url":"heartrate-icon.js","evaluate":true} - ] - }, - { - "id": "widhrm", - "name": "Simple Heart Rate widget", - "version": "0.05", - "description": "When the screen is on, the widget turns on the heart rate monitor and displays the current heart rate (or last known in grey). For this to work well you'll need at least a 15 second LCD Timeout.", - "icon": "widget.png", - "type": "widget", - "tags": "health,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widhrm.wid.js","url":"widget.js"} - ] - }, - { - "id": "bthrm", - "name": "Bluetooth Heart Rate Monitor", - "shortName": "BT HRM", - "version": "0.01", - "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", - "icon": "app.png", - "type": "boot", - "tags": "health,bluetooth", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"bthrm.boot.js","url":"boot.js"}, - {"name":"bthrm.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "stetho", - "name": "Stethoscope", - "version": "0.01", - "description": "Hear your heart rate", - "icon": "stetho.png", - "tags": "health", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"stetho.app.js","url":"stetho.js"}, - {"name":"stetho.img","url":"stetho-icon.js","evaluate":true} - ] - }, - { - "id": "swatch", - "name": "Stopwatch", - "version": "0.07", - "description": "Simple stopwatch with Lap Time logging to a JSON file", - "icon": "stopwatch.png", - "tags": "health", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "interface.html", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-stopwatch-screenshot.png"}], - "storage": [ - {"name":"swatch.app.js","url":"stopwatch.js"}, - {"name":"swatch.img","url":"stopwatch-icon.js","evaluate":true} - ] - }, - { - "id": "hidmsic", - "name": "Bluetooth Music Controls", - "shortName": "Music Control", - "version": "0.02", - "description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!", - "icon": "hid-music.png", - "tags": "bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"hidmsic.app.js","url":"hid-music.js"}, - {"name":"hidmsic.img","url":"hid-music-icon.js","evaluate":true} - ] - }, - { - "id": "hidkbd", - "name": "Bluetooth Keyboard", - "shortName": "Bluetooth Kbd", - "version": "0.02", - "description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps", - "icon": "hid-keyboard.png", - "tags": "bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"hidkbd.app.js","url":"hid-keyboard.js"}, - {"name":"hidkbd.img","url":"hid-keyboard-icon.js","evaluate":true} - ] - }, - { - "id": "hidbkbd", - "name": "Binary Bluetooth Keyboard", - "shortName": "Binary BT Kbd", - "version": "0.02", - "description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want", - "icon": "hid-binary-keyboard.png", - "tags": "bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"hidbkbd.app.js","url":"hid-binary-keyboard.js"}, - {"name":"hidbkbd.img","url":"hid-binary-keyboard-icon.js","evaluate":true} - ] - }, - { - "id": "animals", - "name": "Animals Game", - "version": "0.01", - "description": "Simple toddler's game - displays a different number of animals each time the screen is pressed", - "icon": "animals.png", - "tags": "game", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"animals.app.js","url":"animals.js"}, - {"name":"animals.img","url":"animals-icon.js","evaluate":true}, - {"name":"animals-snake.img","url":"animals-snake.js","evaluate":true}, - {"name":"animals-duck.img","url":"animals-duck.js","evaluate":true}, - {"name":"animals-swan.img","url":"animals-swan.js","evaluate":true}, - {"name":"animals-fox.img","url":"animals-fox.js","evaluate":true}, - {"name":"animals-camel.img","url":"animals-camel.js","evaluate":true}, - {"name":"animals-pig.img","url":"animals-pig.js","evaluate":true}, - {"name":"animals-sheep.img","url":"animals-sheep.js","evaluate":true}, - {"name":"animals-mouse.img","url":"animals-mouse.js","evaluate":true} - ] - }, - { - "id": "qrcode", - "name": "Custom QR Code", - "version": "0.02", - "description": "Use this to upload a customised QR code to Bangle.js", - "icon": "app.png", - "tags": "qrcode", - "supports": ["BANGLEJS","BANGLEJS2"], - "custom": "custom.html", - "customConnect": true, - "storage": [ - {"name":"qrcode.app.js"}, - {"name":"qrcode.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "beer", - "name": "Beer Compass", - "version": "0.01", - "description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "custom": "custom.html", - "storage": [ - {"name":"beer.app.js"}, - {"name":"beer.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "route", - "name": "Route Viewer", - "version": "0.02", - "description": "Upload a KML file of a route, and have your watch display a map with how far around it you are", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "custom": "custom.html", - "storage": [ - {"name":"route.app.js"}, - {"name":"route.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "ncstart", - "name": "NCEU Startup", - "version": "0.06", - "description": "NodeConfEU 2019 'First Start' Sequence", - "icon": "start.png", - "tags": "start,welcome", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"ncstart.app.js","url":"start.js"}, - {"name":"ncstart.boot.js","url":"boot.js"}, - {"name":"ncstart.settings.js","url":"settings.js"}, - {"name":"ncstart.img","url":"start-icon.js","evaluate":true}, - {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, - {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, - {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true}, - {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true}, - {"name":"nc-tf.img","url":"start-tf.js","evaluate":true} - ], - "data": [{"name":"ncstart.json"}] - }, - { - "id": "ncfrun", - "name": "NCEU 5K Fun Run", - "version": "0.01", - "description": "Display a map of the NodeConf EU 2019 5K Fun Run route and your location on it", - "icon": "nceu-funrun.png", - "tags": "health", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"ncfrun.app.js","url":"nceu-funrun.js"}, - {"name":"ncfrun.img","url":"nceu-funrun-icon.js","evaluate":true} - ] - }, - { - "id": "widnceu", - "name": "NCEU Logo Widget", - "version": "0.02", - "description": "Show the NodeConf EU logo in the top left", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"widnceu.wid.js","url":"widget.js"} - ] - }, - { - "id": "sclock", - "name": "Simple Clock", - "version": "0.07", - "description": "A Simple Digital Clock", - "icon": "clock-simple.png", - "screenshots": [{"url":"screenshot_simplec.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"sclock.app.js","url":"clock-simple.js"}, - {"name":"sclock.img","url":"clock-simple-icon.js","evaluate":true} - ] - }, - { - "id": "s7clk", - "name": "Simple 7 segment Clock", - "version": "0.03", - "description": "A simple 7 segment Clock with date", - "icon": "icon.png", - "screenshots": [{"url":"screenshot_s7segment.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"s7clk.app.js","url":"app.js"}, - {"name":"s7clk.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "vibrclock", - "name": "Vibrate Clock", - "version": "0.03", - "description": "When BTN1 is pressed, vibrate out the time as a series of buzzes, one digit at a time. Hours, then Minutes. Zero is signified by one long buzz. Otherwise a simple digital clock.", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-vibrate-clock-screenshot.png"}], - "storage": [ - {"name":"vibrclock.app.js","url":"app.js"}, - {"name":"vibrclock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "svclock", - "name": "Simple V-Clock", - "version": "0.04", - "description": "Modification of Simple Clock 0.04 to use Vectorfont", - "icon": "vclock-simple.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle2-simple-v-clock-screenshot.png"}], - "storage": [ - {"name":"svclock.app.js","url":"vclock-simple.js"}, - {"name":"svclock.img","url":"vclock-simple-icon.js","evaluate":true} - ] - }, - { - "id": "dclock", - "name": "Dev Clock", - "version": "0.10", - "description": "A Digital Clock including timestamp (tst), beats(@), days in current month (dm) and days since new moon (l)", - "icon": "clock-dev.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle2-dev-clock-screenshot.png"},{"url":"bangle1-dev-clock-screenshot.png"}], - "storage": [ - {"name":"dclock.app.js","url":"clock-dev.js"}, - {"name":"dclock.img","url":"clock-dev-icon.js","evaluate":true} - ] - }, - { - "id": "gesture", - "name": "Gesture Test", - "version": "0.01", - "description": "BETA! Uploads a basic Tensorflow Gesture model, and then outputs each gesture as a message", - "icon": "gesture.png", - "type": "app", - "tags": "gesture,ai", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"gesture.app.js","url":"gesture.js"}, - {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, - {"name":".tfmodel","url":"gesture-tfmodel.js","evaluate":true}, - {"name":"gesture.img","url":"gesture-icon.js","evaluate":true} - ] - }, - { - "id": "pparrot", - "name": "Party Parrot", - "version": "0.01", - "description": "Party with a parrot on your wrist", - "icon": "party-parrot.png", - "type": "app", - "tags": "party,parrot,lol", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-party-parrot-screenshot.png"}], - "storage": [ - {"name":"pparrot.app.js","url":"party-parrot.js"}, - {"name":"pparrot.img","url":"party-parrot-icon.js","evaluate":true} - ] - }, - { - "id": "hrings", - "name": "Hypno Rings", - "version": "0.01", - "description": "Experiment with trippy rings, press buttons for change", - "icon": "hypno-rings.png", - "type": "app", - "tags": "rings,hypnosis,psychadelic", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-hypno-rings-screenshot.png"}], - "storage": [ - {"name":"hrings.app.js","url":"hypno-rings.js"}, - {"name":"hrings.img","url":"hypno-rings-icon.js","evaluate":true} - ] - }, - { - "id": "morse", - "name": "Morse Code", - "version": "0.01", - "description": "Learn morse code by hearing/seeing/feeling the code. Tap to toggle buzz!", - "icon": "morse-code.png", - "type": "app", - "tags": "morse,sound,visual,input", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"morse.app.js","url":"morse-code.js"}, - {"name":"morse.img","url":"morse-code-icon.js","evaluate":true} - ] - }, - { - "id": "blescan", - "name": "BLE Scanner", - "version": "0.01", - "description": "Scan for advertising BLE devices", - "icon": "blescan.png", - "tags": "bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"blescan.app.js","url":"blescan.js"}, - {"name":"blescan.img","url":"blescan-icon.js","evaluate":true} - ] - }, - { - "id": "mmonday", - "name": "Manic Monday Tone", - "version": "0.02", - "description": "The Bangles make a comeback", - "icon": "manic-monday-icon.png", - "tags": "sound", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"mmonday.app.js","url":"manic-monday.js"}, - {"name":"mmonday.img","url":"manic-monday-icon.js","evaluate":true} - ] - }, - { - "id": "jbells", - "name": "Jingle Bells", - "version": "0.01", - "description": "Play Jingle Bells", - "icon": "jbells.png", - "type": "app", - "tags": "sound", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"jbells.app.js","url":"jbells.js"}, - {"name":"jbells.img","url":"jbells-icon.js","evaluate":true} - ] - }, - { - "id": "scolor", - "name": "Show Color", - "version": "0.01", - "description": "Display all available Colors and Names", - "icon": "show-color.png", - "type": "app", - "tags": "tool", - "screenshots": [{"url":"bangle1-view-color-screenshot.png"}], - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"scolor.app.js","url":"show-color.js"}, - {"name":"scolor.img","url":"show-color-icon.js","evaluate":true} - ] - }, - { - "id": "miclock", - "name": "Mixed Clock", - "version": "0.05", - "description": "A mix of analog and digital Clock", - "icon": "clock-mixed.png", - "type": "clock", - "tags": "clock", - "screenshots": [{"url":"bangle1-mixed-clock-screenshot.png"}], - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"miclock.app.js","url":"clock-mixed.js"}, - {"name":"miclock.img","url":"clock-mixed-icon.js","evaluate":true} - ] - }, - { - "id": "bclock", - "name": "Binary Clock", - "version": "0.03", - "description": "A simple binary clock watch face", - "icon": "clock-binary.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-binary-clock-screenshot.png"}], - "storage": [ - {"name":"bclock.app.js","url":"clock-binary.js"}, - {"name":"bclock.img","url":"clock-binary-icon.js","evaluate":true} - ] - }, - { - "id": "clotris", - "name": "Clock-Tris", - "version": "0.01", - "description": "A fully functional clone of a classic game of falling blocks", - "icon": "clock-tris.png", - "tags": "game", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-clock-tris-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"clotris.app.js","url":"clock-tris.js"}, - {"name":"clotris.img","url":"clock-tris-icon.js","evaluate":true}, - {"name":".trishig","url":"clock-tris-high"} - ] - }, - { - "id": "flappy", - "name": "Flappy Bird", - "version": "0.05", - "description": "A Flappy Bird game clone", - "icon": "app.png", - "screenshots": [{"url":"screenshot1_flappy.png"},{"url":"screenshot2_flappy.png"}], - "tags": "game", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"flappy.app.js","url":"app.js"}, - {"name":"flappy.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "gpsinfo", - "name": "GPS Info", - "version": "0.05", - "description": "An application that displays information about altitude, lat/lon, satellites and time", - "icon": "gps-info.png", - "type": "app", - "tags": "gps", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"gpsinfo.app.js","url":"gps-info.js"}, - {"name":"gpsinfo.img","url":"gps-info-icon.js","evaluate":true} - ] - }, - { - "id": "assistedgps", - "name": "Assisted GPS Update (AGPS)", - "version": "0.01", - "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", - "icon": "app.png", - "type": "RAM", - "tags": "tool,outdoors,agps", - "supports": ["BANGLEJS"], - "custom": "custom.html", - "storage": [] - }, - { - "id": "pomodo", - "name": "Pomodoro", - "version": "0.02", - "description": "A simple pomodoro timer.", - "icon": "pomodoro.png", - "type": "app", - "tags": "pomodoro,cooking,tools", - "supports": ["BANGLEJS", "BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle2-pomodoro-screenshot.png"}], - "storage": [ - {"name":"pomodo.app.js","url":"pomodoro.js"}, - {"name":"pomodo.img","url":"pomodoro-icon.js","evaluate":true} - ] - }, - { - "id": "blobclk", - "name": "Large Digit Blob Clock", - "shortName": "Blob Clock", - "version": "0.06", - "description": "A clock with big digits", - "icon": "clock-blob.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle2-large-digit-blob-clock-screenshot.png"},{"url":"bangle1-large-digit-blob-clock-screenshot.png"}], - "storage": [ - {"name":"blobclk.app.js","url":"clock-blob.js"}, - {"name":"blobclk.img","url":"clock-blob-icon.js","evaluate":true} - ] - }, - { - "id": "boldclk", - "name": "Bold Clock", - "version": "0.05", - "description": "Simple, readable and practical clock", - "icon": "bold_clock.png", - "screenshots": [{"url":"screenshot_bold.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"boldclk.app.js","url":"bold_clock.js"}, - {"name":"boldclk.img","url":"bold_clock-icon.js","evaluate":true} - ] - }, - { - "id": "widclk", - "name": "Digital clock widget", - "version": "0.06", - "description": "A simple digital clock widget", - "icon": "widget.png", - "type": "widget", - "tags": "widget,clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widclk.wid.js","url":"widget.js"} - ] - }, - { - "id": "widpedom", - "name": "Pedometer widget", - "version": "0.19", - "description": "Daily pedometer widget", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widpedom.wid.js","url":"widget.js"}, - {"name":"widpedom.settings.js","url":"settings.js"} - ] - }, - { - "id": "berlinc", - "name": "Berlin Clock", - "version": "0.05", - "description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)", - "icon": "berlin-clock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"berlin-clock-screenshot.png"}], - "storage": [ - {"name":"berlinc.app.js","url":"berlin-clock.js"}, - {"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true} - ] - }, - { - "id": "ctrclk", - "name": "Centerclock", - "version": "0.03", - "description": "Watch-centered digital 24h clock with date in dd.mm.yyyy format.", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-center-clock-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"ctrclk.app.js","url":"app.js"}, - {"name":"ctrclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "demoapp", - "name": "Demo Loop", - "version": "0.02", - "description": "Simple demo app - displays Bangle.js, JS logo, graphics, and Bangle.js information", - "icon": "app.png", - "type": "app", - "tags": "", - "screenshots": [{"url":"bangle1-demo-loop-screenshot1.png"},{"url":"bangle1-demo-loop-screenshot2.png"},{"url":"bangle1-demo-loop-screenshot3.png"},{"url":"bangle1-demo-loop-screenshot4.png"}], - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"demoapp.app.js","url":"app.js"}, - {"name":"demoapp.img","url":"app-icon.js","evaluate":true} - ], - "sortorder": -9 - }, - { - "id": "flagrse", - "name": "Espruino Flag Raiser", - "version": "0.01", - "description": "App to send a command to another Espruino to cause it to raise a flag", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"flagrse.app.js","url":"app.js"}, - {"name":"flagrse.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "pipboy", - "name": "Pipboy", - "version": "0.04", - "description": "Pipboy themed clock", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-pipboy-themed-clock-screenshot.png"}], - "storage": [ - {"name":"pipboy.app.js","url":"app.js"}, - {"name":"pipboy.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "torch", - "name": "Torch", - "shortName": "Torch", - "version": "0.02", - "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets", - "icon": "app.png", - "tags": "tool,torch", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"torch.app.js","url":"app.js"}, - {"name":"torch.wid.js","url":"widget.js"}, - {"name":"torch.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "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", - "icon": "app.png", - "tags": "tool,torch", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"rtorch.app.js","url":"app.js"}, - {"name":"rtorch.wid.js","url":"widget.js"}, - {"name":"rtorch.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "wohrm", - "name": "Workout HRM", - "version": "0.08", - "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", - "icon": "app.png", - "type": "app", - "tags": "hrm,workout", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}], - "storage": [ - {"name":"wohrm.app.js","url":"app.js"}, - {"name":"wohrm.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widid", - "name": "Bluetooth ID Widget", - "version": "0.03", - "description": "Display the last two tuple of your Bangle.js MAC address in the widget section. This is useful for figuring out which Bangle.js to connect to if you have more than one Bangle.js!", - "icon": "widget.png", - "type": "widget", - "tags": "widget,address,mac", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widid.wid.js","url":"widget.js"} - ] - }, - { - "id": "grocery", - "name": "Grocery", - "version": "0.02", - "description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.", - "icon": "grocery.png", - "type": "app", - "tags": "tool,outdoors,shopping,list", - "supports": ["BANGLEJS"], - "custom": "grocery.html", - "storage": [ - {"name":"grocery.app.js","url":"app.js"}, - {"name":"grocery.img","url":"grocery-icon.js","evaluate":true} - ] - }, - { - "id": "marioclock", - "name": "Mario Clock", - "version": "0.15", - "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", - "icon": "marioclock.png", - "type": "clock", - "tags": "clock,mario,retro", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": false, - "screenshots": [{"url":"bangle1-mario-clock-screenshot.png"}], - "storage": [ - {"name":"marioclock.app.js","url":"marioclock-app.js"}, - {"name":"marioclock.img","url":"marioclock-icon.js","evaluate":true} - ] - }, - { - "id": "cliock", - "name": "Commandline-Clock", - "shortName": "CLI-Clock", - "version": "0.14", - "description": "Simple CLI-Styled Clock", - "icon": "app.png", - "screenshots": [{"url":"screenshot_cli.png"}], - "type": "clock", - "tags": "clock,cli,command,bash,shell", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"cliock.app.js","url":"app.js"}, - {"name":"cliock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widver", - "name": "Firmware Version Widget", - "version": "0.03", - "description": "Display the version of the installed firmware in the top widget section.", - "icon": "widget.png", - "type": "widget", - "tags": "widget,tool,system", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widver.wid.js","url":"widget.js"} - ] - }, - { - "id": "barclock", - "name": "Bar Clock", - "version": "0.09", - "description": "A simple digital clock showing seconds as a bar", - "icon": "clock-bar.png", - "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"barclock.app.js","url":"clock-bar.js"}, - {"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true} - ] - }, - { - "id": "dotclock", - "name": "Dot Clock", - "version": "0.03", - "description": "A Minimal Dot Analog Clock", - "icon": "clock-dot.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle2-dot-clcok-screenshot.png"},{"url":"bangle1-dot-clock-screenshot.png"}], - "storage": [ - {"name":"dotclock.app.js","url":"clock-dot.js"}, - {"name":"dotclock.img","url":"clock-dot-icon.js","evaluate":true} - ] - }, - { - "id": "widtbat", - "name": "Tiny Battery Widget", - "version": "0.02", - "description": "Tiny blueish battery widget, vibs and changes level color when charging", - "icon": "widget.png", - "type": "widget", - "tags": "widget,tool,system", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widtbat.wid.js","url":"widget.js"} - ] - }, - { - "id": "chrono", - "name": "Chrono", - "shortName": "Chrono", - "version": "0.01", - "description": "Single click BTN1 to add 5 minutes. Single click BTN2 to add 30 seconds. Single click BTN3 to add 5 seconds. Tap to pause or play to timer. Double click BTN1 to reset. When timer finishes the watch vibrates.", - "icon": "chrono.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"chrono.app.js","url":"chrono.js"}, - {"name":"chrono.img","url":"chrono-icon.js","evaluate":true} - ] - }, - { - "id": "astrocalc", - "name": "Astrocalc", - "version": "0.02", - "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", - "icon": "astrocalc.png", - "tags": "app,sun,moon,cycles,tool,outdoors", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"astrocalc.app.js","url":"astrocalc-app.js"}, - {"name":"suncalc.js","url":"suncalc.js"}, - {"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true}, - {"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true}, - {"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true}, - {"name":"waning-crescent.img","url":"waning-crescent-icon.js","evaluate":true}, - {"name":"waning-gibbous.img","url":"waning-gibbous-icon.js","evaluate":true}, - {"name":"full.img","url":"full-icon.js","evaluate":true}, - {"name":"new.img","url":"new-icon.js","evaluate":true}, - {"name":"waxing-gibbous.img","url":"waxing-gibbous-icon.js","evaluate":true}, - {"name":"waxing-crescent.img","url":"waxing-crescent-icon.js","evaluate":true} - ] - }, - { - "id": "widhwt", - "name": "Hand Wash Timer", - "version": "0.01", - "description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.", - "icon": "widget.png", - "type": "widget", - "tags": "widget,tool", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"widhwt.wid.js","url":"widget.js"} - ] - }, - { - "id": "toucher", - "name": "Touch Launcher", - "shortName": "Toucher", - "version": "0.07", - "description": "Touch enable left to right launcher.", - "icon": "app.png", - "type": "launch", - "tags": "tool,system,launcher", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"toucher.app.js","url":"app.js"}, - {"name":"toucher.settings.js","url":"settings.js"} - ], - "data": [{"name":"toucher.json"}] - }, - { - "id": "balltastic", - "name": "Balltastic", - "version": "0.02", - "description": "Simple but fun ball eats dots game.", - "icon": "app.png", - "type": "app", - "tags": "game,fun", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"balltastic.app.js","url":"app.js"}, - {"name":"balltastic.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "rpgdice", - "name": "RPG dice", - "version": "0.02", - "description": "Simple RPG dice rolling app.", - "icon": "rpgdice.png", - "type": "app", - "tags": "game,fun", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-rpg-dice-screenshot.png"}], - "storage": [ - {"name":"rpgdice.app.js","url":"app.js"}, - {"name":"rpgdice.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widmp", - "name": "Moon Phase Widget", - "version": "0.02", - "description": "Display the current moon phase in blueish for the northern hemisphere in eight phases", - "icon": "widget.png", - "type": "widget", - "tags": "widget,tools", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"widmp.wid.js","url":"widget.js"} - ] - }, - { - "id": "minionclk", - "name": "Minion clock", - "version": "0.05", - "description": "Minion themed clock.", - "icon": "minionclk.png", - "type": "clock", - "tags": "clock,minion", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-minion-clock-screenshot.png"}], - "storage": [ - {"name":"minionclk.app.js","url":"app.js"}, - {"name":"minionclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "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", - "icon": "app.png", - "tags": "outdoors,gps", - "supports": ["BANGLEJS","BANGLEJS2"], - "custom": "custom.html", - "customConnect": true, - "storage": [ - {"name":"openstmap","url":"openstmap.js"}, - {"name":"openstmap.app.js","url":"app.js"}, - {"name":"openstmap.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "activepedom", - "name": "Active Pedometer", - "shortName": "Active Pedometer", - "version": "0.09", - "description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.", - "icon": "app.png", - "tags": "outdoors,widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"activepedom.wid.js","url":"widget.js"}, - {"name":"activepedom.settings.js","url":"settings.js"}, - {"name":"activepedom.img","url":"app-icon.js","evaluate":true}, - {"name":"activepedom.app.js","url":"app.js"} - ] - }, - { - "id": "chronowid", - "name": "Chrono Widget", - "shortName": "Chrono Widget", - "version": "0.03", - "description": "Chronometer (timer) which runs as widget.", - "icon": "app.png", - "tags": "tool,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"chronowid.wid.js","url":"widget.js"}, - {"name":"chronowid.app.js","url":"app.js"}, - {"name":"chronowid.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "tabata", - "name": "Tabata", - "shortName": "Tabata - Control High-Intensity Interval Training", - "version": "0.01", - "description": "Control high-intensity interval training (according to tabata: https://en.wikipedia.org/wiki/Tabata_method).", - "icon": "tabata.png", - "tags": "workout,health", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"tabata.app.js","url":"tabata.js"}, - {"name":"tabata.img","url":"tabata-icon.js","evaluate":true} - ] - }, - { - "id": "custom", - "name": "Custom Boot Code ", - "version": "0.01", - "description": "Add code you want to run at boot time", - "icon": "custom.png", - "type": "bootloader", - "tags": "tool,system", - "supports": ["BANGLEJS","BANGLEJS2"], - "custom": "custom.html", - "storage": [ - {"name":"custom"} - ] - }, - { - "id": "devstopwatch", - "name": "Dev Stopwatch", - "shortName": "Dev Stopwatch", - "version": "0.03", - "description": "Stopwatch with 5 laps supported (cyclically replaced)", - "icon": "app.png", - "tags": "stopwatch,chrono,timer,chronometer", - "supports": ["BANGLEJS","BANGLEJS2"], - "screenshots": [{"url":"bangle1-dev-stopwatch-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"devstopwatch.app.js","url":"app.js"}, - {"name":"devstopwatch.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "batchart", - "name": "Battery Chart", - "shortName": "Battery Chart", - "version": "0.10", - "description": "A widget and an app for recording and visualizing battery percentage over time.", - "icon": "app.png", - "tags": "app,widget,battery,time,record,chart,tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"batchart.wid.js","url":"widget.js"}, - {"name":"batchart.app.js","url":"app.js"}, - {"name":"batchart.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "nato", - "name": "NATO Alphabet", - "shortName": "NATOAlphabet", - "version": "0.01", - "description": "Learn the NATO Phonetic alphabet plus some numbers.", - "icon": "nato.png", - "type": "app", - "tags": "app,learn,visual", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-NATO-alphabet-screenshot.png"},{"url":"bangle1-NATO-alphabet-screenshot2.png"}], - "storage": [ - {"name":"nato.app.js","url":"nato.js"}, - {"name":"nato.img","url":"nato-icon.js","evaluate":true} - ] - }, - { - "id": "numerals", - "name": "Numerals Clock", - "shortName": "Numerals Clock", - "version": "0.09", - "description": "A simple big numerals clock", - "icon": "numerals.png", - "type": "clock", - "tags": "numerals,clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-numerals-screenshot.png"}], - "storage": [ - {"name":"numerals.app.js","url":"numerals.app.js"}, - {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, - {"name":"numerals.settings.js","url":"numerals.settings.js"} - ], - "data": [{"name":"numerals.json"}] - }, - { - "id": "bledetect", - "name": "BLE Detector", - "shortName": "BLE Detector", - "version": "0.03", - "description": "Detect BLE devices and show some informations.", - "icon": "bledetect.png", - "tags": "app,bluetooth,tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"bledetect.app.js","url":"bledetect.js"}, - {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} - ] - }, - { - "id": "snake", - "name": "Snake", - "shortName": "Snake", - "version": "0.02", - "description": "The classic snake game. Eat apples and don't bite your tail.", - "icon": "snake.png", - "tags": "game,fun", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"snake.app.js","url":"snake.js"}, - {"name":"snake.img","url":"snake-icon.js","evaluate":true} - ] - }, - { - "id": "calculator", - "name": "Calculator", - "shortName": "Calculator", - "version": "0.04", - "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", - "icon": "calculator.png", - "screenshots": [{"url":"screenshot_calculator.png"}], - "tags": "app,tool", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"calculator.app.js","url":"app.js"}, - {"name":"calculator.img","url":"calculator-icon.js","evaluate":true} - ] - }, - { - "id": "dane", - "name": "Digital Assistant, not EDITH", - "shortName": "DANE", - "version": "0.16", - "description": "A Watchface inspired by Tony Stark's EDITH and based on https://arwes.dev/", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"dane.app.js","url":"app.js"}, - {"name":"dane.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "dane_tcr", - "name": "DANE Touch Launcher", - "shortName": "DANE Toucher", - "version": "0.07", - "description": "Touch enable left to right launcher in the style of the DANE Watchface", - "icon": "app.png", - "type": "launch", - "tags": "tool,system,launcher", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"dane_tcr.app.js","url":"app.js"}, - {"name":"dane_tcr.settings.js","url":"settings.js"} - ], - "data": [{"name":"dane_tcr.json"}] - }, - { - "id": "buffgym", - "name": "BuffGym", - "version": "0.02", - "description": "BuffGym is the famous 5x5 workout program for the BangleJS", - "icon": "buffgym.png", - "type": "app", - "tags": "tool,outdoors,gym,exercise", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "buffgym.html", - "allow_emulator": false, - "storage": [ - {"name":"buffgym.app.js","url":"buffgym.app.js"}, - {"name":"buffgym-set.js","url":"buffgym-set.js"}, - {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, - {"name":"buffgym-workout.js","url":"buffgym-workout.js"}, - {"name":"buffgym-workout-a.json","url":"buffgym-workout-a.json"}, - {"name":"buffgym-workout-b.json","url":"buffgym-workout-b.json"}, - {"name":"buffgym-workout-index.json","url":"buffgym-workout-index.json"}, - {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} - ] - }, - { - "id": "banglerun", - "name": "BangleRun", - "shortName": "BangleRun", - "version": "0.10", - "description": "An app for running sessions. Displays info and logs your run for later viewing.", - "icon": "banglerun.png", - "tags": "run,running,fitness,outdoors", - "supports": ["BANGLEJS"], - "interface": "interface.html", - "allow_emulator": false, - "storage": [ - {"name":"banglerun.app.js","url":"app.js"}, - {"name":"banglerun.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "metronome", - "name": "Metronome", - "version": "0.07", - "readme": "README.md", - "description": "Makes the watch blinking and vibrating with a given rate", - "icon": "metronome_icon.png", - "tags": "tool", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-metronome-screenshot.png"}], - "storage": [ - {"name":"metronome.app.js","url":"metronome.js"}, - {"name":"metronome.img","url":"metronome-icon.js","evaluate":true}, - {"name":"metronome.settings.js","url":"settings.js"} - ] - }, - { - "id": "blackjack", - "name": "Black Jack game", - "shortName": "Black Jack game", - "version": "0.02", - "description": "Simple implementation of card game Black Jack", - "icon": "blackjack.png", - "tags": "game", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-black-jack-game-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"blackjack.app.js","url":"blackjack.app.js"}, - {"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true} - ] - }, - { - "id": "hidcam", - "name": "Camera shutter", - "shortName": "Cam shutter", - "version": "0.03", - "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", - "icon": "app.png", - "tags": "bluetooth,tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"hidcam.app.js","url":"app.js"}, - {"name":"hidcam.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "swlclk", - "name": "SWL Clock / Short Wave Listner Clock", - "shortName": "SWL Clock", - "version": "0.02", - "description": "Display Local, UTC time and some programs on the shorts waves along the day, with the frequencies", - "icon": "swlclk.png", - "type": "clock", - "tags": "tool,clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-SWL-clock-screenshot.png"}], - "storage": [ - {"name":"swlclk.app.js","url":"app.js"}, - {"name":"swlclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "rclock", - "name": "Round clock with seconds, minutes and date", - "shortName": "Round Clock", - "version": "0.06", - "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"rclock.app.js","url":"rclock.app.js"}, - {"name":"rclock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "fclock", - "name": "fclock", - "shortName": "F Clock", - "version": "0.02", - "description": "Simple design of a digital clock", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"fclock.app.js","url":"fclock.app.js"}, - {"name":"fclock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "hamloc", - "name": "QTH Locator / Maidenhead Locator System", - "shortName": "QTH Locator", - "version": "0.01", - "description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators", - "icon": "app.png", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"hamloc.app.js","url":"app.js"}, - {"name":"hamloc.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "osmpoi", - "name": "POI Compass", - "version": "0.03", - "description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.", - "icon": "app.png", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], - "readme": "README.md", - "custom": "custom.html", - "storage": [ - {"name":"osmpoi.app.js"}, - {"name":"osmpoi.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "pong", - "name": "Pong", - "shortName": "Pong", - "version": "0.03", - "description": "A clone of the Atari game Pong", - "icon": "pong.png", - "type": "app", - "tags": "game", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-pong-screenshot.png"}], - "storage": [ - {"name":"pong.app.js","url":"app.js"}, - {"name":"pong.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "ballmaze", - "name": "Ball Maze", - "version": "0.02", - "description": "Navigate a ball through a maze by tilting your watch.", - "icon": "icon.png", - "type": "app", - "tags": "game", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"ballmaze.app.js","url":"app.js"}, - {"name":"ballmaze.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"ballmaze.json"}] - }, - { - "id": "calendar", - "name": "Calendar", - "version": "0.02", - "description": "Simple calendar", - "icon": "calendar.png", - "screenshots": [{"url":"screenshot_calendar.png"}], - "tags": "calendar", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"calendar.app.js","url":"calendar.js"}, - {"name":"calendar.img","url":"calendar-icon.js","evaluate":true} - ] - }, - { - "id": "hidjoystick", - "name": "Bluetooth Joystick", - "shortName": "Joystick", - "version": "0.01", - "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.", - "icon": "app.png", - "tags": "bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"hidjoystick.app.js","url":"app.js"}, - {"name":"hidjoystick.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "largeclock", - "name": "Large Clock", - "version": "0.10", - "description": "A readable and informational digital watch, with date, seconds and moon phase", - "icon": "largeclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-large-clock-screenshot.png"}], - "storage": [ - {"name":"largeclock.app.js","url":"largeclock.js"}, - {"name":"largeclock.img","url":"largeclock-icon.js","evaluate":true}, - {"name":"largeclock.settings.js","url":"settings.js"} - ], - "data": [{"name":"largeclock.json"}] - }, - { - "id": "smtswch", - "name": "Smart Switch", - "shortName": "Smart Switch", - "version": "0.01", - "description": "Using EspruinoHub, control your smart devices on and off via Bluetooth Low Energy!", - "icon": "app.png", - "type": "app", - "tags": "bluetooth,btle,smart,switch", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"smtswch.app.js","url":"app.js"}, - {"name":"smtswch.img","url":"app-icon.js","evaluate":true}, - {"name":"light-on.img","url":"light-on.js","evaluate":true}, - {"name":"light-off.img","url":"light-off.js","evaluate":true}, - {"name":"switch-on.img","url":"switch-on.js","evaluate":true}, - {"name":"switch-off.img","url":"switch-off.js","evaluate":true} - ] - }, - { - "id": "miplant", - "name": "Xiaomi Plant Sensor", - "shortName": "Mi Plant", - "version": "0.02", - "description": "Reads and displays data from Xiaomi bluetooth plant moisture sensors", - "icon": "app.png", - "tags": "xiaomi,mi,plant,ble,bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"miplant.app.js","url":"app.js"}, - {"name":"miplant.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "simpletimer", - "name": "Timer", - "version": "0.07", - "description": "Simple timer, useful when playing board games or cooking", - "icon": "app.png", - "tags": "timer", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-timer-screenshot.png"}], - "storage": [ - {"name":"simpletimer.app.js","url":"app.js"}, - {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, - {"name":".tfmodel","url":"gesture-tfmodel.js","evaluate":true}, - {"name":"simpletimer.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"simpletimer.json"}] - }, - { - "id": "beebclock", - "name": "Beeb Clock", - "version": "0.05", - "description": "Clock face that may be coincidentally familiar to BBC viewers", - "icon": "beebclock.png", - "type": "clock", - "tags": "clock", - "screenshots": [{"url":"bangle1-beeb-clock-screenshot.png"}], - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"beebclock.app.js","url":"beebclock.js"}, - {"name":"beebclock.img","url":"beebclock-icon.js","evaluate":true} - ] - }, - { - "id": "findphone", - "name": "Find Phone", - "shortName": "Find Phone", - "version": "0.03", - "description": "Find your phone via Gadgetbridge. Click any button to let your phone ring. 📳 Note: The functionality is available even without this app, just go to Settings, App Settings, Gadgetbridge, Find Phone.", - "icon": "app.png", - "tags": "tool,android", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"findphone.app.js","url":"app.js"}, - {"name":"findphone.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "getup", - "name": "Get Up", - "shortName": "Get Up", - "version": "0.01", - "description": "Reminds you to getup every x minutes. Sitting to long is dangerous!", - "icon": "app.png", - "tags": "tools,health", - "supports": ["BANGLEJS"], - "readme": "README.md", - "screenshots": [{"url":"bangle1-get-up-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"getup.app.js","url":"app.js"}, - {"name":"getup.settings.js","url":"settings.js"}, - {"name":"getup.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "gallifr", - "name": "Time Traveller's Chronometer", - "shortName": "Time Travel Clock", - "version": "0.02", - "description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.", - "icon": "gallifr.png", - "screenshots": [{"url":"screenshot_time.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"gallifr.app.js","url":"app.js"}, - {"name":"gallifr.img","url":"app-icon.js","evaluate":true}, - {"name":"gallifr.settings.js","url":"settings.js"} - ], - "data": [{"name":"gallifr.json"}] - }, - { - "id": "rndmclk", - "name": "Random Clock Loader", - "version": "0.03", - "description": "Load a different clock whenever the LCD is switched on.", - "icon": "rndmclk.png", - "type": "widget", - "tags": "widget,clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"rndmclk.wid.js","url":"widget.js"} - ] - }, - { - "id": "dotmatrixclock", - "name": "Dotmatrix Clock", - "version": "0.01", - "description": "A clear white-on-blue dotmatrix simulated clock", - "icon": "dotmatrixclock.png", - "type": "clock", - "tags": "clock,dotmatrix,retro", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"dotmatrixclock.app.js","url":"app.js"}, - {"name":"dotmatrixclock.img","url":"dotmatrixclock-icon.js","evaluate":true} - ] - }, - { - "id": "jbm8b", - "name": "Magic 8 Ball", - "shortName": "Magic 8 Ball", - "version": "0.03", - "description": "A simple fortune telling app", - "icon": "app.png", - "tags": "game", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"jbm8b.app.js","url":"app.js"}, - {"name":"jbm8b.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "jbm8b_IT", - "name": "Magic 8 Ball Italiano", - "shortName": "Magic 8 Ball IT", - "version": "0.01", - "description": "La palla predice il futuro", - "icon": "app.png", - "screenshots": [{"url":"bangle1-magic-8-ball-italiano-screenshot.png"}], - "tags": "game", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"jbm8b_IT.app.js","url":"app.js"}, - {"name":"jbm8b_IT.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "BLEcontroller", - "name": "BLE Customisable Controller with Joystick", - "shortName": "BLE Controller", - "version": "0.01", - "description": "A configurable controller for BLE devices and robots, with a basic four direction joystick. Designed to be easy to customise so you can add your own menus.", - "icon": "BLEcontroller.png", - "tags": "tool,bluetooth", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": false, - "storage": [ - {"name":"BLEcontroller.app.js","url":"app.js"}, - {"name":"BLEcontroller.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widviz", - "name": "Widget Visibility Widget", - "shortName": "Viz Widget", - "version": "0.02", - "description": "Swipe left to hide top bar widgets, swipe right to redisplay.", - "icon": "eye.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"widviz.wid.js","url":"widget.js"} - ] - }, - { - "id": "binclock", - "name": "Binary Clock", - "shortName": "Binary Clock", - "version": "0.03", - "description": "A binary clock with hours and minutes. BTN1 toggles a digital clock.", - "icon": "app.png", - "type": "clock", - "tags": "clock,binary", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"binclock.app.js","url":"app.js"}, - {"name":"binclock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "pizzatimer", - "name": "Pizza Timer", - "shortName": "Pizza Timer", - "version": "0.01", - "description": "A timer app for when you cook Pizza. Some say it can also time other things", - "icon": "pizza.png", - "tags": "timer,tool,pizza", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"pizzatimer.app.js","url":"app.js"}, - {"name":"pizzatimer.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "animclk", - "name": "Animated Clock", - "shortName": "Anim Clock", - "version": "0.03", - "description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art", - "icon": "app.png", - "type": "clock", - "tags": "clock,animated", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"animclk.app.js","url":"app.js"}, - {"name":"animclk.pixels1","url":"animclk.pixels1"}, - {"name":"animclk.pixels2","url":"animclk.pixels2"}, - {"name":"animclk.pal","url":"animclk.pal"}, - {"name":"animclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "analogimgclk", - "name": "Analog Clock (Image background)", - "shortName": "Analog Clock", - "version": "0.03", - "description": "An analog clock with an image background", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"analogimgclk.app.js","url":"app.js"}, - {"name":"analogimgclk.bg.img","url":"bg.img"}, - {"name":"analogimgclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "verticalface", - "name": "Vertical watch face", - "shortName": "Vertical Face", - "version": "0.09", - "description": "A simple vertical watch face with the date. Heart rate monitor is toggled with BTN1", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-vertical-watch-face-screenshot.png"}], - "storage": [ - {"name":"verticalface.app.js","url":"app.js"}, - {"name":"verticalface.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "sleepphasealarm", - "name": "SleepPhaseAlarm", - "shortName": "SleepPhaseAlarm", - "version": "0.02", - "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", - "icon": "app.png", - "tags": "alarm", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"sleepphasealarm.app.js","url":"app.js"}, - {"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "life", - "name": "Game of Life", - "version": "0.04", - "description": "Conway's Game of Life - 16x16 board", - "icon": "life.png", - "tags": "game", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-game-of-life-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"life.app.js","url":"life.min.js"}, - {"name":"life.img","url":"life-icon.js","evaluate":true} - ] - }, - { - "id": "magnav", - "name": "Navigation Compass", - "version": "0.05", - "description": "Compass with linear display as for GPSNAV. Has Tilt compensation and remembers calibration.", - "screenshots": [{"url":"screenshot-b2.png"},{"url":"screenshot-light-b2.png"}], - "icon": "magnav.png", - "tags": "tool,outdoors", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"magnav.app.js","url":"magnav_b1.js","supports":["BANGLEJS"]}, - {"name":"magnav.app.js","url":"magnav_b2.js","supports":["BANGLEJS2"]}, - {"name":"magnav.img","url":"magnav-icon.js","evaluate":true} - ], - "data": [{"name":"magnav.json"}] - }, - { - "id": "gpspoilog", - "name": "GPS POI Logger", - "shortName": "GPS POI Log", - "version": "0.01", - "description": "A simple app to log points of interest with their GPS coordinates and read them back onto your PC. Based on the https://www.espruino.com/Bangle.js+Storage tutorial", - "icon": "app.png", - "tags": "outdoors", - "supports": ["BANGLEJS"], - "interface": "interface.html", - "storage": [ - {"name":"gpspoilog.app.js","url":"app.js"}, - {"name":"gpspoilog.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "miclock2", - "name": "Mixed Clock 2", - "version": "0.01", - "description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.", - "icon": "clock-mixed.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-mixed-clock-2-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"miclock2.app.js","url":"clock-mixed.js"}, - {"name":"miclock2.img","url":"clock-mixed-icon.js","evaluate":true} - ] - }, - { - "id": "1button", - "name": "One-Button-Tracker", - "version": "0.01", - "description": "A widget that turns BTN1 into a tracker, records time of button press/release.", - "icon": "widget.png", - "type": "widget", - "tags": "tool,quantifiedself,widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"1button.wid.js","url":"widget.js"} - ], - "data": [{"name":"one_button_presses.csv","storageFile":true}] - }, - { - "id": "gpsautotime", - "name": "GPS auto time", - "shortName": "GPS auto time", - "version": "0.01", - "description": "A widget that automatically updates the Bangle.js time to the GPS time whenever there is a valid GPS fix.", - "icon": "widget.png", - "type": "widget", - "tags": "widget,gps", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"gpsautotime.wid.js","url":"widget.js"} - ] - }, - { - "id": "espruinoctrl", - "name": "Espruino Control", - "shortName": "Espruino Ctrl", - "version": "0.01", - "description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "custom": "custom.html", - "storage": [ - {"name":"espruinoctrl.app.js"}, - {"name":"espruinoctrl.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "multiclock", - "name": "Multi Clock", - "version": "0.09", - "description": "Clock with multiple faces. Switch between faces with BTN1 & BTN3 (Bangle 2 touch top-right, bottom right). For best display set theme Background 2 to cyan or some other bright colour in settings.", - "screenshots": [{"url":"screen-ana.png"},{"url":"screen-big.png"},{"url":"screen-td.png"},{"url":"screen-nifty.png"},{"url":"screen-word.png"},{"url":"screen-sec.png"}], - "icon": "multiclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"multiclock.app.js","url":"multiclock.app.js"}, - {"name":"big.face.js","url":"big.face.js"}, - {"name":"ana.face.js","url":"ana.face.js"}, - {"name":"digi.face.js","url":"digi.face.js"}, - {"name":"txt.face.js","url":"txt.face.js"}, - {"name":"dk.face.js","url":"dk.face.js"}, - {"name":"nifty.face.js","url":"nifty.face.js"}, - {"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true} - ] - }, - { - "id": "widancs", - "name": "Apple Notification Widget", - "shortName": "ANCS Widget", - "version": "0.07", - "description": "Displays call, message etc notifications from a paired iPhone. Read README before installation as it only works with compatible apps", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"widancs.wid.js","url":"ancs.min.js"}, - {"name":"widancs.settings.js","url":"settings.js"} - ] - }, - { - "id": "accelrec", - "name": "Acceleration Recorder", - "shortName": "Accel Rec", - "version": "0.02", - "description": "This app puts the Bangle's accelerometer into 100Hz mode and reads 2 seconds worth of data after movement starts. The data can then be exported back to the PC.", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"accelrec.app.js","url":"app.js"}, - {"name":"accelrec.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"wildcard":"accelrec.?.csv"}] - }, - { - "id": "accellog", - "name": "Acceleration Logger", - "shortName": "Accel Log", - "version": "0.03", - "description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC", - "icon": "app.png", - "tags": "outdoor", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"accellog.app.js","url":"app.js"}, - {"name":"accellog.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"wildcard":"accellog.?.csv"}] - }, - { - "id": "cprassist", - "name": "CPR Assist", - "version": "0.01", - "description": "Provides assistance while performing a CPR", - "icon": "cprassist-icon.png", - "tags": "tool,firstaid", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-CPR-assist-screenshot.png"}], - "storage": [ - {"name":"cprassist.app.js","url":"cprassist.js"}, - {"name":"cprassist.img","url":"cprassist-icon.js","evaluate":true}, - {"name":"cprassist.settings.js","url":"settings.js"} - ] - }, - { - "id": "osgridref", - "name": "Ordnance Survey Grid Reference", - "shortName": "OS Grid ref", - "version": "0.01", - "description": "Displays the UK Ordnance Survey grid reference of your current GPS location. Useful when in the United Kingdom with an Ordnance Survey map", - "icon": "app.png", - "tags": "outdoors,gps", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"osgridref.app.js","url":"app.js"}, - {"name":"osgridref.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "openseizure", - "name": "OpenSeizureDetector Widget", - "shortName": "Short Name", - "version": "0.01", - "description": "[BETA!] A widget to work alongside [OpenSeizureDetector](https://www.openseizuredetector.org.uk/)", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"openseizure.wid.js","url":"widget.js"} - ] - }, - { - "id": "counter", - "name": "Counter", - "version": "0.03", - "description": "Simple counter", - "icon": "counter_icon.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-counter-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"counter.app.js","url":"counter.js"}, - {"name":"counter.img","url":"counter-icon.js","evaluate":true} - ] - }, - { - "id": "bootgattbat", - "name": "BLE GATT Battery Service", - "shortName": "BLE Battery Service", - "version": "0.01", - "description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n", - "icon": "bluetooth.png", - "type": "bootloader", - "tags": "battery,ble,bluetooth,gatt", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"gattbat.boot.js","url":"boot.js"} - ] - }, - { - "id": "viewstl", - "name": "STL file viewer", - "shortName": "ViewSTL", - "version": "0.02", - "description": "This app allows you to view STL 3D models on your watch", - "icon": "icons8-octahedron-48.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"viewstl.app.js","url":"viewstl.min.js"}, - {"name":"viewstl.img","url":"viewstl-icon.js","evaluate":true}, - {"name":"tetra.stl","url":"tetra.stl"}, - {"name":"cube.stl","url":"cube.stl"}, - {"name":"icosa.stl","url":"icosa.stl"} - ] - }, - { - "id": "cscsensor", - "name": "Cycling speed sensor", - "shortName": "CSCSensor", - "version": "0.06", - "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", - "icon": "icons8-cycling-48.png", - "tags": "outdoors,exercise,ble,bluetooth", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"cscsensor.app.js","url":"cscsensor.app.js"}, - {"name":"cscsensor.settings.js","url":"settings.js"}, - {"name":"cscsensor.img","url":"cscsensor-icon.js","evaluate":true} - ] - }, - { - "id": "fileman", - "name": "File manager", - "shortName": "FileManager", - "version": "0.03", - "description": "Simple file manager, allows user to examine watch storage and display, load or delete individual files", - "icon": "icons8-filing-cabinet-48.png", - "tags": "tools", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"fileman.app.js","url":"fileman.app.js"}, - {"name":"fileman.img","url":"fileman-icon.js","evaluate":true} - ] - }, - { - "id": "worldclock", - "name": "World Clock - 4 time zones", - "shortName": "World Clock", - "version": "0.05", - "description": "Current time zone plus up to four others", - "icon": "app.png", - "screenshots": [{"url":"screenshot_world.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "custom": "custom.html", - "storage": [ - {"name":"worldclock.app.js","url":"app.js"}, - {"name":"worldclock.img","url":"worldclock-icon.js","evaluate":true} - ], - "data": [{"name":"worldclock.settings.json"}] - }, - { - "id": "digiclock", - "name": "Digital Clock Face", - "shortName": "Digi Clock", - "version": "0.02", - "description": "A simple digital clock with the time, day, month, and year", - "icon": "digiclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"digiclock.app.js","url":"digiclock.js"}, - {"name":"digiclock.img","url":"digiclock-icon.js","evaluate":true} - ] - }, - { - "id": "dsdrelay", - "name": "DSD BLE Relay controller", - "shortName": "DSDRelay", - "version": "0.01", - "description": "Control BLE relay board from the watch", - "icon": "icons8-relay-48.png", - "tags": "ble,bluetooth", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"dsdrelay.app.js","url":"dsdrelay.app.js"}, - {"name":"dsdrelay.img","url":"dsdrelay-icon.js","evaluate":true} - ] - }, - { - "id": "mandel", - "name": "Mandelbrot", - "shortName": "Mandel", - "version": "0.01", - "description": "Draw a zoomable Mandelbrot set", - "icon": "mandel.png", - "tags": "game", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"mandel.app.js","url":"mandel.min.js"}, - {"name":"mandel.img","url":"mandel-icon.js","evaluate":true} - ] - }, - { - "id": "petrock", - "name": "Pet rock", - "version": "0.02", - "description": "A virtual pet rock with wobbly eyes", - "icon": "petrock.png", - "type": "app", - "tags": "game", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"petrock.app.js","url":"app.js"}, - {"name":"petrock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "smartibot", - "name": "Smartibot controller", - "shortName": "Smartibot", - "version": "0.01", - "description": "Control a [Smartibot Robot](https://thecraftyrobot.net/) straight from your Bangle.js", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"smartibot.app.js","url":"app.js"}, - {"name":"smartibot.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widncr", - "name": "NCR Logo Widget", - "version": "0.01", - "description": "Show the NodeConf Remote logo in the top left", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"widncr.wid.js","url":"widget.js"} - ] - }, - { - "id": "ncrclk", - "name": "NCR Clock", - "shortName": "NCR Clock", - "version": "0.02", - "description": "NodeConf Remote clock", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"ncrclk.app.js","url":"app.js"}, - {"name":"ncrclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "isoclock", - "name": "ISO Compliant Clock Face", - "shortName": "ISO Clock", - "version": "0.02", - "description": "Tweaked fork of digiclock for ISO date and time", - "icon": "isoclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"isoclock.app.js","url":"isoclock.js"}, - {"name":"isoclock.img","url":"isoclock-icon.js","evaluate":true} - ] - }, - { - "id": "gpstimeserver", - "name": "GPS Time Server", - "version": "0.01", - "description": "A widget which automatically starts the GPS and turns Bangle.js into a Bluetooth time server.", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"gpstimeserver.wid.js","url":"widget.js"} - ] - }, - { - "id": "tilthydro", - "name": "Tilt Hydrometer Display", - "shortName": "Tilt Hydro", - "version": "0.01", - "description": "A display for the [Tilt Hydrometer](https://tilthydrometer.com/) - [more info here](http://www.espruino.com/Tilt+Hydrometer+Display)", - "icon": "app.png", - "tags": "tools,bluetooth", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"tilthydro.app.js","url":"app.js"}, - {"name":"tilthydro.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "supmariodark", - "name": "Super mario clock night mode", - "shortName": "supmariodark", - "version": "0.01", - "description": "Super mario clock in night mode", - "icon": "supmariodark.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"supmariodark.app.js","url":"supmariodark.js"}, - {"name":"supmariodark.img","url":"supmariodark-icon.js","evaluate":true}, - {"name":"supmario30x24.bin","url":"supmario30x24.bin.js"}, - {"name":"supmario30x24.wdt","url":"supmario30x24.wdt.js"}, - {"name":"banner-up.img","url":"banner-up.js","evaluate":true}, - {"name":"banner-down.img","url":"banner-down.js","evaluate":true}, - {"name":"brick2.img","url":"brick2.js","evaluate":true}, - {"name":"enemy.img","url":"enemy.js","evaluate":true}, - {"name":"flower.img","url":"flower.js","evaluate":true}, - {"name":"flower_b.img","url":"flower_b.js","evaluate":true}, - {"name":"mario_wh.img","url":"mario_wh.js","evaluate":true}, - {"name":"pipe.img","url":"pipe.js","evaluate":true} - ] - }, - { - "id": "gmeter", - "name": "G-Meter", - "shortName": "G-Meter", - "version": "0.01", - "description": "Simple G-Meter", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"gmeter.app.js","url":"app.js"}, - {"name":"gmeter.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "dtlaunch", - "name": "Desktop Launcher", - "version": "0.05", - "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", - "type": "launch", - "tags": "tool,system,launcher", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]}, - {"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]}, - {"name":"dtlaunch.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "HRV", - "name": "Heart Rate Variability monitor", - "shortName": "HRV monitor", - "version": "0.04", - "description": "Heart Rate Variability monitor, see Readme for more info", - "icon": "hrv.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"HRV.app.js","url":"app.js"}, - {"name":"HRV.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "hardalarm", - "name": "Hard Alarm", - "shortName": "HardAlarm", - "version": "0.02", - "description": "Make sure you wake up! Count to the right number to turn off the alarm", - "icon": "app.png", - "tags": "tool,alarm,widget", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"hardalarm.app.js","url":"app.js"}, - {"name":"hardalarm.boot.js","url":"boot.js"}, - {"name":"hardalarm.js","url":"hardalarm.js"}, - {"name":"hardalarm.img","url":"app-icon.js","evaluate":true}, - {"name":"hardalarm.wid.js","url":"widget.js"} - ], - "data": [{"name":"hardalarm.json"}] - }, - { - "id": "edisonsball", - "name": "Edison's Ball", - "shortName": "Edison's Ball", - "version": "0.01", - "description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness", - "icon": "app-icon.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"edisonsball.app.js","url":"app.js"}, - {"name":"edisonsball.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "hrrawexp", - "name": "HRM Data Exporter", - "shortName": "HRM Data Exporter", - "version": "0.01", - "description": "export raw hrm signal data to a csv file", - "icon": "app-icon.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "interface.html", - "storage": [ - {"name":"hrrawexp.app.js","url":"app.js"}, - {"name":"hrrawexp.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "breath", - "name": "Breathing App", - "shortName": "Breathing App", - "version": "0.01", - "description": "app to aid relaxation and train breath syncronicity using haptics and visualisation, also displays HR", - "icon": "app-icon.png", - "tags": "tools,health", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"breath.app.js","url":"app.js"}, - {"name":"breath.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"breath.settings.json","url":"settings.json"}] - }, - { - "id": "lazyclock", - "name": "Lazy Clock", - "version": "0.03", - "description": "Tells the time, roughly", - "icon": "lazyclock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "screenshots": [{"url":"bangle1-lazy-clock-screenshot.png"}], - "allow_emulator": true, - "storage": [ - {"name":"lazyclock.app.js","url":"lazyclock-app.js"}, - {"name":"lazyclock.img","url":"lazyclock-icon.js","evaluate":true} - ] - }, - { - "id": "astral", - "name": "Astral Clock", - "version": "0.03", - "description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.", - "icon": "app-icon.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"astral.app.js","url":"app.js"}, - {"name":"astral.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "alpinenav", - "name": "Alpine Nav", - "version": "0.01", - "description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime", - "icon": "app-icon.png", - "tags": "outdoors,gps", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"alpinenav.app.js","url":"app.js"}, - {"name":"alpinenav.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "lifeclk", - "name": "Game of Life Clock", - "shortName": "Conway's Clock", - "version": "0.06", - "description": "Modification and clockification of Conway's Game of Life", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"lifeclk.app.js","url":"app.min.js"}, - {"name":"lifeclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "speedalt", - "name": "GPS Adventure Sports", - "shortName": "GPS Adv Sport", - "version": "1.02", - "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", - "icon": "app.png", - "type": "app", - "tags": "tool,outdoors", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"speedalt.app.js","url":"app.js"}, - {"name":"speedalt.img","url":"app-icon.js","evaluate":true}, - {"name":"speedalt.settings.js","url":"settings.js"} - ], - "data": [{"name":"speedalt.json"}] - }, - { - "id": "speedalt2", - "name": "GPS Adventure Sports II", - "shortName": "GPS Adv Sport II", - "version": "0.07", - "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", - "icon": "app.png", - "type": "app", - "tags": "tool,outdoors", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"speedalt2.app.js","url":"app.js"}, - {"name":"speedalt2.img","url":"app-icon.js","evaluate":true}, - {"name":"speedalt2.settings.js","url":"settings.js"} - ], - "data": [{"name":"speedalt2.json"}] - }, - { - "id": "slomoclock", - "name": "SloMo Clock", - "shortName": "SloMo Clock", - "version": "0.10", - "description": "Simple 24h clock face with large digits, hour above minute. Uses Layout library.", - "icon": "watch.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-slow-mo-clock-screenshot.png"}], - "storage": [ - {"name":"slomoclock.app.js","url":"app.js"}, - {"name":"slomoclock.img","url":"app-icon.js","evaluate":true}, - {"name":"slomoclock.settings.js","url":"settings.js"} - ], - "data": [{"name":"slomoclock.json"}] - }, - { - "id": "de-stress", - "name": "De-Stress", - "shortName": "De-Stress", - "version": "0.02", - "description": "Simple haptic heartbeat", - "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"de-stress.app.js","url":"app.js"}, - {"name":"de-stress.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "mclockplus", - "name": "Morph Clock+", - "shortName": "Morph Clock+", - "version": "0.02", - "description": "Morphing Clock with more readable seconds and date and additional stopwatch", - "icon": "mclockplus.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"mclockplus.app.js","url":"mclockplus.app.js"}, - {"name":"mclockplus.img","url":"mclockplus-icon.js","evaluate":true} - ] - }, - { - "id": "intervals", - "name": "Intervals App", - "shortName": "Intervals", - "version": "0.01", - "description": "Intervals for training. It is possible to configure work time and rest time and number of sets.", - "icon": "intervals.png", - "tags": "", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"intervals.app.js","url":"intervals.app.js"}, - {"name":"intervals.img","url":"intervals-icon.js","evaluate":true} - ] - }, - { - "id": "planetarium", - "name": "Planetarium", - "shortName": "Planetarium", - "version": "0.03", - "description": "Planetarium showing up to 500 stars using the watch location and time", - "icon": "planetarium.png", - "tags": "", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"planetarium.app.js","url":"planetarium.app.js"}, - {"name":"planetarium.data.csv","url":"planetarium.data.csv"}, - {"name":"planetarium.const.csv","url":"planetarium.const.csv"}, - {"name":"planetarium.extra.csv","url":"planetarium.extra.csv"}, - {"name":"planetarium.settings.js","url":"settings.js"}, - {"name":"planetarium.img","url":"planetarium-icon.js","evaluate":true} - ], - "data": [{"name":"planetarium.json"}] - }, - { - "id": "tapelauncher", - "name": "Tape Launcher", - "version": "0.02", - "description": "An App launcher, icons displayed in a horizontal tape, swipe or use buttons", - "icon": "icon.png", - "type": "launch", - "tags": "tool,system,launcher", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"tapelauncher.app.js","url":"app.js"}, - {"name":"tapelauncher.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "oblique", - "name": "Oblique Strategies", - "version": "0.01", - "description": "Oblique Strategies for creativity. Copied from Brian Eno.", - "icon": "eno.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"oblique.app.js","url":"app.js"}, - {"name":"oblique.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "testuserinput", - "name": "Test User Input", - "shortName": "Test User Input", - "version": "0.06", - "description": "App to test the bangle.js input interface. It displays the user action in text, circle buttons or on/off switch UI elements.", - "icon": "app.png", - "tags": "input,interface,buttons,touch,UI", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"testuserinput.app.js","url":"app.js"}, - {"name":"testuserinput.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "gpssetup", - "name": "GPS Setup", - "shortName": "GPS Setup", - "version": "0.02", - "description": "Configure the GPS power options and store them in the GPS nvram", - "icon": "gpssetup.png", - "tags": "gps,tools,outdoors", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"gpssetup","url":"gpssetup.js"}, - {"name":"gpssetup.settings.js","url":"settings.js"}, - {"name":"gpssetup.app.js","url":"app.js"}, - {"name":"gpssetup.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"gpssetup.settings.json","url":"settings.json"}] - }, - { - "id": "walkersclock", - "name": "Walkers Clock", - "shortName": "Walkers Clock", - "version": "0.04", - "description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference", - "icon": "walkersclock48.png", - "type": "clock", - "tags": "clock,gps,tools,outdoors", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"walkersclock.app.js","url":"app.js"}, - {"name":"walkersclock.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "widgps", - "name": "GPS Widget", - "version": "0.03", - "description": "Tiny widget to show the power on/off status of the GPS", - "icon": "widget.png", - "type": "widget", - "tags": "widget,gps", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"widgps.wid.js","url":"widget.js"} - ] - }, - { - "id": "widhrt", - "name": "HRM Widget", - "version": "0.03", - "description": "Tiny widget to show the power on/off status of the Heart Rate Monitor", - "icon": "widget.png", - "type": "widget", - "tags": "widget,hrm", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"widhrt.wid.js","url":"widget.js"} - ] - }, - { - "id": "countdowntimer", - "name": "Countdown Timer", - "version": "0.01", - "description": "A simple countdown timer with a focus on usability", - "icon": "countdowntimer.png", - "tags": "timer,tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"countdowntimer.app.js","url":"countdowntimer.js"}, - {"name":"countdowntimer.img","url":"countdowntimer-icon.js","evaluate":true} - ] - }, - { - "id": "helloworld", - "name": "hello, world!", - "shortName": "hello world", - "version": "0.02", - "description": "A cross cultural hello world!/hola mundo! app with colors and languages", - "icon": "app.png", - "tags": "input,interface,buttons,touch", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"helloworld.app.js","url":"app.js"}, - {"name":"helloworld.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widcom", - "name": "Compass Widget", - "version": "0.02", - "description": "Tiny widget to show the power on/off status of the Compass", - "icon": "widget.png", - "type": "widget", - "tags": "widget,compass", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"widcom.wid.js","url":"widget.js"} - ] - }, - { - "id": "arrow", - "name": "Arrow Compass", - "version": "0.05", - "description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass", - "icon": "arrow.png", - "type": "app", - "tags": "tool,outdoors", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"arrow.app.js","url":"app.js"}, - {"name":"arrow.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "waypointer", - "name": "Way Pointer", - "version": "0.01", - "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", - "icon": "waypointer.png", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "waypoints.html", - "storage": [ - {"name":"waypointer.app.js","url":"app.js"}, - {"name":"waypointer.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] - }, - { - "id": "color_catalog", - "name": "Colors Catalog", - "shortName": "Colors Catalog", - "version": "0.01", - "description": "Displays RGB565 and RGB888 colors, its name and code in screen.", - "icon": "app.png", - "tags": "Color,input,buttons,touch,UI", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"color_catalog.app.js","url":"app.js"}, - {"name":"color_catalog.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "UI4swatch", - "name": "UI 4 swatch", - "shortName": "UI 4 swatch", - "version": "0.01", - "description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.", - "icon": "app.png", - "tags": "Color,input,buttons,touch,UI", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"UI4swatch.app.js","url":"app.js"}, - {"name":"UI4swatch.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "simplest", - "name": "Simplest Clock", - "version": "0.03", - "description": "The simplest working clock, acts as a tutorial piece", - "icon": "simplest.png", - "screenshots": [{"url":"screenshot_simplest.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"simplest.app.js","url":"app.js"}, - {"name":"simplest.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "stepo", - "name": "Stepometer Clock", - "version": "0.03", - "description": "A large font watch, displays step count in a doughnut guage and warns of low battery, requires one of the steps widgets to be installed", - "icon": "stepo.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"stepo.app.js","url":"app.js"}, - {"name":"stepo.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "gbmusic", - "name": "Gadgetbridge Music Controls", - "shortName": "Music Controls", - "version": "0.07", - "description": "Control the music on your Gadgetbridge-connected phone", - "icon": "icon.png", - "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], - "type": "app", - "tags": "tools,bluetooth,gadgetbridge,music", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"gbmusic.app.js","url":"app.js"}, - {"name":"gbmusic.settings.js","url":"settings.js"}, - {"name":"gbmusic.wid.js","url":"widget.js"}, - {"name":"gbmusic.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"gbmusic.json"},{"name":"gbmusic.load.json"}] - }, - { - "id": "battleship", - "name": "Battleship", - "version": "0.01", - "description": "The classic game of battleship", - "icon": "battleship-icon.png", - "tags": "game", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-battle-ship-screenshot.png"}], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"battleship.app.js","url":"battleship.js"}, - {"name":"battleship.img","url":"battleship-icon.js","evaluate":true} - ] - }, - { - "id": "kitchen", - "name": "Kitchen Combo", - "version": "0.13", - "description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'", - "icon": "kitchen.png", - "type": "clock", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], - "readme": "README.md", - "interface": "waypoints.html", - "storage": [ - {"name":"kitchen.app.js","url":"kitchen.app.js"}, - {"name":"stepo2.kit.js","url":"stepo2.kit.js"}, - {"name":"swatch.kit.js","url":"swatch.kit.js"}, - {"name":"gps.kit.js","url":"gps.kit.js"}, - {"name":"compass.kit.js","url":"compass.kit.js"}, - {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] - }, - { - "id": "banglebridge", - "name": "BangleBridge", - "shortName": "BangleBridge", - "version": "0.01", - "description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App", - "icon": "widget.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"banglebridge.wid.js","url":"widget.js"}, - {"name":"banglebridge.watch.img","url":"watch.img"}, - {"name":"banglebridge.heart.img","url":"heart.img"} - ] - }, - { - "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", - "icon": "app.png", - "screenshots": [{"url":"screenshot_edit.png"},{"url":"screenshot_main.png"},{"url":"screenshot_widget_alarms.png"},{"url":"screenshot_widget_silent.png"}], - "tags": "tool,widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"qmsched","url":"lib.js"}, - {"name":"qmsched.app.js","url":"app.js"}, - {"name":"qmsched.boot.js","url":"boot.js"}, - {"name":"qmsched.img","url":"icon.js","evaluate":true}, - {"name":"qmsched.wid.js","url":"widget.js"} - ], - "data": [{"name":"qmsched.json"}] - }, - { - "id": "hourstrike", - "name": "Hour Strike", - "shortName": "Hour Strike", - "version": "0.08", - "description": "Strike the clock on the hour. A great tool to remind you an hour has passed!", - "icon": "app-icon.png", - "tags": "tool,alarm", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"hourstrike.app.js","url":"app.js"}, - {"name":"hourstrike.boot.js","url":"boot.js"}, - {"name":"hourstrike.img","url":"app-icon.js","evaluate":true}, - {"name":"hourstrike.json","url":"hourstrike.json"} - ] - }, - { - "id": "whereworld", - "name": "Where in the World?", - "shortName": "Where World", - "version": "0.01", - "description": "Shows your current location on the world map", - "icon": "app.png", - "tags": "gps", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"whereworld.app.js","url":"app.js"}, - {"name":"whereworld.img","url":"app-icon.js","evaluate":true}, - {"name":"whereworld.worldmap","url":"worldmap"} - ] - }, - { - "id": "omnitrix", - "name": "Omnitrix", - "version": "0.01", - "description": "An Omnitrix Showpiece", - "icon": "omnitrix.png", - "screenshots": [{"url":"screenshot.png"}], - "tags": "game", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"omnitrix.app.js","url":"omnitrix.app.js"}, - {"name":"omnitrix.img","url":"omnitrix.icon.js","evaluate":true} - ] - }, - { - "id": "batclock", - "name": "Bat Clock", - "shortName": "Bat Clock", - "version": "0.02", - "description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.", - "icon": "bat-clock.png", - "screenshots": [{"url":"screenshot.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"batclock.app.js","url":"bat-clock.app.js"}, - {"name":"batclock.img","url":"bat-clock.icon.js","evaluate":true} - ] - }, - { - "id": "doztime", - "name": "Dozenal Time", - "shortName": "Dozenal Time", - "version": "0.04", - "description": "A dozenal Holocene calendar and dozenal diurnal clock", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"doztime.app.js","url":"app.js"}, - {"name":"doztime.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "gbtwist", - "name": "Gadgetbridge Twist Control", - "shortName": "Twist Control", - "version": "0.01", - "description": "Shake your wrist to control your music app via Gadgetbridge", - "icon": "app.png", - "type": "app", - "tags": "tools,bluetooth,gadgetbridge,music", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": false, - "storage": [ - {"name":"gbtwist.app.js","url":"app.js"}, - {"name":"gbtwist.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "thermom", - "name": "Thermometer", - "version": "0.03", - "description": "Displays the current temperature in degree Celsius, updated every 20 seconds", - "icon": "app.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"thermom.app.js","url":"app.js"}, - {"name":"thermom.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "mysticdock", - "name": "Mystic Dock", - "version": "1.00", - "description": "A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line.", - "icon": "mystic-dock.png", - "type": "dock", - "tags": "dock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"mysticdock.app.js","url":"mystic-dock-app.js"}, - {"name":"mysticdock.boot.js","url":"mystic-dock-boot.js"}, - {"name":"mysticdock.settings.js","url":"mystic-dock-settings.js"}, - {"name":"mysticdock.img","url":"mystic-dock-icon.js","evaluate":true} - ] - }, - { - "id": "mysticclock", - "name": "Mystic Clock", - "version": "1.01", - "description": "A retro-inspired watchface featuring time, date, and an interactive data display line.", - "icon": "mystic-clock.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"bangle1-mystic-clock-screenshot.png"}], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"mysticclock.app.js","url":"mystic-clock-app.js"}, - {"name":"mysticclock.settings.js","url":"mystic-clock-settings.js"}, - {"name":"mysticclock.img","url":"mystic-clock-icon.js","evaluate":true} - ] - }, - { - "id": "hcclock", - "name": "Hi-Contrast Clock", - "version": "0.02", - "description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.", - "icon": "hcclock-icon.png", - "type": "clock", - "tags": "clock", - "screenshots": [{"url":"bangle1-high-contrast-clock-screenshot.png"}], - "supports": ["BANGLEJS"], - "allow_emulator": true, - "storage": [ - {"name":"hcclock.app.js","url":"hcclock.app.js"}, - {"name":"hcclock.img","url":"hcclock-icon.js","evaluate":true} - ] - }, - { - "id": "thermomF", - "name": "Fahrenheit Temp", - "version": "0.01", - "description": "A modification of the Thermometer App to display temprature in Fahrenheit", - "icon": "thermf.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"thermomF.app.js","url":"app.js"}, - {"name":"thermomF.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "nixie", - "name": "Nixie Clock", - "shortName": "Nixie", - "version": "0.01", - "description": "A nixie tube clock for both Bangle 1 and 2.", - "icon": "nixie.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"nixie.app.js","url":"app.js"}, - {"name":"nixie.img","url":"app-icon.js","evaluate":true}, - {"name":"m_vatch.js","url":"m_vatch.js"} - ] - }, - { - "id": "carcrazy", - "name": "Car Crazy", - "shortName": "Car Crazy", - "version": "0.03", - "description": "A simple car game where you try to avoid the other cars by tilting your wrist left and right. Hold down button 2 to start.", - "icon": "carcrash.png", - "tags": "game", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"carcrazy.app.js","url":"app.js"}, - {"name":"carcrazy.img","url":"app-icon.js","evaluate":true}, - {"name":"carcrazy.settings.js","url":"settings.js"} - ], - "data": [{"name":"app.json"}] - }, - { - "id": "shortcuts", - "name": "Shortcuts", - "shortName": "Shortcuts", - "version": "0.01", - "description": "Quickly load your favourite apps from (almost) any watch face.", - "icon": "app.png", - "type": "bootloader", - "tags": "tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "storage": [ - {"name":"shortcuts.boot.js","url":"boot.js"}, - {"name":"shortcuts.settings.js","url":"settings.js"} - ], - "data": [{"name":"shortcuts.json"}] - }, - { - "id": "vectorclock", - "name": "Vector Clock", - "version": "0.02", - "description": "A digital clock that uses the built-in vector font.", - "icon": "app.png", - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS"], - "allow_emulator": true, - "screenshots": [{"url":"bangle1-vector-clock-screenshot.png"}], - "storage": [ - {"name":"vectorclock.app.js","url":"app.js"}, - {"name":"vectorclock.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "fd6fdetect", - "name": "fd6fdetect", - "shortName": "fd6fdetect", - "version": "0.2", - "description": "Allows you to see 0xFD6F beacons near you.", - "icon": "app.png", - "tags": "tool", - "readme": "README.md", - "supports": ["BANGLEJS"], - "storage": [ - {"name":"fd6fdetect.app.js","url":"app.js"}, - {"name":"fd6fdetect.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "choozi", - "name": "Choozi", - "version": "0.01", - "description": "Choose people or things at random using Bangle.js.", - "icon": "app.png", - "tags": "tool", - "supports": ["BANGLEJS"], - "readme": "README.md", - "allow_emulator": true, - "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], - "storage": [ - {"name":"choozi.app.js","url":"app.js"}, - {"name":"choozi.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "widclkbttm", - "name": "Digital clock (Bottom) widget", - "shortName": "Digital clock Bottom Widget", - "version": "0.03", - "description": "Displays time in the bottom area.", - "icon": "widclkbttm.png", - "type": "widget", - "tags": "widget", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"widclkbttm.wid.js","url":"widclkbttm.wid.js"} - ] - }, - { - "id": "pastel", - "name": "Pastel Clock", - "shortName": "Pastel", - "version": "0.07", - "description": "A Configurable clock with custom fonts and background", - "icon": "pastel.png", - "screenshots": [{"url":"screenshot_pastel.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"f_architect","url":"f_architect.js"}, - {"name":"f_gochihand","url":"f_gochihand.js"}, - {"name":"f_cabin","url":"f_cabin.js"}, - {"name":"f_orbitron","url":"f_orbitron.js"}, - {"name":"f_monoton","url":"f_monoton.js"}, - {"name":"f_elite","url":"f_elite.js"}, - {"name":"f_lato","url":"f_lato.js"}, - {"name":"f_latosmall","url":"f_latosmall.js"}, - {"name":"pastel.app.js","url":"pastel.app.js"}, - {"name":"pastel.img","url":"pastel.icon.js","evaluate":true}, - {"name":"pastel.settings.js","url":"pastel.settings.js"} - ], - "data": [{"name":"pastel.json"}] - }, - { - "id": "antonclk", - "name": "Anton Clock", - "version": "0.03", - "description": "A simple clock using the bold Anton font.", - "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"antonclk.app.js","url":"app.js"}, - {"name":"antonclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "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**", - "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"waveclk.app.js","url":"app.js"}, - {"name":"waveclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "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**", - "icon": "app.png", - "screenshots": [{"url":"screenshot_floral.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"floralclk.app.js","url":"app.js"}, - {"name":"floralclk.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "score", - "name": "Score Tracker", - "version": "0.01", - "description": "Score Tracker for sports that use plain numbers (e.g. Badminton, Volleyball, Soccer, Table Tennis, ...). Also supports tennis scoring.", - "icon": "score.app.png", - "screenshots": [{"url":"screenshot_score.png"}], - "type": "app", - "tags": "", - "supports": ["BANGLEJS","BANGLEJS2"], - "storage": [ - {"name":"score.app.js","url":"score.app.js"}, - {"name":"score.settings.js","url":"score.settings.js"}, - {"name":"score.presets.json","url":"score.presets.json"}, - {"name":"score.img","url":"score.app-icon.js","evaluate":true} - ], - "data": [{"name":"score.json"}] - }, - { - "id": "menusmall", - "name": "Small Menus", - "version": "0.02", - "description": "Replace Bangle.js 2's menus with a version that contains smaller text", - "icon": "app.png", - "type": "boot", - "tags": "system", - "supports": ["BANGLEJS2"], - "storage": [ - {"name":"menusmall.boot.js","url":"boot.js"} - ] - }, - { - "id": "ffcniftya", - "name": "Nifty-A Clock", - "version": "0.01", - "description": "A nifty clock with time and date", - "icon": "app.png", - "screenshots": [{"url":"screenshot_nifty.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"ffcniftya.app.js","url":"app.js"}, - {"name":"ffcniftya.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "ffcniftyb", - "name": "Nifty-B Clock", - "version": "0.02", - "description": "A nifty clock (series B) with time, date and color configuration", - "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"ffcniftyb.app.js","url":"app.js"}, - {"name":"ffcniftyb.img","url":"app-icon.js","evaluate":true}, - {"name":"ffcniftyb.settings.js","url":"settings.js"} - ], - "data": [{"name":"ffcniftyb.json"}] - }, - { - "id": "stopwatch", - "name": "Stopwatch Touch", - "version": "0.01", - "description": "A touch based stop watch for Bangle JS 2", - "icon": "stopwatch.png", - "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], - "tags": "tools,app", - "supports": ["BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"stopwatch.app.js","url":"stopwatch.app.js"}, - {"name":"stopwatch.img","url":"stopwatch.icon.js","evaluate":true} - ] - }, - { - "id": "vernierrespirate", - "name": "Vernier Go Direct Respiration Belt", - "shortName": "Respiration Belt", - "version": "0.01", - "description": "Connects to a Go Direct Respiration Belt and shows respiration rate", - "icon": "app.png", - "tags": "health,bluetooth", - "supports": ["BANGLEJS","BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"vernierrespirate.app.js","url":"app.js"}, - {"name":"vernierrespirate.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"vernierrespirate.json"}] - }, - { - "id": "gpstouch", - "name": "GPS Touch", - "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"}], - "tags": "tools,app", - "supports": ["BANGLEJS2"], - "readme": "README.md", - "storage": [ - {"name":"geotools","url":"geotools.js"}, - {"name":"gpstouch.app.js","url":"gpstouch.app.js"}, - {"name":"gpstouch.img","url":"gpstouch.icon.js","evaluate":true} - ] - }, - { - "id": "swiperclocklaunch", - "name": "Swiper Clock Launch", - "version": "0.02", - "description": "Navigate between clock and launcher with Swipe action", - "icon": "swiperclocklaunch.png", - "type": "bootloader", - "tags": "tools, system", - "supports": ["BANGLEJS", "BANGLEJS2"], - "storage": [ - {"name":"swiperclocklaunch.boot.js","url":"boot.js"}, - {"name":"swiperclocklaunch.img","url":"icon.js","evaluate":true} - ] - }, - { - "id": "qalarm", - "name": "Q Alarm and Timer", - "shortName": "Q Alarm", - "icon": "app.png", - "version": "0.02", - "description": "Alarm and timer app with days of week and 'hard' option.", - "tags": "tool,alarm,widget", - "supports": ["BANGLEJS", "BANGLEJS2"], - "storage": [ - { "name": "qalarm.app.js", "url": "app.js" }, - { "name": "qalarm.boot.js", "url": "boot.js" }, - { "name": "qalarm.js", "url": "qalarm.js" }, - { "name": "qalarmcheck.js", "url": "qalarmcheck.js" }, - { "name": "qalarm.img", "url": "app-icon.js", "evaluate": true }, - { "name": "qalarm.wid.js", "url": "widget.js" } - ], - "data": [{ "name": "qalarm.json" }] - }, - { - "id": "emojuino", - "name": "Emojuino", - "shortName": "Emojuino", - "version": "0.02", - "description": "Emojis & Espruino: broadcast Unicode emojis via Bluetooth Low Energy.", - "icon": "emojuino.png", - "screenshots": [ - { "url": "screenshot-tx.png" }, - { "url": "screenshot-swipe.png" }, - { "url": "screenshot-welcome.png" } - ], - "type": "app", - "tags": "emoji", - "supports" : [ "BANGLEJS2" ], - "allow_emulator": true, - "readme": "README.md", - "storage": [ - { "name": "emojuino.app.js", "url": "emojuino.js" }, - { "name": "emojuino.img", "url": "emojuino-icon.js", "evaluate": true } - ] - }, - { - "id": "cliclockJS2Enhanced", - "name": "Commandline-Clock JS2 Enhanced", - "shortName": "CLI-Clock JS2", - "version": "0.02", - "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"}], - "type": "clock", - "tags": "clock,cli,command,bash,shell", - "supports": ["BANGLEJS","BANGLEJS2"], - "allow_emulator": true, - "storage": [ - {"name":"cliclockJS2Enhanced.app.js","url":"app.js"}, - {"name":"cliclockJS2Enhanced.img","url":"app.icon.js","evaluate":true} - ] - }, - { - "id": "wid_a_battery_widget", - "name": "A Battery Widget (with percentage)", - "shortName":"A Battery Widget", - "icon": "widget.png", - "version":"1.01", - "type": "widget", - "supports": ["BANGLEJS2"], - "readme": "README.md", - "description": "Simple and slim battery widget with charge status and percentage", - "tags": "widget,battery", - "storage": [ - {"name":"wid_a_battery_widget.wid.js","url":"widget.js"} - ] - }, - { - "id": "lcars", - "name": "LCARS Clock", - "shortName":"LCARS", - "icon": "lcars.png", - "version":"0.06", - "readme": "README.md", - "supports": ["BANGLEJS2"], - "description": "Library Computer Access Retrieval System (LCARS) clock.", - "type": "clock", - "tags": "clock", - "screenshots": [{"url":"screenshot.png"}], - "storage": [ - {"name":"lcars.app.js","url":"lcars.app.js"}, - {"name":"lcars.img","url":"lcars.icon.js","evaluate":true} - ] - }, - { "id": "binwatch", - "name": "Binary Watch", - "shortName":"BinWatch", - "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "version":"0.04", - "supports": ["BANGLEJS2"], - "readme": "README.md", - "allow_emulator":true, - "description": "Famous binary watch", - "tags": "clock", - "type": "clock", - "storage": [ - {"name":"binwatch.app.js","url":"app.js"}, - {"name":"binwatch.bg176.img","url":"Background176_center.img"}, - {"name":"binwatch.bg240.img","url":"Background240_center.img"}, - {"name":"binwatch.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id": "hidmsicswipe", - "name": "Bluetooth Music Swipe Controls", - "shortName": "Swipe Control", - "version": "0.01", - "description": "Based on the original Bluetooth Music Controls. Swipe up/down for volume, left/right for previous and next, tap for play/pause and btn1 to lock and unlock the controls. Enable HID in settings, pair with your phone, then use this app to control music from your watch!", - "icon": "hidmsicswipe.png", - "tags": "bluetooth", - "supports": ["BANGLEJS2"], - "storage": [ - {"name":"hidmsicswipe.app.js","url":"hidmsicswipe.js"}, - {"name":"hidmsicswipe.img","url":"hidmsicswipe-icon.js","evaluate":true} - ] - }, - { - "id": "authentiwatch", - "name": "2FA Authenticator", - "shortName": "AuthWatch", - "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "version": "0.01", - "description": "Google Authenticator compatible tool.", - "tags": "tool", - "interface": "interface.html", - "supports": ["BANGLEJS", "BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"authentiwatch.app.js","url":"app.js"}, - {"name":"authentiwatch.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"authentiwatch.json"}] - }, - { "id": "schoolCalendar", - "name": "School Calendar", - "shortName":"SCalendar", - "icon": "CalenderLogo.png", - "version": "0.01", - "description": "A simple calendar that you can see your upcoming events that you create in the customizer. Keep in note that your events reapeat weekly.(Beta)", - "tags": "tool", - "readme":"README.md", - "custom":"custom.html", - "supports": ["BANGLEJS"], - "screenshots": [{"url":"screenshot_basic.png"},{"url":"screenshot_info.png"}], - "storage": [ - {"name":"schoolCalendar.app.js"}, - {"name":"schoolCalendar.img","url":"app-icon.js","evaluate":true} - ], - "data": [ - {"name":"app.json"} - ] - }, - { "id": "timecal", - "name": "TimeCal", - "shortName":"TimeCal", - "icon": "icon.png", - "version":"0.01", - "description": "TimeCal shows the Time along with a 3 week calendar", - "tags": "clock", - "type": "clock", - "supports":["BANGLEJS2"], - "storage": [ - {"name":"timecal.app.js","url":"timecal.app.js"} - ] - }, - { - "id": "a_clock_timer", - "name": "A Clock with Timer", - "version": "0.01", - "description": "A Clock with Timer, Map and Time Zones", - "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "type": "clock", - "tags": "clock", - "supports": ["BANGLEJS2"], - "allow_emulator": true, - "readme": "README.md", - "storage": [ - {"name":"a_clock_timer.app.js","url":"app.js"}, - {"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true} - ] - }, - { - "id":"intervalTimer", - "name":"Interval Timer", - "shortName":"Interval Timer", - "icon": "app.png", - "version":"0.01", - "description": "Interval Timer for workouts, HIIT, or whatever else.", - "tags": "timer, interval, hiit, workout", - "readme":"README.md", - "supports":["BANGLEJS2"], - "storage": [ - {"name":"intervalTimer.app.js","url":"app.js"}, - {"name":"intervalTimer.img","url":"app-icon.js","evaluate":true} - ] - }, - { "id": "93dub", - "name": "93 Dub", - "shortName":"93 Dub", - "icon": "93dub.png", - "screenshots": [{"url":"screenshot.png"}], - "version":"0.03", - "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", - "supports":["BANGLEJS2"], - "readme": "README.md", - "allow_emulator": true, - "storage": [ - {"name":"93dub.app.js","url":"app.js"}, - {"name":"93dub.img","url":"app-icon.js","evaluate":true} - ] - } + +{%- include_relative {{ apps.first }} -%} + +{%- for app in apps offset:1 -%} +,{%- include_relative {{ app }} -%} +{%- endfor -%} + ] diff --git a/apps/1button/metadata.json b/apps/1button/metadata.json new file mode 100644 index 000000000..6cfcb9310 --- /dev/null +++ b/apps/1button/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "1button", + "name": "One-Button-Tracker", + "version": "0.01", + "description": "A widget that turns BTN1 into a tracker, records time of button press/release.", + "icon": "widget.png", + "type": "widget", + "tags": "tool,quantifiedself,widget", + "supports": ["BANGLEJS"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"1button.wid.js","url":"widget.js"} + ], + "data": [{"name":"one_button_presses.csv","storageFile":true}] +} diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog index 5fbfe4fa3..1c18ca59b 100644 --- a/apps/93dub/ChangeLog +++ b/apps/93dub/ChangeLog @@ -1,3 +1,6 @@ 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 +0.06: Fix light theme issue, where widgets would end up on a light strip 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..f970eec5d 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); @@ -119,7 +122,13 @@ function draw(){ queueDraw(); } - +/** + * This watch is mostly dark, it does not make sense to respect the + * light theme as you end up with a white strip at the top for the + * widgets and black watch. So set the colours to the dark theme. + * + */ +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); draw(); //the following section is also from waveclk diff --git a/apps/93dub/metadata.json b/apps/93dub/metadata.json new file mode 100644 index 000000000..524780792 --- /dev/null +++ b/apps/93dub/metadata.json @@ -0,0 +1,17 @@ +{ "id": "93dub", + "name": "93 Dub", + "shortName":"93 Dub", + "icon": "93dub.png", + "screenshots": [{"url":"screenshot.png"}], + "version":"0.06", + "description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo", + "tags": "clock", + "type": "clock", + "supports":["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"93dub.app.js","url":"app.js"}, + {"name":"93dub.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/BLEcontroller/metadata.json b/apps/BLEcontroller/metadata.json new file mode 100644 index 000000000..bb28b2360 --- /dev/null +++ b/apps/BLEcontroller/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "BLEcontroller", + "name": "BLE Customisable Controller with Joystick", + "shortName": "BLE Controller", + "version": "0.01", + "description": "A configurable controller for BLE devices and robots, with a basic four direction joystick. Designed to be easy to customise so you can add your own menus.", + "icon": "BLEcontroller.png", + "tags": "tool,bluetooth", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": false, + "storage": [ + {"name":"BLEcontroller.app.js","url":"app.js"}, + {"name":"BLEcontroller.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/HRV/metadata.json b/apps/HRV/metadata.json new file mode 100644 index 000000000..9e0aed176 --- /dev/null +++ b/apps/HRV/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "HRV", + "name": "Heart Rate Variability monitor", + "shortName": "HRV monitor", + "version": "0.04", + "description": "Heart Rate Variability monitor, see Readme for more info", + "icon": "hrv.png", + "tags": "", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"HRV.app.js","url":"app.js"}, + {"name":"HRV.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/UI4swatch/metadata.json b/apps/UI4swatch/metadata.json new file mode 100644 index 000000000..379d173c3 --- /dev/null +++ b/apps/UI4swatch/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "UI4swatch", + "name": "UI 4 swatch", + "shortName": "UI 4 swatch", + "version": "0.01", + "description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.", + "icon": "app.png", + "tags": "Color,input,buttons,touch,UI", + "supports": ["BANGLEJS"], + "readme": "README.md", + "screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}], + "storage": [ + {"name":"UI4swatch.app.js","url":"app.js"}, + {"name":"UI4swatch.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/_example_app/add_to_apps.json b/apps/_example_app/metadata.json similarity index 89% rename from apps/_example_app/add_to_apps.json rename to apps/_example_app/metadata.json index cc28e1e93..e0d664338 100644 --- a/apps/_example_app/add_to_apps.json +++ b/apps/_example_app/metadata.json @@ -1,4 +1,3 @@ -// Create an entry in apps.json as follows: { "id": "7chname", "name": "My app's human readable name", "shortName":"Short Name", diff --git a/apps/_example_widget/add_to_apps.json b/apps/_example_widget/metadata.json similarity index 89% rename from apps/_example_widget/add_to_apps.json rename to apps/_example_widget/metadata.json index b55adce9d..ad4b7537d 100644 --- a/apps/_example_widget/add_to_apps.json +++ b/apps/_example_widget/metadata.json @@ -1,4 +1,3 @@ -// Create an entry in apps.json as follows: { "id": "7chname", "name": "My widget's human readable name", "shortName":"Short Name", diff --git a/apps/a_clock_timer/metadata.json b/apps/a_clock_timer/metadata.json new file mode 100644 index 000000000..cc61fc57b --- /dev/null +++ b/apps/a_clock_timer/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "a_clock_timer", + "name": "A Clock with Timer", + "version": "0.01", + "description": "A Clock with Timer, Map and Time Zones", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"a_clock_timer.app.js","url":"app.js"}, + {"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true} + ] +} 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/metadata.json b/apps/a_speech_timer/metadata.json new file mode 100644 index 000000000..6255a6b92 --- /dev/null +++ b/apps/a_speech_timer/metadata.json @@ -0,0 +1,16 @@ +{ +"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"], +"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], +"allow_emulator": true, +"storage": [ + {"name":"a_speech_timer.app.js","url":"app.js"}, + {"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true} +] +} 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("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"); +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/about/metadata.json b/apps/about/metadata.json new file mode 100644 index 000000000..6c22bdc56 --- /dev/null +++ b/apps/about/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "about", + "name": "About", + "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", + "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"bangle1-about-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"about.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, + {"name":"about.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]}, + {"name":"about.img","url":"app-icon.js","evaluate":true} + ], + "sortorder": -4 +} diff --git a/apps/ac_ac/Customizer.html b/apps/ac_ac/Customizer.html new file mode 100644 index 000000000..f2aa79920 --- /dev/null +++ b/apps/ac_ac/Customizer.html @@ -0,0 +1,890 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

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

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

+ +

Clock Size Calculation

+ +

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

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

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

+ custom URL: +

+ +

Clock Face

+ +

+ Click on the desired clock face: +

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

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

+ custom URL: +

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

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

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

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

+ without dots
+ with dots +

+ +

Clock Hands

+ +

+ Click on the desired clock hands: +

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

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

+ custom URL: +

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

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

+ Hollow Hand Fill Color: +

+ + + + + + + + + + +

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

+ Second Hand Color: +

+ + + + + + + + + + +

+ +

Complications

+ +

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

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

+ + +

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

+ +

Settings

+ +

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

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

+ Background Color: +

+ + + + + + + + + +

+ Foreground Color: +

+ + + + + + + + + +

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

+ + + +

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

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

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

-

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

-
- - - - - + -

Click

+ + diff --git a/apps/assistedgps/metadata.json b/apps/assistedgps/metadata.json new file mode 100644 index 000000000..1dbc42c87 --- /dev/null +++ b/apps/assistedgps/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "assistedgps", + "name": "Assisted GPS Update (AGPS)", + "version": "0.03", + "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", + "icon": "app.png", + "type": "RAM", + "tags": "tool,outdoors,agps", + "supports": ["BANGLEJS","BANGLEJS2"], + "custom": "custom.html", + "customConnect": true, + "storage": [] +} diff --git a/apps/astral/metadata.json b/apps/astral/metadata.json new file mode 100644 index 000000000..3317092db --- /dev/null +++ b/apps/astral/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "astral", + "name": "Astral Clock", + "version": "0.03", + "description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"astral.app.js","url":"app.js"}, + {"name":"astral.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json new file mode 100644 index 000000000..384c7fa1e --- /dev/null +++ b/apps/astrocalc/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "astrocalc", + "name": "Astrocalc", + "version": "0.02", + "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", + "icon": "astrocalc.png", + "tags": "app,sun,moon,cycles,tool,outdoors", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "storage": [ + {"name":"astrocalc.app.js","url":"astrocalc-app.js"}, + {"name":"suncalc.js","url":"suncalc.js"}, + {"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true}, + {"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true}, + {"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true}, + {"name":"waning-crescent.img","url":"waning-crescent-icon.js","evaluate":true}, + {"name":"waning-gibbous.img","url":"waning-gibbous-icon.js","evaluate":true}, + {"name":"full.img","url":"full-icon.js","evaluate":true}, + {"name":"new.img","url":"new-icon.js","evaluate":true}, + {"name":"waxing-gibbous.img","url":"waxing-gibbous-icon.js","evaluate":true}, + {"name":"waxing-crescent.img","url":"waxing-crescent-icon.js","evaluate":true} + ] +} diff --git a/apps/astroid/metadata.json b/apps/astroid/metadata.json new file mode 100644 index 000000000..abb3681ff --- /dev/null +++ b/apps/astroid/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "astroid", + "name": "Asteroids!", + "version": "0.03", + "description": "Retro asteroids game", + "icon": "asteroids.png", + "screenshots": [{"url":"screenshot_asteroids.png"}], + "tags": "game", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"astroid.app.js","url":"asteroids.js"}, + {"name":"astroid.img","url":"asteroids-icon.js","evaluate":true} + ] +} diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog index 7b83706bf..7a902a731 100644 --- a/apps/authentiwatch/ChangeLog +++ b/apps/authentiwatch/ChangeLog @@ -1 +1,4 @@ 0.01: First release +0.02: Fix JSON save format +0.03: Add "Calculating" placeholder, update JSON save format +0.04: Fix tapping at very bottom of list, exit on inactivity 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/authentiwatch/metadata.json b/apps/authentiwatch/metadata.json new file mode 100644 index 000000000..7a0138d24 --- /dev/null +++ b/apps/authentiwatch/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "authentiwatch", + "name": "2FA Authenticator", + "shortName": "AuthWatch", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "version": "0.04", + "description": "Google Authenticator compatible tool.", + "tags": "tool", + "interface": "interface.html", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"authentiwatch.app.js","url":"app.js"}, + {"name":"authentiwatch.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"authentiwatch.json"}] +} diff --git a/apps/awairmonitor/ChangeLog b/apps/awairmonitor/ChangeLog new file mode 100644 index 000000000..71d6399c4 --- /dev/null +++ b/apps/awairmonitor/ChangeLog @@ -0,0 +1,3 @@ +0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11) +0.02: The app is now a clock, the data is greyed after the connection is lost (2021/12/22) +0.03: Set the Awair's IP directly on the webpage (2021/12/27) diff --git a/apps/awairmonitor/README.md b/apps/awairmonitor/README.md new file mode 100644 index 000000000..f4c7c42c4 --- /dev/null +++ b/apps/awairmonitor/README.md @@ -0,0 +1,21 @@ +# 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 retrieve the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection +* How to get started + * Launch the Awair Monitor app on your BangleJS + * Open awair_to_bangle.html on Chrome (desktop or Android), input the IP address of your Awair device, 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..9123a9c2c --- /dev/null +++ b/apps/awairmonitor/app.js @@ -0,0 +1,108 @@ +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; + +var display_frozen = false; + +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); + + if (last_update != bt_last_update) { + display_frozen = false; + 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) && (internal_last_update < last_update + 60)) { + g.drawString("Trying to reconnect since " + (internal_last_update - last_update), 88, 164); + } else if (internal_last_update > last_update + 5) { + display_frozen = true; + g.drawString("Waiting for connection", 88, 164); + } + + if (display_frozen) { g.setColor("#888"); } + + 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); + + for (i = 0; i < 10; i++) { + if (display_frozen) { g.setColor("#888"); } + + // 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 +Bangle.setUI("clock"); +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..69c52499f --- /dev/null +++ b/apps/awairmonitor/awair_to_bangle.html @@ -0,0 +1,673 @@ + + + + + + + + + + +

+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: Launch the Awair Monitor app on your BangleJS +

+Step 3: Input your Awair IP address and click the Connect button: + + +

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

+Step 5: Once you are done, click the Disconnect button to properly close the Blutooth connection +

+

+ +

+ +
+
+
+
+
+ +(buf)); + } + + function str2ab(str) { + var buf = new ArrayBuffer(str.length); + var bufView = new Uint8Array(buf); + for (var i=0, strLen=str.length; i Device UUIDs: ' + device.uuids.join('\n' + ' '.repeat(21))); + device.addEventListener('gattserverdisconnected', function() { + log(1, "Disconnected (gattserverdisconnected)"); + connection.close(); + }); + connection.device = device; + connection.reconnect(callback); + }).catch(function(error) { + log(1, 'ERROR: ' + error); + connection.close(); + }); + + connection.reconnect = function(callback) { + connection.device.gatt.connect().then(function(server) { + log(1, "Connected"); + btServer = server; + return server.getPrimaryService(NORDIC_SERVICE); + }).then(function(service) { + log(2, "Got service"); + btService = service; + return btService.getCharacteristic(NORDIC_RX); + }).then(function (characteristic) { + rxCharacteristic = characteristic; + log(2, "RX characteristic:"+JSON.stringify(rxCharacteristic)); + rxCharacteristic.addEventListener('characteristicvaluechanged', function(event) { + var dataview = event.target.value; + var data = ab2str(dataview.buffer); + if (data.length > chunkSize) { + log(2, "Received packet of length "+data.length+", increasing chunk size"); + chunkSize = data.length; + } + if (puck.flowControl) { + for (var i=0;i pause upload"); + flowControlXOFF = true; + } else if (ch==17) {// XON + log(2,"XON received => resume upload"); + flowControlXOFF = false; + } else + remove = false; + if (remove) { // remove character + data = data.substr(0,i-1)+data.substr(i+1); + i--; + } + } + } + log(3, "Received "+JSON.stringify(data)); + connection.emit('data', data); + }); + return rxCharacteristic.startNotifications(); + }).then(function() { + return btService.getCharacteristic(NORDIC_TX); + }).then(function (characteristic) { + txCharacteristic = characteristic; + log(2, "TX characteristic:"+JSON.stringify(txCharacteristic)); + }).then(function() { + connection.txInProgress = false; + connection.isOpen = true; + connection.isOpening = false; + isBusy = false; + queue = []; + callback(connection); + connection.emit('open'); + // if we had any writes queued, do them now + connection.write(); + }).catch(function(error) { + log(1, 'ERROR: ' + error); + connection.close(); + }); + }; + + return connection; + }; + + // ---------------------------------------------------------- + var connection; + /* convenience function... Write data, call the callback with data: + callbackNewline = false => if no new data received for ~0.2 sec + callbackNewline = true => after a newline */ + function write(data, callback, callbackNewline) { + if (!checkIfSupported()) return; + + let result; + /// If there wasn't a callback function, then promisify + if (typeof callback !== 'function') { + callbackNewline = callback; + + result = new Promise((resolve, reject) => callback = (value, err) => { + if (err) reject(err); + else resolve(value); + }); + } + + if (isBusy) { + log(3, "Busy - adding Puck.write to queue"); + queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline}); + return result; + } + + var cbTimeout; + function onWritten() { + if (callbackNewline) { + connection.cb = function(d) { + var newLineIdx = connection.received.indexOf("\n"); + if (newLineIdx>=0) { + var l = connection.received.substr(0,newLineIdx); + connection.received = connection.received.substr(newLineIdx+1); + connection.cb = undefined; + if (cbTimeout) clearTimeout(cbTimeout); + cbTimeout = undefined; + if (callback) + callback(l); + isBusy = false; + handleQueue(); + } + }; + } + // wait for any received data if we have a callback... + var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data + var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/; + var maxDataTime = dataWaitTime; // max time we wait after having received data + cbTimeout = setTimeout(function timeout() { + cbTimeout = undefined; + if (maxTime) maxTime--; + if (maxDataTime) maxDataTime--; + if (connection.hadData) maxDataTime=dataWaitTime; + if (maxDataTime && maxTime) { + cbTimeout = setTimeout(timeout, 100); + } else { + connection.cb = undefined; + if (callback) + callback(connection.received); + isBusy = false; + handleQueue(); + connection.received = ""; + } + connection.hadData = false; + }, 100); + } + + if (connection && (connection.isOpen || connection.isOpening)) { + if (!connection.txInProgress) connection.received = ""; + isBusy = true; + connection.write(data, onWritten); + return result + } + + connection = connect(function(puck) { + if (!puck) { + connection = undefined; + if (callback) callback(null); + return; + } + connection.received = ""; + connection.on('data', function(d) { + connection.received += d; + connection.hadData = true; + if (connection.cb) connection.cb(d); + }); + connection.on('close', function(d) { + connection = undefined; + }); + isBusy = true; + connection.write(data, onWritten); + }); + + return result + } + + // ---------------------------------------------------------- + + var puck = { + /// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all. + debug : 1, + /// Should we use flow control? Default is true + flowControl : true, + /// Used internally to write log information - you can replace this with your own function + log : function(level, s) { if (level <= this.debug) console.log(" "+s)}, + /// Called with the current send progress or undefined when done - you can replace this with your own function + writeProgress : function (charsSent, charsTotal) { + //console.log(charsSent + "/" + charsTotal); + }, + /** Connect to a new device - this creates a separate + connection to the one `write` and `eval` use. */ + connect : connect, + /// Write to Puck.js and call back when the data is written. Creates a connection if it doesn't exist + write : write, + /// Evaluate an expression and call cb with the result. Creates a connection if it doesn't exist + eval : function(expr, cb) { + + const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true) + .then(function (d) { + try { + return JSON.parse(d); + } catch (e) { + log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString()); + return Promise.reject(d); + } + }); + + + if (cb) { + return void response.then(cb, (err) => cb(null, err)); + } else { + return response; + } + + }, + /// Write the current time to the Puck + setTime : function(cb) { + var d = new Date(); + var cmd = 'setTime('+(d.getTime()/1000)+');'; + // in 1v93 we have timezones too + cmd += 'if (E.setTimeZone) E.setTimeZone('+d.getTimezoneOffset()/-60+');\n'; + write(cmd, cb); + }, + /// Did `write` and `eval` manage to create a connection? + isConnected : function() { + return connection!==undefined; + }, + /// get the connection used by `write` and `eval` + getConnection : function() { + return connection; + }, + /// Close the connection used by `write` and `eval` + close : function() { + if (connection) + connection.close(); + }, + /** Utility function to fade out everything on the webpage and display + a window saying 'Click to continue'. When clicked it'll disappear and + 'callback' will be called. This is useful because you can't initialise + Web Bluetooth unless you're doing so in response to a user input.*/ + modal : function(callback) { + var e = document.createElement('div'); + e.style = 'position:absolute;top:0px;left:0px;right:0px;bottom:0px;opacity:0.5;z-index:100;background:black;'; + e.innerHTML = '
Click to Continue...
'; + e.onclick = function(evt) { + callback(); + evt.preventDefault(); + document.body.removeChild(e); + }; + document.body.appendChild(e); + } + }; + return puck; +})); + + + + + + diff --git a/apps/fwupdate/metadata.json b/apps/fwupdate/metadata.json new file mode 100644 index 000000000..c44a0115a --- /dev/null +++ b/apps/fwupdate/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "fwupdate", + "name": "Firmware Update", + "version": "0.03", + "description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates", + "icon": "app.png", + "type": "RAM", + "tags": "tools,system", + "supports": ["BANGLEJS2"], + "custom": "custom.html", + "customConnect": true, + "storage": [], + "sortorder": 20 +} diff --git a/apps/gallifr/metadata.json b/apps/gallifr/metadata.json new file mode 100644 index 000000000..9ce7d7f97 --- /dev/null +++ b/apps/gallifr/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "gallifr", + "name": "Time Traveller's Chronometer", + "shortName": "Time Travel Clock", + "version": "0.02", + "description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.", + "icon": "gallifr.png", + "screenshots": [{"url":"screenshot_time.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"gallifr.app.js","url":"app.js"}, + {"name":"gallifr.img","url":"app-icon.js","evaluate":true}, + {"name":"gallifr.settings.js","url":"settings.js"} + ], + "data": [{"name":"gallifr.json"}] +} 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/gbdebug/metadata.json b/apps/gbdebug/metadata.json new file mode 100644 index 000000000..20b709d47 --- /dev/null +++ b/apps/gbdebug/metadata.json @@ -0,0 +1,14 @@ +{ "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} + ] +} 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/gbmusic/metadata.json b/apps/gbmusic/metadata.json new file mode 100644 index 000000000..9400f70e0 --- /dev/null +++ b/apps/gbmusic/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "gbmusic", + "name": "Gadgetbridge Music Controls", + "shortName": "Music Controls", + "version": "0.08", + "description": "Control the music on your Gadgetbridge-connected phone", + "icon": "icon.png", + "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], + "type": "app", + "tags": "tools,bluetooth,gadgetbridge,music", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"gbmusic.app.js","url":"app.js"}, + {"name":"gbmusic.settings.js","url":"settings.js"}, + {"name":"gbmusic.wid.js","url":"widget.js"}, + {"name":"gbmusic.img","url":"icon.js","evaluate":true} + ], + "data": [{"name":"gbmusic.json"},{"name":"gbmusic.load.json"}] +} 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/metadata.json b/apps/gbridge/metadata.json new file mode 100644 index 000000000..cdbc95c11 --- /dev/null +++ b/apps/gbridge/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "gbridge", + "name": "Gadgetbridge", + "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", + "supports": ["BANGLEJS","BANGLEJS2"], + "dependencies": {"notify":"type"}, + "readme": "README.md", + "storage": [ + {"name":"gbridge.settings.js","url":"settings.js"}, + {"name":"gbridge.img","url":"app-icon.js","evaluate":true}, + {"name":"gbridge.wid.js","url":"widget.js"} + ], + "data": [{"name":"gbridge.json"}] +} 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/gbtwist/metadata.json b/apps/gbtwist/metadata.json new file mode 100644 index 000000000..24f39a9d4 --- /dev/null +++ b/apps/gbtwist/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "gbtwist", + "name": "Gadgetbridge Twist Control", + "shortName": "Twist Control", + "version": "0.01", + "description": "Shake your wrist to control your music app via Gadgetbridge", + "icon": "app.png", + "type": "app", + "tags": "tools,bluetooth,gadgetbridge,music", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": false, + "storage": [ + {"name":"gbtwist.app.js","url":"app.js"}, + {"name":"gbtwist.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/geissclk/metadata.json b/apps/geissclk/metadata.json new file mode 100644 index 000000000..456854dbd --- /dev/null +++ b/apps/geissclk/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "geissclk", + "name": "Geiss Clock", + "version": "0.03", + "description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation", + "icon": "clock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"geissclk.app.js","url":"clock.js"}, + {"name":"geissclk.precompute.js","url":"precompute.js"}, + {"name":"geissclk.img","url":"clock-icon.js","evaluate":true} + ], + "data": [{"name":"geissclk.0.map"},{"name":"geissclk.1.map"},{"name":"geissclk.2.map"},{"name":"geissclk.3.map"},{"name":"geissclk.4.map"},{"name":"geissclk.5.map"},{"name":"geissclk.0.pal"},{"name":"geissclk.1.pal"},{"name":"geissclk.2.pal"}] +} diff --git a/apps/gesture/metadata.json b/apps/gesture/metadata.json new file mode 100644 index 000000000..952faa5ea --- /dev/null +++ b/apps/gesture/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "gesture", + "name": "Gesture Test", + "version": "0.01", + "description": "BETA! Uploads a basic Tensorflow Gesture model, and then outputs each gesture as a message", + "icon": "gesture.png", + "type": "app", + "tags": "gesture,ai", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"gesture.app.js","url":"gesture.js"}, + {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, + {"name":".tfmodel","url":"gesture-tfmodel.js","evaluate":true}, + {"name":"gesture.img","url":"gesture-icon.js","evaluate":true} + ] +} diff --git a/apps/getup/metadata.json b/apps/getup/metadata.json new file mode 100644 index 000000000..0c5a7cc5b --- /dev/null +++ b/apps/getup/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "getup", + "name": "Get Up", + "shortName": "Get Up", + "version": "0.01", + "description": "Reminds you to getup every x minutes. Sitting to long is dangerous!", + "icon": "app.png", + "tags": "tools,health", + "supports": ["BANGLEJS"], + "readme": "README.md", + "screenshots": [{"url":"bangle1-get-up-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"getup.app.js","url":"app.js"}, + {"name":"getup.settings.js","url":"settings.js"}, + {"name":"getup.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/gmeter/metadata.json b/apps/gmeter/metadata.json new file mode 100644 index 000000000..550153f31 --- /dev/null +++ b/apps/gmeter/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "gmeter", + "name": "G-Meter", + "shortName": "G-Meter", + "version": "0.01", + "description": "Simple G-Meter", + "icon": "app.png", + "tags": "", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"gmeter.app.js","url":"app.js"}, + {"name":"gmeter.img","url":"app-icon.js","evaluate":true} + ] +} 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/metadata.json b/apps/golfscore/metadata.json new file mode 100644 index 000000000..8bef32765 --- /dev/null +++ b/apps/golfscore/metadata.json @@ -0,0 +1,15 @@ +{ "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} + ] +} 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/gpsautotime/metadata.json b/apps/gpsautotime/metadata.json new file mode 100644 index 000000000..a64a45f6d --- /dev/null +++ b/apps/gpsautotime/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "gpsautotime", + "name": "GPS auto time", + "shortName": "GPS auto time", + "version": "0.01", + "description": "A widget that automatically updates the Bangle.js time to the GPS time whenever there is a valid GPS fix.", + "icon": "widget.png", + "type": "widget", + "tags": "widget,gps", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"gpsautotime.wid.js","url":"widget.js"} + ] +} diff --git a/apps/gpsinfo/ChangeLog b/apps/gpsinfo/ChangeLog index 381412c16..5bb531bc7 100644 --- a/apps/gpsinfo/ChangeLog +++ b/apps/gpsinfo/ChangeLog @@ -2,3 +2,8 @@ 0.03: Show number of satellites while waiting for fix 0.04: Add Maidenhead readout of GPS location 0.05: Refactor to use 'layout' library for multi-device support +0.06: Add number of satellites in view and fix crash with GPS time +0.07: Resolve one FIFO_FULL case and exit App with button press +0.08: Leave GPS power switched on on exit (will switch off after 0.5 seconds anyway) +0.09: Fix FIFO_FULL error +0.10: Show satellites "in view" separated by GNS-system diff --git a/apps/gpsinfo/gps-info.js b/apps/gpsinfo/gps-info.js index df888651a..0eca2ccf5 100644 --- a/apps/gpsinfo/gps-info.js +++ b/apps/gpsinfo/gps-info.js @@ -4,7 +4,7 @@ function satelliteImage() { var Layout = require("Layout"); var layout; -Bangle.setGPSPower(1, "app"); +//Bangle.setGPSPower(1, "app"); E.showMessage("Loading..."); // avoid showing rubbish on screen var lastFix = { @@ -16,13 +16,19 @@ var lastFix = { time: 0, satellites: 0 }; -var nofix = 0; +var SATinView = 0, lastSATinView = -1, nofGP = 0, nofBD = 0, nofGL = 0; +const leaveNofixLayout = 1; // 0 = stay on initial screen for debugging (default = 1) +var listenerGPSraw = 0; function formatTime(now) { - var fd = now.toUTCString().split(" "); - var time = fd[4].substr(0, 5); - var date = [fd[0], fd[1], fd[2]].join(" "); - return time + " - " + date; + if (now == undefined) { + return "no GPS time available"; + } else { + var fd = now.toUTCString().split(" "); + var time = fd[4].substr(0, 5); + var date = [fd[0], fd[1], fd[2]].join(" "); + return time + " - " + date; + } } function getMaidenHead(param1,param2){ var lat=-100.0; @@ -56,7 +62,7 @@ function getMaidenHead(param1,param2){ function onGPS(fix) { if (lastFix.fix != fix.fix) { // if fix is different, change the layout - if (fix.fix) { + if (fix.fix && leaveNofixLayout) { layout = new Layout( { type:"v", c: [ {type:"txt", font:"6x8:2", label:"GPS Info" }, @@ -77,17 +83,21 @@ function onGPS(fix) { {type:"txt", font:"6x8", label:"Waiting for GPS" }, {type:"h", c: [ {type:"txt", font:"10%", label:fix.satellites, pad:2, id:"sat" }, - {type:"txt", font:"6x8", pad:3, label:"Satellites" } + {type:"txt", font:"6x8", pad:3, label:"Satellites used" } ]}, - {type:"txt", font:"6x8", label:"", id:"progress" } - ]},{lazy:true}); + {type:"txt", font:"6x8", label:"", fillx:true, id:"progress" } + ]},{lazy:false}); } g.clearRect(0,24,g.getWidth(),g.getHeight()); layout.render(); } - lastFix = fix; - if (fix.fix) { - nofix = 0; + if (fix.fix && leaveNofixLayout) { + if (listenerGPSraw == 1) { + Bangle.removeListener('GPS-raw', onGPSraw); + listenerGPSraw = 0; + lastSATinView = -1; + Bangle.buzz(50); + } var locale = require("locale"); var satellites = fix.satellites; var maidenhead = getMaidenHead(fix.lat,fix.lon); @@ -98,14 +108,53 @@ function onGPS(fix) { layout.time.label = "Time: "+formatTime(fix.time); layout.sat.label = "Satellites: "+satellites; layout.maidenhead.label = "Maidenhead: "+maidenhead; + layout.render(); } else { - layout.sat.label = fix.satellites; - nofix = (nofix+1) % 4; - layout.progress.label = ".".repeat(nofix) + " ".repeat(4-nofix); + if (fix.satelites != lastFix.satelites) { + layout.clear(layout.sat); + layout.sat.label = fix.satellites; + layout.render(layout.sat); + } + if (SATinView != lastSATinView) { + if (!leaveNofixLayout) SATinView = -1; + lastSATinView = SATinView; + layout.clear(layout.progress); + layout.progress.label = "in view GP/BD/GL: " + nofGP + " " + nofBD + " " + nofGL; + // console.log("in view GP/BD/GL: " + nofGP + " " + nofBD + " " + nofGL); + layout.render(layout.progress); + } } - layout.render(); + + if (listenerGPSraw == 0 && !fix.fix) { + setTimeout(() => Bangle.on('GPS-raw', onGPSraw), 10); + listenerGPSraw = 1; + } + lastFix = fix; } +function onGPSraw(nmea) { + if (nmea.slice(3,6) == "GSV") { + // console.log(nmea.slice(1,3) + " " + nmea.slice(11,13)); + if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13)); + if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13)); + if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13)); + SATinView = nofGP + nofBD + nofGL; + } +} + + Bangle.loadWidgets(); Bangle.drawWidgets(); Bangle.on('GPS', onGPS); +//Bangle.on('GPS-raw', onGPSraw); +Bangle.setGPSPower(1, "app"); + +function exitApp() { + load(); +} + +setWatch(_=>exitApp(), BTN1); +if (global.BTN2) { + setWatch(_=>exitApp(), BTN2); + setWatch(_=>exitApp(), BTN3); +} diff --git a/apps/gpsinfo/metadata.json b/apps/gpsinfo/metadata.json new file mode 100644 index 000000000..60bd90c03 --- /dev/null +++ b/apps/gpsinfo/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "gpsinfo", + "name": "GPS Info", + "version": "0.10", + "description": "An application that displays information about altitude, lat/lon, satellites and time", + "icon": "gps-info.png", + "type": "app", + "tags": "gps", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"gpsinfo.app.js","url":"gps-info.js"}, + {"name":"gpsinfo.img","url":"gps-info-icon.js","evaluate":true} + ] +} diff --git a/apps/gpsnav/metadata.json b/apps/gpsnav/metadata.json new file mode 100644 index 000000000..5c1830318 --- /dev/null +++ b/apps/gpsnav/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "gpsnav", + "name": "GPS Navigation", + "version": "0.05", + "description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor", + "icon": "icon.png", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS"], + "readme": "README.md", + "interface": "waypoints.html", + "storage": [ + {"name":"gpsnav.app.js","url":"app.min.js"}, + {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"waypoints.json","url":"waypoints.json"}] +} diff --git a/apps/gpspoilog/metadata.json b/apps/gpspoilog/metadata.json new file mode 100644 index 000000000..0a0902cea --- /dev/null +++ b/apps/gpspoilog/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "gpspoilog", + "name": "GPS POI Logger", + "shortName": "GPS POI Log", + "version": "0.01", + "description": "A simple app to log points of interest with their GPS coordinates and read them back onto your PC. Based on the https://www.espruino.com/Bangle.js+Storage tutorial", + "icon": "app.png", + "tags": "outdoors", + "supports": ["BANGLEJS"], + "interface": "interface.html", + "storage": [ + {"name":"gpspoilog.app.js","url":"app.js"}, + {"name":"gpspoilog.img","url":"app-icon.js","evaluate":true} + ] +} 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/metadata.json b/apps/gpsrec/metadata.json new file mode 100644 index 000000000..088b8c741 --- /dev/null +++ b/apps/gpsrec/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "gpsrec", + "name": "GPS Recorder", + "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", + "storage": [ + {"name":"gpsrec.app.js","url":"app.js"}, + {"name":"gpsrec.img","url":"app-icon.js","evaluate":true}, + {"name":"gpsrec.wid.js","url":"widget.js"}, + {"name":"gpsrec.settings.js","url":"settings.js"} + ], + "data": [{"name":"gpsrec.json"},{"wildcard":".gpsrc?","storageFile":true}] +} 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/gpssetup/metadata.json b/apps/gpssetup/metadata.json new file mode 100644 index 000000000..b8b6dfc23 --- /dev/null +++ b/apps/gpssetup/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "gpssetup", + "name": "GPS Setup", + "shortName": "GPS Setup", + "version": "0.02", + "description": "Configure the GPS power options and store them in the GPS nvram", + "icon": "gpssetup.png", + "tags": "gps,tools,outdoors", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"gpssetup","url":"gpssetup.js"}, + {"name":"gpssetup.settings.js","url":"settings.js"}, + {"name":"gpssetup.app.js","url":"app.js"}, + {"name":"gpssetup.img","url":"icon.js","evaluate":true} + ], + "data": [{"name":"gpssetup.settings.json","url":"settings.json"}] +} diff --git a/apps/gpstime/metadata.json b/apps/gpstime/metadata.json new file mode 100644 index 000000000..27ee16105 --- /dev/null +++ b/apps/gpstime/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "gpstime", + "name": "GPS Time", + "version": "0.05", + "description": "Update the Bangle.js's clock based on the time from the GPS receiver", + "icon": "gpstime.png", + "tags": "tool,gps", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"gpstime.app.js","url":"gpstime.js"}, + {"name":"gpstime.img","url":"gpstime-icon.js","evaluate":true} + ] +} diff --git a/apps/gpstimeserver/metadata.json b/apps/gpstimeserver/metadata.json new file mode 100644 index 000000000..973fa34a9 --- /dev/null +++ b/apps/gpstimeserver/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "gpstimeserver", + "name": "GPS Time Server", + "version": "0.01", + "description": "A widget which automatically starts the GPS and turns Bangle.js into a Bluetooth time server.", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"gpstimeserver.wid.js","url":"widget.js"} + ] +} diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/Changelog index 7f837e50e..e4a0bdfe8 100644 --- a/apps/gpstouch/Changelog +++ b/apps/gpstouch/Changelog @@ -1 +1,2 @@ 0.01: First version +0.02: Enchanced contrast of icon image diff --git a/apps/gpstouch/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/gpstouch/gpstouch.icon.js b/apps/gpstouch/gpstouch.icon.js index c4cf85676..3e05da0ff 100644 --- a/apps/gpstouch/gpstouch.icon.js +++ b/apps/gpstouch/gpstouch.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA=")) +require("heatshrink").decompress(atob("mEw4UA///iADCn+EqoAWqAuJgoLcn/8BZENGwNwBY/VBYNXBY0DJ4fABYoiCEggLDmtX1Wq6tcBYvVrQLB0owCBYdVtQLB1NVBYg6BBQIABHgQLCgIuCGAVABYcNqwtBGIOVJAILFyoCCBY5eBBdo7IgIIB1t6BYJfENZaDB9QKB1aDFBYKbEBYizBrwLB2qnFdwSmCX401cYdUBZTjGfYgHCBZB2BBYhUBAARSBBYhICAAIGCBYkVBQJSCBYpICIwQLFHgQ6CBYo8CHQQLFHgQFDBYsVQIQLHgo6DBY0BHQYLGgY6DBYwAFBbCjDACY")) diff --git a/apps/gpstouch/gpstouch.png b/apps/gpstouch/gpstouch.png index c411356ae..a40419a3f 100644 Binary files a/apps/gpstouch/gpstouch.png and b/apps/gpstouch/gpstouch.png differ diff --git a/apps/gpstouch/metadata.json b/apps/gpstouch/metadata.json new file mode 100644 index 000000000..45e3d786b --- /dev/null +++ b/apps/gpstouch/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "gpstouch", + "name": "GPS Touch", + "version": "0.02", + "description": "A touch based GPS watch, shows OS map reference", + "icon": "gpstouch.png", + "screenshots": [{"url":"screenshot4.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot1.png"}], + "tags": "tools,app", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"geotools","url":"geotools.js"}, + {"name":"gpstouch.app.js","url":"gpstouch.app.js"}, + {"name":"gpstouch.img","url":"gpstouch.icon.js","evaluate":true} + ] +} diff --git a/apps/grocery/metadata.json b/apps/grocery/metadata.json new file mode 100644 index 000000000..8c0e34dff --- /dev/null +++ b/apps/grocery/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "grocery", + "name": "Grocery", + "version": "0.02", + "description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.", + "icon": "grocery.png", + "type": "app", + "tags": "tool,outdoors,shopping,list", + "supports": ["BANGLEJS", "BANGLEJS2"], + "custom": "grocery.html", + "allow_emulator": true, + "storage": [ + {"name":"grocery.app.js","url":"app.js"}, + {"name":"grocery.img","url":"grocery-icon.js","evaluate":true} + ] +} diff --git a/apps/hamloc/metadata.json b/apps/hamloc/metadata.json new file mode 100644 index 000000000..932b639b7 --- /dev/null +++ b/apps/hamloc/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "hamloc", + "name": "QTH Locator / Maidenhead Locator System", + "shortName": "QTH Locator", + "version": "0.01", + "description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators", + "icon": "app.png", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"hamloc.app.js","url":"app.js"}, + {"name":"hamloc.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/hardalarm/metadata.json b/apps/hardalarm/metadata.json new file mode 100644 index 000000000..13a8fb920 --- /dev/null +++ b/apps/hardalarm/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "hardalarm", + "name": "Hard Alarm", + "shortName": "HardAlarm", + "version": "0.02", + "description": "Make sure you wake up! Count to the right number to turn off the alarm", + "icon": "app.png", + "tags": "tool,alarm,widget", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"hardalarm.app.js","url":"app.js"}, + {"name":"hardalarm.boot.js","url":"boot.js"}, + {"name":"hardalarm.js","url":"hardalarm.js"}, + {"name":"hardalarm.img","url":"app-icon.js","evaluate":true}, + {"name":"hardalarm.wid.js","url":"widget.js"} + ], + "data": [{"name":"hardalarm.json"}] +} diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog index aaa55d01a..f70653d58 100644 --- a/apps/hcclock/ChangeLog +++ b/apps/hcclock/ChangeLog @@ -1,2 +1,3 @@ -0.01: base code -0.02: saved settings when switching color scheme \ No newline at end of file +0.01: Base code +0.02: Saved settings when switching color scheme +0.03: Added Button 3 opening messages (if app is installed) \ No newline at end of file diff --git a/apps/hcclock/README.md b/apps/hcclock/README.md index 328f1fe03..122401dd8 100644 --- a/apps/hcclock/README.md +++ b/apps/hcclock/README.md @@ -5,6 +5,7 @@ A High-contrast, black-on-white or white-on-black clock displaying huge pixel di ## Usage * BTN 1 switches between the two modes : black-on-white or white-on-black +* BTN 3 opens the messages (if installed, and there are new messages) * That's it! ## Issues and Requests diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js index 4664dd763..de5163996 100644 --- a/apps/hcclock/hcclock.app.js +++ b/apps/hcclock/hcclock.app.js @@ -129,6 +129,7 @@ function updateTime() g.setFontAlign(0, -1, 0); g.drawString(fmtDate(d,mo,y,hour), 120, 120); } + drawMessages(); } function drawDigits(x, value) @@ -222,6 +223,55 @@ function flipColors() setColorScheme(0); } +////////////////////////////////////////// +// +// MESSAGE HANDLING() +// + +let messages_installed = require("Storage").read("messages.app.js") != undefined; + +function handleMessages() +{ + if(messages_installed && hasMessages() > 0) + { + E.showMessage("Loading Messages..."); + load("messages.app.js"); + } +} + +function hasMessages() +{ + if(!messages_installed) + return false; + + var messages = require("Storage").readJSON("messages.json",1)||[]; + if (messages.some(m=>m.new)) + return true; + else + return false; +} + +let msg = atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA=="); +let had_messages = false; + +function drawMessages() +{ + if(!had_messages && hasMessages()) { + g.setColor(255,255,255); + g.drawImage(msg, 184, 212); + g.setFont("6x8", 2); + g.setFontAlign(0, -1, 0); + g.drawString(">", 224, 216); + had_messages = true; + } + else if (had_messages && !hasMessages()) + { + g.setColor(0,0,0); + g.fillRect(180, 210, 240, 240); + had_messages = false; + } +} + ////////////////////////////////////////// // // MAIN FUNCTION() @@ -238,6 +288,7 @@ setInterval(updateTime, interval); // Handle Button Press setWatch(flipColors, BTN1, true); setWatch(Bangle.showLauncher, BTN2, false); +setWatch(handleMessages, BTN3, true); // Handle redraw on LCD on / fullscreen notifications dismissed Bangle.on('lcdPower', (on) => { if(on) redraw(); }); diff --git a/apps/hcclock/metadata.json b/apps/hcclock/metadata.json new file mode 100644 index 000000000..e372a0a2c --- /dev/null +++ b/apps/hcclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "hcclock", + "name": "Hi-Contrast Clock", + "version": "0.03", + "description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.", + "icon": "hcclock-icon.png", + "type": "clock", + "tags": "clock", + "screenshots": [{"url":"bangle1-high-contrast-clock-screenshot.png"}], + "supports": ["BANGLEJS"], + "allow_emulator": true, + "storage": [ + {"name":"hcclock.app.js","url":"hcclock.app.js"}, + {"name":"hcclock.img","url":"hcclock-icon.js","evaluate":true} + ] +} diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index bde4f8ab8..c65cc3ab4 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -7,3 +7,4 @@ 0.06: Fix daily health summary for movement (a line got deleted!) 0.07: Added coloured bar charts 0.08: Suppress bleed through of E.showMenu's when displaying bar charts +0.09: Fix file naming so months are 1-based (not 0) (fix #1119) diff --git a/apps/health/boot.js b/apps/health/boot.js index 386d75833..c72e62b41 100644 --- a/apps/health/boot.js +++ b/apps/health/boot.js @@ -27,7 +27,7 @@ Bangle.on("health", health => { const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN; function getRecordFN(d) { - return "health-"+d.getFullYear()+"-"+d.getMonth()+".raw"; + return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".raw"; } function getRecordIdx(d) { return (DB_RECORDS_PER_DAY*(d.getDate()-1)) + diff --git a/apps/health/lib.js b/apps/health/lib.js index 70305bff8..2e3e0c002 100644 --- a/apps/health/lib.js +++ b/apps/health/lib.js @@ -6,7 +6,7 @@ const DB_HEADER_LEN = 8; const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN; function getRecordFN(d) { - return "health-"+d.getFullYear()+"-"+d.getMonth()+".raw"; + return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".raw"; } function getRecordIdx(d) { return (DB_RECORDS_PER_DAY*(d.getDate()-1)) + diff --git a/apps/health/metadata.json b/apps/health/metadata.json new file mode 100644 index 000000000..b96087e1b --- /dev/null +++ b/apps/health/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "health", + "name": "Health Tracking", + "version": "0.09", + "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"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"health.app.js","url":"app.js"}, + {"name":"health.img","url":"app-icon.js","evaluate":true}, + {"name":"health.boot.js","url":"boot.js"}, + {"name":"health","url":"lib.js"} + ] +} diff --git a/apps/heart/metadata.json b/apps/heart/metadata.json new file mode 100644 index 000000000..6265dbfef --- /dev/null +++ b/apps/heart/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "heart", + "name": "Heart Rate Recorder", + "shortName": "HRM Record", + "version": "0.07", + "description": "Application that allows you to record your heart rate. Can run in background", + "icon": "app.png", + "tags": "tool,health,widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name":"heart.app.js","url":"app.js"}, + {"name":"heart.img","url":"app-icon.js","evaluate":true}, + {"name":"heart.wid.js","url":"widget.js"} + ], + "data": [{"name":"heart.json"},{"wildcard":".heart?","storageFile":true}] +} 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) + \ 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..372033d58 --- /dev/null +++ b/apps/hebrew_calendar/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cBz3v///s/immVkf27Xu+ec5PikmSpMt21JAoNbtgIBkm27IOCAQVt23JkmW7dt2VJku27YREpdkyQaBy3JkoRBloLDF4YCDHAQCBHAQ7DyVeAQN2km8BAPsBwfYAQMlDwYbGK4VdkmJlMk2fpCIVn6QRC5+kC4WRCIO774ICu+/CIXvvwRDAQOXt5gCl3eQIIyB3qNEAQP7CIUl+wRCpftCIwCglbmBABvRkzQBZoICLEoMAAB0JCIUUgNogXQgughdAi2AlARF2WBsEC4EEwEJoESwNlCLGLssW5UsylZtGy6IRFgiNLwARXrMk2VJbYPJkrYBCIsFyWLksWpRHClGyqNk0BrIoBrCgB9KssC5QRBrNABAQRIgFJomC5AIECIkSgAEBrMsy1KgGQgMgCI8sagJ6BqEFygRHi1QgmUCgICCwELdIxoBQAOALIMBPQJuBCI3KKYImBEAMlJoNACIpoBoEWpJZBpMogRuBCI0JHwLIByRrBiwiCCIovCLIY7CCI5TBBwJHBCgJfBCI7+CLIb4CCJAvBLIY7CCJMC7JZBpZXDCJA4CGQgRbgXbtmW7Y1MLIJHBLIkC6ARGBwIRCBQYRigu2CI4AHCIOACJxrEQ44AHpIA=")) 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..ad9ec9af7 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/hebrew_calendar/metadata.json b/apps/hebrew_calendar/metadata.json new file mode 100644 index 000000000..a2b7932b6 --- /dev/null +++ b/apps/hebrew_calendar/metadata.json @@ -0,0 +1,30 @@ +{ + "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 + } + ] +} diff --git a/apps/helloworld/metadata.json b/apps/helloworld/metadata.json new file mode 100644 index 000000000..b8fe1b1e3 --- /dev/null +++ b/apps/helloworld/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "helloworld", + "name": "hello, world!", + "shortName": "hello world", + "version": "0.02", + "description": "A cross cultural hello world!/hola mundo! app with colors and languages", + "icon": "app.png", + "tags": "input,interface,buttons,touch", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"helloworld.app.js","url":"app.js"}, + {"name":"helloworld.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/hidbkbd/metadata.json b/apps/hidbkbd/metadata.json new file mode 100644 index 000000000..135b86651 --- /dev/null +++ b/apps/hidbkbd/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "hidbkbd", + "name": "Binary Bluetooth Keyboard", + "shortName": "Binary BT Kbd", + "version": "0.02", + "description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want", + "icon": "hid-binary-keyboard.png", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"hidbkbd.app.js","url":"hid-binary-keyboard.js"}, + {"name":"hidbkbd.img","url":"hid-binary-keyboard-icon.js","evaluate":true} + ] +} diff --git a/apps/hidcam/metadata.json b/apps/hidcam/metadata.json new file mode 100644 index 000000000..b2ef33229 --- /dev/null +++ b/apps/hidcam/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "hidcam", + "name": "Camera shutter", + "shortName": "Cam shutter", + "version": "0.03", + "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", + "icon": "app.png", + "tags": "bluetooth,tool", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"hidcam.app.js","url":"app.js"}, + {"name":"hidcam.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/hidjoystick/metadata.json b/apps/hidjoystick/metadata.json new file mode 100644 index 000000000..e2b78a97b --- /dev/null +++ b/apps/hidjoystick/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "hidjoystick", + "name": "Bluetooth Joystick", + "shortName": "Joystick", + "version": "0.01", + "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.", + "icon": "app.png", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"hidjoystick.app.js","url":"app.js"}, + {"name":"hidjoystick.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/hidkbd/metadata.json b/apps/hidkbd/metadata.json new file mode 100644 index 000000000..15e5410b4 --- /dev/null +++ b/apps/hidkbd/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "hidkbd", + "name": "Bluetooth Keyboard", + "shortName": "Bluetooth Kbd", + "version": "0.02", + "description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps", + "icon": "hid-keyboard.png", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"hidkbd.app.js","url":"hid-keyboard.js"}, + {"name":"hidkbd.img","url":"hid-keyboard-icon.js","evaluate":true} + ] +} diff --git a/apps/hidmsic/metadata.json b/apps/hidmsic/metadata.json new file mode 100644 index 000000000..dc0079d74 --- /dev/null +++ b/apps/hidmsic/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "hidmsic", + "name": "Bluetooth Music Controls", + "shortName": "Music Control", + "version": "0.02", + "description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!", + "icon": "hid-music.png", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"hidmsic.app.js","url":"hid-music.js"}, + {"name":"hidmsic.img","url":"hid-music-icon.js","evaluate":true} + ] +} diff --git a/apps/hidmsicswipe/metadata.json b/apps/hidmsicswipe/metadata.json new file mode 100644 index 000000000..3f1ea5f4f --- /dev/null +++ b/apps/hidmsicswipe/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "hidmsicswipe", + "name": "Bluetooth Music Swipe Controls", + "shortName": "Swipe Control", + "version": "0.01", + "description": "Based on the original Bluetooth Music Controls. Swipe up/down for volume, left/right for previous and next, tap for play/pause and btn1 to lock and unlock the controls. Enable HID in settings, pair with your phone, then use this app to control music from your watch!", + "icon": "hidmsicswipe.png", + "tags": "bluetooth", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"hidmsicswipe.app.js","url":"hidmsicswipe.js"}, + {"name":"hidmsicswipe.img","url":"hidmsicswipe-icon.js","evaluate":true} + ] +} diff --git a/apps/horsey/metadata.json b/apps/horsey/metadata.json new file mode 100644 index 000000000..256d1f373 --- /dev/null +++ b/apps/horsey/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "horsey", + "name": "Horse Race!", + "version": "0.01", + "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", + "icon": "horse-race.png", + "tags": "game", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"horsey.app.js","url":"horse-race.js"}, + {"name":"horsey.img","url":"horse-race-icon.js","evaluate":true} + ] +} diff --git a/apps/hourstrike/ChangeLog b/apps/hourstrike/ChangeLog index 09eb45b36..aee136eef 100644 --- a/apps/hourstrike/ChangeLog +++ b/apps/hourstrike/ChangeLog @@ -6,3 +6,4 @@ 0.06: Move the next strike time to the first row of display 0.07: Change the boot function to avoid reloading the entire watch 0.08: Default to no strikes. Fix file-not-found issue during the first boot. Add data file. +0.09: Add some customisation options diff --git a/apps/hourstrike/app.js b/apps/hourstrike/app.js index 7dc62d440..9169b5def 100644 --- a/apps/hourstrike/app.js +++ b/apps/hourstrike/app.js @@ -1,5 +1,6 @@ const storage = require('Storage'); var settings = storage.readJSON('hourstrike.json', 1); +const chimes = ["Buzz", "Beep"]; function updateSettings() { storage.write('hourstrike.json', settings); @@ -26,6 +27,12 @@ function showMainMenu() { mainmenu.Strength = { value: settings.vlevel*10, min: 1, max: 10, format: v=>v/10, onchange: v=> {settings.vlevel = v/10; updateSettings();}}; + mainmenu.Strikecount = { + value: settings.scount, min: 1, max: 2, format: v=>v, + onchange: v=> {settings.scount = v; updateSettings();}}; + mainmenu.Chimetype = { + value: settings.buzzOrBeep, min: 0, max: 1, format: v => chimes[v], + onchange: v=> {settings.buzzOrBeep = v; updateSettings();}}; mainmenu['< Back'] = ()=>load(); return E.showMenu(mainmenu); } diff --git a/apps/hourstrike/boot.js b/apps/hourstrike/boot.js index 027b8bb5b..7f0cdd4e8 100644 --- a/apps/hourstrike/boot.js +++ b/apps/hourstrike/boot.js @@ -30,9 +30,23 @@ } function strike_func () { var setting = require('Storage').readJSON('hourstrike.json',1)||[]; - Bangle.buzz(200, setting.vlevel||0.5) - .then(() => new Promise(resolve => setTimeout(resolve,200))) - .then(() => Bangle.buzz(200, setting.vlevel||0.5)); + if (0 == setting.buzzOrBeep) { + if (2 == setting.scount) { + Bangle.buzz(200, setting.vlevel||0.5) + .then(() => new Promise(resolve => setTimeout(resolve,200))) + .then(() => Bangle.buzz(200, setting.vlevel||0.5)); + } else { + Bangle.buzz(200, setting.vlevel||0.5); + } + } else { + if (2 == setting.scount) { + Bangle.beep(200) + .then(() => new Promise(resolve => setTimeout(resolve,100))) + .then(() => Bangle.beep(300)); + } else { + Bangle.beep(200); + } + } setup(); } setup(); diff --git a/apps/hourstrike/hourstrike.json b/apps/hourstrike/hourstrike.json index 09b17dc8e..6e4d583de 100644 --- a/apps/hourstrike/hourstrike.json +++ b/apps/hourstrike/hourstrike.json @@ -1 +1 @@ -{"interval":-1,"start":9,"end":21,"vlevel":0.5,"next_hour":-1,"next_minute":-1} +{"interval":-1,"start":9,"end":21,"vlevel":0.5,"scount":2,"buzzOrBeep":0,"next_hour":-1,"next_minute":-1} diff --git a/apps/hourstrike/metadata.json b/apps/hourstrike/metadata.json new file mode 100644 index 000000000..614db54e4 --- /dev/null +++ b/apps/hourstrike/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "hourstrike", + "name": "Hour Strike", + "shortName": "Hour Strike", + "version": "0.09", + "description": "Strike the clock on the hour. A great tool to remind you an hour has passed!", + "icon": "app-icon.png", + "tags": "tool,alarm", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"hourstrike.app.js","url":"app.js"}, + {"name":"hourstrike.boot.js","url":"boot.js"}, + {"name":"hourstrike.img","url":"app-icon.js","evaluate":true}, + {"name":"hourstrike.json","url":"hourstrike.json"} + ] +} diff --git a/apps/hralarm/ChangeLog b/apps/hralarm/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/hralarm/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/hralarm/README.md b/apps/hralarm/README.md new file mode 100644 index 000000000..37b14ad9d --- /dev/null +++ b/apps/hralarm/README.md @@ -0,0 +1,15 @@ +# Heart rate alarm + +This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits. + +## Usage + +Configure the heart rate limits in the apps settings. This widget uses both 'HRM' and 'BTHRM' events. + +## Features + +Long vibration every 10 seconds on reaching upper limit, short vibrations between upper limit and warning threshold and an single vibration when reaching the lower limit again. + +## Requests/Creator + +https://github.com/halemmerich diff --git a/apps/hralarm/metadata.json b/apps/hralarm/metadata.json new file mode 100644 index 000000000..1fae68084 --- /dev/null +++ b/apps/hralarm/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "hralarm", + "name": "Heart rate alarm", + "shortName":"HR Alarm", + "version":"0.01", + "description": "This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"hralarm.wid.js","url":"widget.js"}, + {"name":"hralarm.settings.js","url":"settings.js"} + ] +} diff --git a/apps/hralarm/settings.js b/apps/hralarm/settings.js new file mode 100644 index 000000000..3158ab8b7 --- /dev/null +++ b/apps/hralarm/settings.js @@ -0,0 +1,57 @@ +(function(back) { + var FILE = "hralarm.json"; + + var settings = Object.assign({ + enabled: false, + upper: 180, + warning: 170, + lower: 150, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + '': { 'title': 'HR Alarm' }, + '< Back': back, + 'Enabled': { + value: !!settings.enabled, + format: v => settings.enabled ? "On" : "Off", + onchange: v => { + settings.enabled = v; + writeSettings(); + } + }, + 'Upper limit': { + value: settings.upper, + min: 0, + step:5, + max: 300, + onchange: v => { + settings.upper = v; + writeSettings(); + } + }, + 'Lower limit': { + value: settings.lower, + min: 0, + step:5, + max: 300, + onchange: v => { + settings.lower = v; + writeSettings(); + } + }, + 'Warning at': { + value: settings.warning, + min: 0, + step:5, + max: 300, + onchange: v => { + settings.warning = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/hralarm/widget.js b/apps/hralarm/widget.js new file mode 100644 index 000000000..30a94fdf2 --- /dev/null +++ b/apps/hralarm/widget.js @@ -0,0 +1,27 @@ +(() => { + var settings = require('Storage').readJSON("hralarm.json", true) || {}; + if (!settings.enabled){ Bangle.setHRMPower(0, 'hralarm'); return; } + Bangle.setHRMPower(1, 'hralarm'); + var hitLimit = 0; + var checkHr = function(hr){ + if (hr.bpm > settings.warning && hr.bpm <= settings.upper){ + Bangle.buzz(100, 1); + } + if (hitLimit < getTime() && hr.bpm > settings.upper){ + hitLimit = getTime() + 10; + Bangle.buzz(2000, 1); + } + if (hitLimit > 0 && hr.bpm < settings.lower){ + hitLimit = 0; + Bangle.buzz(500, 1); + } + }; + Bangle.on("HRM", checkHr); + Bangle.on("BTHRM", checkHr); + + WIDGETS["hralarm"]={ + area:"tl", + width: 0, + draw: function(){} + }; +})() diff --git a/apps/hralarm/widget.png b/apps/hralarm/widget.png new file mode 100644 index 000000000..726cf3f9b Binary files /dev/null and b/apps/hralarm/widget.png differ diff --git a/apps/hrings/metadata.json b/apps/hrings/metadata.json new file mode 100644 index 000000000..c47523377 --- /dev/null +++ b/apps/hrings/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "hrings", + "name": "Hypno Rings", + "version": "0.01", + "description": "Experiment with trippy rings, press buttons for change", + "icon": "hypno-rings.png", + "type": "app", + "tags": "rings,hypnosis,psychadelic", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-hypno-rings-screenshot.png"}], + "storage": [ + {"name":"hrings.app.js","url":"hypno-rings.js"}, + {"name":"hrings.img","url":"hypno-rings-icon.js","evaluate":true} + ] +} diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json new file mode 100644 index 000000000..1504253bd --- /dev/null +++ b/apps/hrm/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "hrm", + "name": "Heart Rate Monitor", + "version": "0.06", + "description": "Measure your heart rate and see live sensor data", + "icon": "heartrate.png", + "tags": "health", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"hrm.app.js","url":"heartrate.js"}, + {"name":"hrm.img","url":"heartrate-icon.js","evaluate":true} + ] +} diff --git a/apps/hrrawexp/metadata.json b/apps/hrrawexp/metadata.json new file mode 100644 index 000000000..3920731aa --- /dev/null +++ b/apps/hrrawexp/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "hrrawexp", + "name": "HRM Data Exporter", + "shortName": "HRM Data Exporter", + "version": "0.01", + "description": "export raw hrm signal data to a csv file", + "icon": "app-icon.png", + "tags": "", + "supports": ["BANGLEJS"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"hrrawexp.app.js","url":"app.js"}, + {"name":"hrrawexp.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/imgclock/metadata.json b/apps/imgclock/metadata.json new file mode 100644 index 000000000..799d11acc --- /dev/null +++ b/apps/imgclock/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "imgclock", + "name": "Image background clock", + "shortName": "Image Clock", + "version": "0.08", + "description": "A clock with an image as a background", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "custom": "custom.html", + "storage": [ + {"name":"imgclock.app.js","url":"app.js"}, + {"name":"imgclock.img","url":"app-icon.js","evaluate":true}, + {"name":"imgclock.face.img"}, + {"name":"imgclock.face.json"}, + {"name":"imgclock.face.bg","content":""} + ] +} 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/impwclock/metadata.json b/apps/impwclock/metadata.json new file mode 100644 index 000000000..6bf5183f4 --- /dev/null +++ b/apps/impwclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "impwclock", + "name": "Imprecise Word Clock", + "version": "0.04", + "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", + "icon": "clock-impword.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"bangle1-impercise-word-clock-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"impwclock.app.js","url":"clock-impword.js"}, + {"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true} + ] +} diff --git a/apps/intervalTimer/metadata.json b/apps/intervalTimer/metadata.json new file mode 100644 index 000000000..2722473c1 --- /dev/null +++ b/apps/intervalTimer/metadata.json @@ -0,0 +1,15 @@ +{ + "id":"intervalTimer", + "name":"Interval Timer", + "shortName":"Interval Timer", + "icon": "app.png", + "version":"0.01", + "description": "Interval Timer for workouts, HIIT, or whatever else.", + "tags": "timer, interval, hiit, workout", + "readme":"README.md", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"intervalTimer.app.js","url":"app.js"}, + {"name":"intervalTimer.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/intervals/metadata.json b/apps/intervals/metadata.json new file mode 100644 index 000000000..bc054a539 --- /dev/null +++ b/apps/intervals/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "intervals", + "name": "Intervals App", + "shortName": "Intervals", + "version": "0.01", + "description": "Intervals for training. It is possible to configure work time and rest time and number of sets.", + "icon": "intervals.png", + "tags": "", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"intervals.app.js","url":"intervals.app.js"}, + {"name":"intervals.img","url":"intervals-icon.js","evaluate":true} + ] +} diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index 895f50e04..1873649f9 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -1,3 +1,8 @@ 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 +0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements 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..d317c23b0 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,30 +56,74 @@ 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.mobileslideshow": "Pictures", + "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.marktplaats.iphone": "Marktplaats", + "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.storytel.iphone": "Storytel", + "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", + "io.robbie.HomeAssistant": "Home Assistant", + "net.weks.prowl": "Prowl", + "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) + body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) || "Cannot display" }); // TODO: posaction/negaction? }); @@ -82,9 +133,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/ios/metadata.json b/apps/ios/metadata.json new file mode 100644 index 000000000..26e474f89 --- /dev/null +++ b/apps/ios/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "ios", + "name": "iOS Integration", + "version": "0.08", + "description": "Display notifications/music/etc from iOS devices", + "icon": "app.png", + "tags": "tool,system,ios,apple,messages,notifications", + "dependencies": {"messages":"app"}, + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"ios.app.js","url":"app.js"}, + {"name":"ios.img","url":"app-icon.js","evaluate":true}, + {"name":"ios.boot.js","url":"boot.js"} + ], + "sortorder": -8 +} diff --git a/apps/isoclock/metadata.json b/apps/isoclock/metadata.json new file mode 100644 index 000000000..313153dde --- /dev/null +++ b/apps/isoclock/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "isoclock", + "name": "ISO Compliant Clock Face", + "shortName": "ISO Clock", + "version": "0.02", + "description": "Tweaked fork of digiclock for ISO date and time", + "icon": "isoclock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"isoclock.app.js","url":"isoclock.js"}, + {"name":"isoclock.img","url":"isoclock-icon.js","evaluate":true} + ] +} diff --git a/apps/jbells/metadata.json b/apps/jbells/metadata.json new file mode 100644 index 000000000..397638669 --- /dev/null +++ b/apps/jbells/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "jbells", + "name": "Jingle Bells", + "version": "0.01", + "description": "Play Jingle Bells", + "icon": "jbells.png", + "type": "app", + "tags": "sound", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"jbells.app.js","url":"jbells.js"}, + {"name":"jbells.img","url":"jbells-icon.js","evaluate":true} + ] +} diff --git a/apps/jbm8b/metadata.json b/apps/jbm8b/metadata.json new file mode 100644 index 000000000..4bae23cdc --- /dev/null +++ b/apps/jbm8b/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "jbm8b", + "name": "Magic 8 Ball", + "shortName": "Magic 8 Ball", + "version": "0.03", + "description": "A simple fortune telling app", + "icon": "app.png", + "tags": "game", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"jbm8b.app.js","url":"app.js"}, + {"name":"jbm8b.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/jbm8b_IT/metadata.json b/apps/jbm8b_IT/metadata.json new file mode 100644 index 000000000..dcb2aaffc --- /dev/null +++ b/apps/jbm8b_IT/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "jbm8b_IT", + "name": "Magic 8 Ball Italiano", + "shortName": "Magic 8 Ball IT", + "version": "0.01", + "description": "La palla predice il futuro", + "icon": "app.png", + "screenshots": [{"url":"bangle1-magic-8-ball-italiano-screenshot.png"}], + "tags": "game", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "storage": [ + {"name":"jbm8b_IT.app.js","url":"app.js"}, + {"name":"jbm8b_IT.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/kitchen/metadata.json b/apps/kitchen/metadata.json new file mode 100644 index 000000000..ab2e7183c --- /dev/null +++ b/apps/kitchen/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "kitchen", + "name": "Kitchen Combo", + "version": "0.13", + "description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'", + "icon": "kitchen.png", + "type": "clock", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS"], + "readme": "README.md", + "interface": "waypoints.html", + "storage": [ + {"name":"kitchen.app.js","url":"kitchen.app.js"}, + {"name":"stepo2.kit.js","url":"stepo2.kit.js"}, + {"name":"swatch.kit.js","url":"swatch.kit.js"}, + {"name":"gps.kit.js","url":"gps.kit.js"}, + {"name":"compass.kit.js","url":"compass.kit.js"}, + {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} + ], + "data": [{"name":"waypoints.json","url":"waypoints.json"}] +} diff --git a/apps/lapcounter/ChangeLog b/apps/lapcounter/ChangeLog new file mode 100644 index 000000000..146ff1b05 --- /dev/null +++ b/apps/lapcounter/ChangeLog @@ -0,0 +1,2 @@ +0.01: first release +0.02: Themeable app icon 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..354c07124 --- /dev/null +++ b/apps/lapcounter/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwI0xg+evPsAon+ApX8Aon4AonwAod78AFDv4FWvoFE/IFDz4FXvIFD3wFE/wFW7wFDh5xBAoUfAok/Aol/BZUXAogA6A=")) 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/metadata.json b/apps/lapcounter/metadata.json new file mode 100644 index 000000000..dcc3440ec --- /dev/null +++ b/apps/lapcounter/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "lapcounter", + "name": "Lap Counter", + "version": "0.02", + "description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "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/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/largeclock/metadata.json b/apps/largeclock/metadata.json new file mode 100644 index 000000000..dde790786 --- /dev/null +++ b/apps/largeclock/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "largeclock", + "name": "Large Clock", + "version": "0.10", + "description": "A readable and informational digital watch, with date, seconds and moon phase", + "icon": "largeclock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-large-clock-screenshot.png"}], + "storage": [ + {"name":"largeclock.app.js","url":"largeclock.js"}, + {"name":"largeclock.img","url":"largeclock-icon.js","evaluate":true}, + {"name":"largeclock.settings.js","url":"settings.js"} + ], + "data": [{"name":"largeclock.json"}] +} diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index bd8a9bd03..ceb0177da 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -6,3 +6,7 @@ 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 +0.11: Merge Bangle.js 1 and 2 launchers, again diff --git a/apps/launch/app-bangle1.js b/apps/launch/app-bangle1.js deleted file mode 100644 index 3d4682e55..000000000 --- a/apps/launch/app-bangle1.js +++ /dev/null @@ -1,66 +0,0 @@ -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)); -apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; -}); -var selected = 0; -var menuScroll = 0; -var menuShowing = false; - -function drawMenu() { - g.reset().setFont("6x8",2).setFontAlign(-1,0); - var w = g.getWidth(); - var h = g.getHeight(); - var m = w/2; - var n = Math.floor((h-48)/64); - if (selected>=n+menuScroll) menuScroll = 1+selected-n; - if (selectedn+menuScroll) ? g.theme.fg : g.theme.bg); - g.fillPoly([m,h-7,m-14,h-21,m+14,h-21]); - // draw - g.setColor(g.theme.fg); - for (var i=0;i{ - if (dir) { - selected += dir; - if (selected<0) selected = apps.length-1; - if (selected>=apps.length) selected = 0; - drawMenu(); - } else { - if (!apps[selected].src) return; - if (require("Storage").read(apps[selected].src)===undefined) { - E.showMessage("App Source\nNot found"); - setTimeout(drawMenu, 2000); - } else { - E.showMessage("Loading..."); - load(apps[selected].src); - } - } -}); -Bangle.loadWidgets(); -Bangle.drawWidgets(); diff --git a/apps/launch/app-bangle2.js b/apps/launch/app-bangle2.js deleted file mode 100644 index 8b66247c5..000000000 --- a/apps/launch/app-bangle2.js +++ /dev/null @@ -1,48 +0,0 @@ -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)); -apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; -}); -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); - apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n")); -} - -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.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -E.showScroller({ - h : 64, c : apps.length, - draw : drawApp, - select : i => { - var app = apps[i]; - if (!app) return; - if (!app.src || require("Storage").read(app.src)===undefined) { - E.showMessage("App Source\nNot found"); - setTimeout(drawMenu, 2000); - } else { - E.showMessage("Loading..."); - load(app.src); - } - } -}); diff --git a/apps/launch/app.js b/apps/launch/app.js new file mode 100644 index 000000000..42aba1bb9 --- /dev/null +++ b/apps/launch/app.js @@ -0,0 +1,80 @@ +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); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); +apps.forEach(app=>{ + if (app.icon) + app.icon = s.read(app.icon); // should just be a link to a memory area +}); +// FIXME: check not needed after 2v11 +if (g.wrapString) { + g.setFont(font); + apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n")); +} + +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*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(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +E.showScroller({ + h : 64*scaleval, c : apps.length, + draw : drawApp, + select : i => { + var app = apps[i]; + if (!app) return; + if (!app.src || require("Storage").read(app.src)===undefined) { + E.showMessage("App Source\nNot found"); + setTimeout(drawMenu, 2000); + } else { + E.showMessage("Loading..."); + load(app.src); + } + } +}); + +// on bangle.js 2, the screen is used for navigating, so the single button goes back +// on bangle.js 1, the buttons are used for navigating +if (process.env.HWVERSION==2) { + 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/metadata.json b/apps/launch/metadata.json new file mode 100644 index 000000000..1701d1f87 --- /dev/null +++ b/apps/launch/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "launch", + "name": "Launcher", + "shortName": "Launcher", + "version": "0.11", + "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", + "tags": "tool,system,launcher", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"launch.app.js","url":"app.js"}, + {"name":"launch.settings.js","url":"settings.js"} + ], + "data": [{"name":"launch.json"}], + "sortorder": -10 +} 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/lazyclock/metadata.json b/apps/lazyclock/metadata.json new file mode 100644 index 000000000..c08485fc7 --- /dev/null +++ b/apps/lazyclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "lazyclock", + "name": "Lazy Clock", + "version": "0.03", + "description": "Tells the time, roughly", + "icon": "lazyclock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "screenshots": [{"url":"bangle1-lazy-clock-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"lazyclock.app.js","url":"lazyclock-app.js"}, + {"name":"lazyclock.img","url":"lazyclock-icon.js","evaluate":true} + ] +} diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index 85bcbad36..1abd519ab 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -3,4 +3,12 @@ 0.03: New design with different icons if gps, hrm or compass is on. 0.04: Inluded LCARS Logo. 0.05: Additional icons for (1) charging and (2) bat < 30%. -0.06: Fix - Alarm disabled, if clock was closed \ No newline at end of file +0.06: Fix - Alarm disabled, if clock was closed. +0.07: Added settings to adjust data that is shown for each row. +0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. +0.09: Tab anywhere to open the launcher. +0.10: Removed swipes to be compatible with the Pattern Launcher. Stability improvements. +0.11: Show the gadgetbridge weather temperature (settings). +0.12: Added humidity as an option to display. +0.13: Improved battery visualization. +0.14: Added altitude as an option to display. \ No newline at end of file diff --git a/apps/lcars/README.md b/apps/lcars/README.md index 3acaacb4d..017be246c 100644 --- a/apps/lcars/README.md +++ b/apps/lcars/README.md @@ -1,19 +1,44 @@ # LCARS clock A simple LCARS inspired clock. -Note: To display the steps, its necessary to install -the [Pedometer widget](https://banglejs.com/apps/#pedometer%20widget). +Note: To display the steps, the health app is required. If this app is not installed, the data will not be shown. +To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerdavid/BangleApps) + +## Control + * Tap left / right to change between screens. + * Tap top / bottom to control the current screen. ## Features - * Shows the time - * Shows the date - * Shows the current battery level in % - * Shows the number of daily steps - * Swipe left/right to activate an alarm + * LCARS Style watch face. + * Full screen mode - widgets are still loaded but not shown. + * Tab on left/right to switch between different screens. + * Cusomizable data that is shown on screen 1 (steps, weather etc.) + * Shows random and real images of planets. + * Tap on top/bottom of screen 1 to activate an alarm. + * The lower orange line indicates the battery level. + * Display graphs (day or month) for steps + hrm on the second screen. -## Icons -
Icons made by Smashicons, Freepik from www.flaticon.com
+## Data that can be configured + * Steps - Steps loaded via the health module + * Battery - Current battery level in % + * VREF - Voltage of battery + * HRM - Last measured HRM + * Temp - Weather temperature loaded via the weather module + gadgetbridge + * Humidity - Humidity loaded via the weather module + gadgetbridge + * Altitude - Shows the altitude in m. + * CoreT - Temperature of device + +## Multiple screens support +Access different screens via tap on the left/ right side of the screen + +![](screenshot.png) +![](screenshot_2.png) -## Creator -Made by [David Peer](https://github.com/peerdavid) \ No newline at end of file +# Ideas +- Tap top / bottom to disable steps (also icon) and start a timer + +## Contributors +- [David Peer](https://github.com/peerdavid). +- [Adam Schmalhofer](https://github.com/adamschmalhofer). +- [Jon Warrington](https://github.com/BartokW). diff --git a/apps/lcars/bg_large.png b/apps/lcars/bg_large.png deleted file mode 100644 index dd5bda4f3..000000000 Binary files a/apps/lcars/bg_large.png and /dev/null differ diff --git a/apps/lcars/bg_left.png b/apps/lcars/bg_left.png new file mode 100644 index 000000000..91c2bb6f7 Binary files /dev/null and b/apps/lcars/bg_left.png differ diff --git a/apps/lcars/bg_right.png b/apps/lcars/bg_right.png new file mode 100644 index 000000000..6e23a5d6e Binary files /dev/null and b/apps/lcars/bg_right.png differ diff --git a/apps/lcars/bg_small.png b/apps/lcars/bg_small.png deleted file mode 100644 index 8030c0ddb..000000000 Binary files a/apps/lcars/bg_small.png and /dev/null differ diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index 9b7244ece..81a501481 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -1,86 +1,105 @@ -const filename = "lcars.setting.json"; -const Storage = require("Storage"); -let settings = Storage.readJSON(filename,1) || { +const SETTINGS_FILE = "lcars.setting.json"; +const locale = require('locale'); +const storage = require('Storage') +let settings = { alarm: -1, + dataRow1: "Steps", + dataRow2: "Temp", + dataRow3: "Battery" }; +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key] +} + +/* + * Colors to use + */ +let cBlue = "#0094FF"; +let cOrange = "#FF9900"; +let cPurple = "#FF00DC"; +let cWhite = "#FFFFFF"; +let cBlack = "#000000"; +let cGrey = "#424242"; + +/* + * Global lcars variables + */ +let lcarsViewPos = 0; +// let hrmValue = 0; +var plotMonth = false; /* * Requirements and globals */ -const locale = require('locale'); -var backgroundImage = { - width : 176, height : 151, bpp : 3, - transparent : 2, - buffer : require("heatshrink").decompress(atob("AAUEufPnnzATkAg4daIIXnz15ATvkwEDDrUAgPHQDyDghyAeQcNzJQ0cuPHATCDBDrUDJQ1AgAA3jjOF+BA4T4KDFyBB5Qf4ABQAaD9QAaD/QesH8CD/n/8Qf8//+AQfsB///GQ6D2h5BJQf6D7/yD8jl/IIIABjiD5n4/DAAWAQe8B//8QYfH//x4CD2HwMDQIf4AoP4Qesf/56BQYYFBuP/Qev//0AQYoKBn/gQecH/lwQwQADBYaDzGoZBHR4OAQehBKj5BBsuWrICDBAIAofYZBFBAZ6qIJJ6DQZBB3IAiDDgZBygJ6EIIn8IOqDKIIscuPHAQdwINkHIJEfIIPnz15AQeAINT+CHwcPAYI1BIIU8+fPAQbOqg56BQYcAgKD4IIv4RgSDCAQSD34AIC//wBYSDyO4P+IIoIB+E/8AFBQeL7B//HHYJKE+P/AoSDygF/QQJBF//4AoSDygEBQYgFBj/xZYaDzgE/PoIAE/wMDQeZBB/jICAAMcuAMDQevgQwR0CvyD3gP/BAxBEQek4A40OQe4ANQegAMQf6D/AAccQf8Ak6DFyCD/QfcDQYueIPMAuaDE+fBIPMOQYoCb8glB7dt2wCW2EAgKDFATkAg2atOmAS5eBhKDigyDZ2zHCjiD/AAMChEgwQCcQb4AiQb5BiQbscuPHATyDfyfPnnzATnwQbsBQD6DghKAeQcJoHiFBggCYQYVhdwQATgOmgVPNAnOECwAGQYIZXgM2dI1wIL2aoCDYibsF4CD/QcGYILGmyaDFwCD/QfaADQf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D4jCD/ADKDnILSD/Qf6DEHO6DJIP6D/Qf6D/QY8cuPHAQdAQfPz588AQeAQf8cuCD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6DqoCD5HO6DJIP6D/Qf6D/QY8cuPHAQdwE7sGzCDZ+fPngCDwBBe7aD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/QfcTQYvAQf6DgzVAQbECp6DE5yD5gCDFATqDCsOAIKtB00AhKDEATnwQYVt2wCXQwKDltOmAS6IC2aD82BBCQccaQbGAA==")) -} + +var bgLeft = { + width : 27, height : 176, bpp : 3, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAFCh/eX5Q/KAwdCAGVbtu27YCCoAJBkuWrNlAQRGCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A==")) +}; + +var bgRight = { + width : 27, height : 176, bpp : 3, + transparent : 0, + buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAnUP7y/KH4yGeVYAJrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgAA=")) +}; var iconEarth = { - text: "EARTH", width : 50, height : 50, bpp : 3, buffer : require("heatshrink").decompress(atob("AFtx48ECBsDwU5k/yhARLjgjBjlzAQMQEZcIkOP/fn31IEZgCBnlz58cEpM4geugEgwU/8+WNZJHDuHHvgmBCQ8goEOnVgJoMnyV58mACItHI4X8uAFBuVHnnz4BuGxk4////Egz3IkmWvPgNw8f/prB//BghTC+AjE7848eMjNnzySBwUJkmf/BuGuPDAQIjBiPHhhTCSQnjMo0ITANJn44Dg8MuFBggCCiFBcAJ0Bv5xEh+ITo2OhHkyf/OIQdBWwVHhgjBNwUE+fP/5EEgePMoYLBhMgyVJk/+BQQdC688I4XxOIc8v//NAvr+QEBj/5NwKVBy1/QYUciPBhk1EAJrC+KeC489QYaMBgU/8BNB9+ChEjz1Jkn/QYMBDQIgCcYTCCiP/nlzJQmenMAgV4//uy/9wRaB/1J8iVCcAfHjt9TYYICnhKCgRKBw159/v//r927OIeeoASBDQccvv3791KYVDBYPLJQeCnPnz//AAP6ocEjEkXgMgJQtz79fLAP8KYkccAcJ8Gf/f/xu/cAMQ4eP5MlyQRCMolx40YsOGBAPfnnzU4KVDpKMBvz8Dh0/8me7IICgkxJQXPIgZTD58sEgcJk+eNoONnFBhk4/5uB/pcDg5KD+4mEv4CBXISVDhEn31/8/+mH7x//JQK5CAAMB4JBCnnxJQf/+fJEgkAa4L+CAQOOjMn/1bXIRxDJQXx58f//Hhlz/88EgsChMgz/Zs/+nfkyV/8huDOI6SD498NwoACi1Z8+S/Plz17/+QCI7jC+ZxBmfPnojIAAMDcYWSp//2wRJEwq2GABECjMgNYwAmA=")) -} +}; var iconSaturn = { - text: "SATURN", width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("AH4A/AEkQuPHCJ0ChEAwARNjAjBjgjOhs06Q2OEYVx4ARMhEggUMkANIDoIgBoEEgEBNxJEC6ZrBAAMwNxAjDNYcHNxIjB7dtEwIHBwRoKj158+cuPEjlwCRAjC23bpu0wRNDAAsHEYWeEwaSJ6YjCAQUNSRQjEzxQBWZMNEYlsmg2JWAIjCz95SoJuJggjDtuw6dMG5JKCz998wFBJRVNEYW0yaVBJRNhJQN9+4pCzhKJmBKC4YpB/fINxIgCzFxSoQ3J4ENm3CAQPb98wbpEcAQMYWwKYBNxMDXgc2/fv3g2IEAOAgAjBjy5CEhEMfYICBgfPnjdLjj+CgMHiC3JknDhhoINw4jCAB0IJQIANR4QjPAH4A/AFA")) -} +}; var iconMoon = { - text: "MOON", width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("AH4AQjlx44CCCZsg8eOkHDwAQKEYgmPhEgEQM48AOIgMHEYoCB4ATI8UAmH/x04JoRuJsImHuBKLn37EwZuIgEQOI8cEpXj/yYBhE8+YNGgkYoJxITBUPnAaC///nC+FjBuIOJZEB8YeCh/8AoYACoMEEAnEjhQDPQJKJ/DCDAoi5DoLdHAoMQgLjFWYPOnngh02IwXzwDjEgPGEYS8BI4MBYoSVG4fP/nghkAgZrDkngJQqSG4gvBg4sBQgkImHihEAWwP8ZBMBEYl5/+cSoVAGQIUFh04weJn///0gj/OEw5KEz45BzhuCTYQAEgePB4IACAoJuBnAQEa4XHjxKB//xFgWHJQsCRgMDEonipwjENwUBDQNx8+evvn/hTDLw3igE+EgZxB8UOXIvEJQUfEYOfv53DEQkgga5BJQvzx84cAj+CDoNh8/eEYJKDuCSEcocnEon+/7xEgFBIIcfB4Mf/IICXI2DgDdBAAn758gCIq5Dv4zBvJuIOIfjEgvP/ARHgwdCB4P3AoTdFAAk4EYk8SQgAFTALaDSQwAGh08//vnDmBABYmEEZYAzA==")) -} +}; var iconMars = { - text: "MARS", width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("AH4ATjlwCJ+Dh0wwAQMg0cuPHjFhCZkDps0yVJkmQCBMEjFx42atOmzQmLhMkEYQCCCREQoOGEYmmzB0IEY4CBkARGoJKBEYQCEzgSGkGSpAjDyYCCphuGiFhJQgCD8ASFgRHGAQKbB6BuHJRGeOIsINxEk6dNmARDgMEjQjHAQPnVQojIyZKB6YSDNwK5FAQt54BuDXJIjBEwK5EgxKKXgq5BJRdgXIojJAQJKMcAM0EwM2JUApDoCVFExa7FkGCgAmIkAREEwUEjAmHCIgABhEggQmFpACBCIojBEwRQCzVhwkQU4YADgQmBwQCCI4IFBCAojFAQojGJQQjDAQgRGEZICBEo4gFyUIkilFJQUYEAZrBAQMYNw5KDSQSbCNwwABgOGEwgCBsPACQ5xGwdNnARJcAVh48evvnCJK8Chs+/fv33gCRcB48cuPHCBYA/ADAA==")) -} +}; var iconSatellite = { - text: "GPS ON", width : 50, height : 50, bpp : 3, transparent : 2, buffer : require("heatshrink").decompress(atob("pMkyQC/ATGXhIRPyNl0gmPjlwCJ9ly1aCJ1c+fHJR1Hy1ZJR1I+fPnlx6QRLpe+/JKBr5KMuYjBJQMdCJce/fvJQW0CJUlEYQCBSpvvJQbXJjl0NwnzNxGQwEOnHhgF78+WqQyIrFx48cAQXz4ShJgAABh0+8cP//9LJEhg4jDuP3//0LhGQgYlBgeAn///5cIy8MuAmDCIP/9I4HkmCEYMOgHfCQWkCI0cuBuDgF/CIP+CI1Ny1IkeAgHANwIAB/QRFrj7BhkxEwQRC/4RFpbXDgSVBg4RCSorXDI4MJAQMfCIP8cwImDn37fwN58+kwHgLgSVFub7CI4NyBAJKDLgkuEYX78+evKtCLg0jEYRKC58JMoRcFkwjDJQTFDl65EkojEAQMdcwn/+gFC3YjEJQLXEpYRDWwQmEdI6SHAQO0CJUkx4jDF4gCIJQgRMXIjCEARIjCCJ2XEYPKCJqJBJQIROcAUpCJ0kybaDARtdCKAC2kAA=")) -} - -var iconAlarm = { - text: "TIMER", - width : 50, height : 50, bpp : 3, - transparent : 1, - buffer : require("heatshrink").decompress(atob("kmSpICEp//BAwCJn/+CJ8k//5CKAABCJs8uPH//x48EI5YjCAARNKEYUcv//jgFBExEnEYoAC+QmHIgIgC/gpCuPBCI2fIgU4AQXjA4P8CIuTEYZKBAolwHApXBEAWP//jxwpBAALaFDoYCIiQmDDIP4EAT+CEwnJEwYjLAQLaFEYomDKALmDNwoCIOIZuD8AkFgCYDHAQjMAQTdDNwOAEg0Dx0/cYeREZtxQYOTHgJuHOIvkXJy8DNwIACJQ8Ah4NDAAfxEZARHOIIkHg4jQAQb1CQ4KVJgEOnDIBSoIjNAQPBcAaVJcAKVBcDGOcD7OBMQM48BuH8f//JKCnhKNggRBkmfTQJxBEwhuD/gRCyVHJRlyCIVJXgYmB8ZQBAoIKBXIQmCOIt/NxAUCOIImCIgIpCBAJuDAQZEE/huIAQWTDgImBTYQGC8gRFcYpKFCI8kDwQAFCJBfBEAX/+IjBiQRIEw4jJAQc8v//NYwCIOgJrIJpA1OcwbaFAQWQA=")) -} +}; var iconCharging = { - text: "CHARGE", width : 50, height : 50, bpp : 3, transparent : 5, buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A")) -} +}; var iconNoBattery = { text: "NO BAT", width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA")) -} +}; // Font to use: // -Graphics.prototype.setFontAntonioSmall = function(scale) { - // Actual height 18 (17 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAf4Mf/sYAMAAAAAAfgAfAAAAAfgAeAAAAAAiAAj8H/4fyEAv8f/gfiAAgAAAAD54H98eOPHn8Hz8AhwAAAP8Af+AYGAYCAf+AP8MAB8AHwA+AD4AfAAcf4A/8AwMAwMA/8Af4AAAAAwGD8f/8f8MY/cfz4PD8AHMAAAfAAeAAAAAAAAP/+f//YADAAAQABYADf//P/+AAAAAANAAPAAfwAfgAPAANAAAAAAEAAEAA/AA/AAEAAEAAAAAAZAAfAAYAAAAIAAIAAIAAIAAAAAAAAAMAAMAAAAAAAAEAB8Af4H+AfwAcAAAAAP/4f/8YAMf/8f/8H/wAAAAAAEAAMAAf/8f/8f/8AAAAAAAAAHgcfh8cH8YPMf8MPwEAAAAAAOB4eB8YYMY4Mf/8Pn4AAAAAgAHwA/wPwwf/8f/8AAwAAgAAAf54f58ZwMZwMY/8Qf4AAAAAAP/4f/8YYMYYMff8HP4AAAQAAYAAYD8Y/8f/AfgAcAAAAAAAAPv4f/8YYMY8Mf/8Pn4AAAAAAP94f98YGMcMMf/8H/wAAAAAABgwBgwAAAAAABgABg/Bg8AAAAEAAOAAbAA7gAxgBwwASAAbAAbAAbAAbAASAAAAAxwA5gAbAAPAAOAAAAPAAfHcYPcf8Af4AHgAAAAAAAB/gH/wOA4Y/MZ/sbAsbBkb/MZ/sOBsH/AAAAAAMAP8f/4fwwf4wH/8AH8AAMAAAf/8f/8YYMYYMf/8P/4ADgAAAP/4f/8YAMYAMfj8Pj4AAAAAAf/8f/8YAMYAMf/8P/4B/AAAAf/8f/8YMMYMMYIMAAAAAAf/8f/8YYAYYAYYAAAAAAAP/4f/8YAMYIMfP8Pv8AAAAAAf/8f/8AMAAMAf/8f/8f/8AAAAAAf/8f/8AAAAAAAD4AB8AAMf/8f/4f/gAAAAAAf/8f/8A+AD/gfj4eA8QAEAAAf/8f/8AAMAAMAAMAAAf/8f/8f8AB/wAB8AP8P/Af/8f/8AAAAAAf/8f/8HwAA+AAPwf/8f/8AAAAAAP/4f/8YAMYAMf/8P/4AAAAAAf/8f/8YGAYGAf8AP8ABAAAAAf/w//4wAYwAc//+f/yAAAAAAf/8f/8YMAYMAf/8f/8DA8CAAPj4fz8Y4MeeMfP8HD4YAAYAAf/8f/8YAAQAAAAAf/4f/8AAMAAMf/8f/4AAAYAAf4AP/4AP8AP8f/4fwAQAAYAAf8AP/8AD8D/8f8Af8AD/8AD8f/8f8AAAAQAEeB8P/4B/AP/4fA8QAEYAAfAAP4AB/8H/8fwAcAAAAMYD8Y/8f/MfwMcAMAAAf/+f//YADYADAAAAAAfAAf8AB/wAH8AAMQACYADf//f//AAAAA"), 32, atob("BAUHCAcTCAQFBQgGBAYFBggICAgICAgICAgEBQYGBggNCAgICAcHCAkECAgGCwkICAgIBwYICAwHBwYGBgY="), 18+(scale<<8)+(1<<16)); -} +Graphics.prototype.setFontAntonioMedium = function(scale) { + // Actual height 20 (19 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAA//mP/5gAAAAAAAAAAAAA/gAMAAAAAA/gAPAAAEIIBP+H/8D+IYBP+H/8D+IABCAAwIAfnwP8+PHh448eP3+B4fAAAAAAAH/AD/4AwGAMBgD/4Af8GAAPgAPgAfgAfAAfAA+AAOP/AH/4BgGAYBgH/4A/8AAAAAAAAAQAA/B+f4/+GMPhjv/4/h8Dg/gAcYwAAPwADgAAAAAAAAB//8///sAAaAACAAAMAAb//+f//AAAAAAAbAAGwAA4AA/wADgABsAAbAAAAAAAgAAMAAPwAD8AAMAADAAAAAAAAAAHAAB/AAOAAAAAAAAMAADAAAwAAMAACAAAAAAAAAABgAAYAAAAAAAAA4AD+AP+A/4A/gAOAAAAAAAAAH//j//8wADMAAz//8f/+AAAAAAAMAADAABgAA//+P//gAAAAAAAAAAAAAfgfP4fzAfswfDP/gx/gMAAAHgPj4D8wMDMHAz//8f3+AAEAAAAADwAH8APzA/AwP//j//4AAwAAAD/Hw/x+MwBjOAYz/+Mf/AAAAAAAH//j//8wYDMGAz9/8fP+AAcDAAAwAAMAfjB/4z/wP+AD4AAwAAAAOB/f4///MHAzBwM///H9/gAAAAAAH/Pj/78wGDMBgz//8f/+AAAAAAADhwA4cAAAAAAAAAAAAAADh/A4fgAAAAOAAHwABsAA7gAccAGDAAAAANgADYAA2AANgADYAA2AAAAAAAABgwAccADuAAbAAHwAA4AAAAHwAD8c4/POMHAD/wAfwAAAAAAAAD/wD//B4B4Y/HMf8zMBMyATMwczP+M4BzHwcgf+AA+AAAAAAD4A/+P/8D+DA/4wH/+AB/4AAeAAAAAAA//+P//jBgYwYGP//j//4PH4AAAAAAAf/+P//zgAcwADP4fz+P4Ph8AAAAAAA//+P//jAAYwAGPADj//4P/4AAAAAAA//+P//jBgYwYGMGBgAAAAAAP//j//4wYAMGADBgAAAAAAAA//w///PAHzAQM4MHP7/x+/8AAAAAAD//4//+AGAABgAAYAP//j//4AAAAAAAAAA//+P//gAAAAAAAAAAAHwAB+AABgAAY//+P//AAAAAAAAAAD//4//+APgAf+Afj8PgPjAAYAAAAAAD//4//+AABgAAYAAGAAAAAAA//+P//j/gAD/wAB/gAP4B/4P/AD//4//+AAAAAAAAAAP//j//4P4AAfwAA/g//+P//gAAAAAAAAAA//g//+PAHjAAY4AOP//h//wAAAAAAD//4//+MDADAwA4cAP/AB/gAAAAAAAA//g//+PAHjAAc4APv//5//yAAAAAAD//4//+MGADBgA48AP//h+f4AAAAAAB+Pw/z+MOBjBwY/P+Hx/AAHgwAAMAAD//4//+MAADAAAAAAP//D//4AAOAABgAA4//+P//AAAAwAAP8AD//AA/+AAfgP/4//gPwAAAAA+AAP/4Af/4AD+A//j/wA/wAD/+AA/4B/+P/+D+AAAAAMADj8P4P/4A/4B//w+A+MABgAAA4AAPwAB/gAB/+A//j/gA+AAMAAAAAYwB+MH/jf+Y/8GPwBjAAAAAAP//7//+wABsAAYAAAAAAPAAD/gAH/gAD/gAD4AACAAADAAGwABv//7//+AAAA=="), 32, atob("BQUHCAgVCQQFBQkHBQcFBwgICAgICAgICAgFBQcHBwgPCQkJCQcHCQoFCQkHDQoJCQkJCAYJCQ0ICAcGBwY="), 20+(scale<<8)+(1<<16)); +}; Graphics.prototype.setFontAntonioLarge = function(scale) { - // Actual height 34 (34 - 1) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAADwAAAAAeAAAAADwAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAD+AAAAH/wAAAP/+AAAf/+AAA//8AAB//4AAD//wAAD//gAAAf/AAAAD+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAB////gA/////AP////8D/////wfAAAA+DwAAADweAAAAeDwAAADwf////+D/////wP////8Af///+AAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAOAAAAADwAAAAAeAAAAAHgAAAAB/////wf////+D/////wf////+D/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAPwH/4AP+B//AH/wf/4D/+D4AB/9weAAf4ODwAP8BweAP/AOD///gBwP//wAOA//4ABwB/4AAOAAAAAAAAAAAAAAAAAAAAB8AA/gA/gAH/AP8AA/8D/gAH/wfAHAA+DwA4ADweAHgAeDwB8ADwf7/+H+D/////gP/9//8A//H/+AA/AH/AAAAAAAAAAAAAAAAAABwAAAAD+AAAAD/wAAAH/+AAAH/5wAAH/wOAAP/gBwAP/gAOAD/////wf////+D/////wf////+AAAABwAAAAAOAAAAABwAAAAAAAAAAAAAAAAAAeAD//4D/Af//Af8D//4D/wf//Af+DwPAADweB4AAeDwPAADweB///+DwP///weA///8DwD//+AAAA/8AAAAAAAAAAAAAAAAAAAAAA////AA/////AP////8D/////wfgPAB+DwB4ADweAOAAeDwBwADwf+PAA+D/x///wP+H//8A/wf//AAAA//gAAAAAAAAAAAAADgAAAAAeAAAAADwAAAAAeAAAD+DwAAP/weAA//+DwA///weB///8Dx//8AAf//wAAD//gAAAf/AAAAD/AAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAD/wf/wB//v//AP////8D/////weAPwAeDwA8ADwcAHAAeDwB8ADwf////+D/////wP/9//8A//H//AA/AD/AAAAAAAAAAAAAAAAAAAAAD//gfAA///D/AP//8f8D///j/weAA8A+DwADgDweAAcAeDwAHgDwf////+B/////gP////8Af///+AAP//4AAAAAAAAAAAAAAAAAAAAAAD4AfAAAfAD4AAD4AfAAAfAD4AAD4AfAAAAAAAAAAAAAA=="), 46, atob("Cg4QEBAQEBAQEBAQCQ=="), 39+(scale<<8)+(1<<16)); -} + // Actual height 39 (39 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAPgAAAAAB8AAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAD8AAAAAH/gAAAAP/8AAAAf//gAAA///AAAB//+AAAD//8AAAH//4AAAP//wAAAB//gAAAAP/AAAAAB+AAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///AAAf////8AP/////4B//////Af/////8D8AAAAfgeAAAAA8DwAAAAHgeAAAAA8D//////gf/////8B//////AP/////wAf////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAHgAAAAAA8AAAAAAPgAAAAAB4AAAAAAf/////gP/////8B//////gP/////8B//////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAD/+AAP8A//wAP/gP/+AH/8D//wD//gfgAA//8DwAAf+HgeAAP/A8DwAH/gHgfgP/wA8D///4AHgP//+AA8A///AAHgB//AAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AA/gAD/AAH/gA/4AA/+AP/AAH/4D/4AA//gfgA4AB8DwAPAAHgeAB4AA8DwAPgAHgfAD+AB8D//////gP/////4B//5//+AD/+H//gAH/AH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAP/AAAAAP/4AAAAP//AAAAP/x4AAAf/wPAAAf/gB4AAf/AAPAAP/AAB4AB//////gP/////8B//////gP/////8AAAAAPAAAAAAB4AAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//wD/AB///Af+AP//4D/4B///Af/gP//4B/8B4D4AAPgPAeAAA8B4DwAAHgPAfAAB8B4D////gPAf///4B4B////APAD///gAAAD//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAP////4AH/////wB//////Af/////8D8APAA/geADwAB8DwAeAAHgeADwAA8D4AeAAPgf/j+AH8B/8f///gP/h///4Af8H//+AAPgP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAPAAAAAAB4AAAABgPAAAA/8B4AAB//gPAAD//8B4AH///gPAH///8B4P//+AAPH//wAAB///gAAAP//AAAAB/+AAAAAP+AAAAAB+AAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4A/+AAf/w//+AP//v//4B//////Af/////8D4AfwAPgeAB8AA8DwAHAAHgeAB8AA8D4Af4APgf/////8B//////AP//v//4A//4//8AA/4A/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+AAAAD//+D/gB///4f+AP///j/4D///8f/gfAAHgB8DwAA8AHgeAAHgA8DwAA8AHgfgAHgB8D//////gP/////4A/////+AD/////gAD////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAfgAAB+AD8AAAPwAfgAAB+AD8AAAPwAfgAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DBATExMTExMTExMTCw=="), 45+(scale<<8)+(1<<16)); +}; + /* * Draw watch face @@ -94,92 +113,378 @@ function queueDraw() { }, 60000 - (Date.now() % 60000)); } +/** + * This function plots a data row in LCARS style. + * Note: It can be called async and therefore, the text alignment and + * font is set each time the function is called. + */ +function printRow(text, value, y, c){ + g.setFontAntonioMedium(); + g.setFontAlign(-1,-1,0); + g.setColor(c); + g.fillRect(79, y-2, 85 ,y+18); + + g.setFontAlign(0,-1,0); + g.drawString(value, 110, y); + + g.setColor(c); + g.setFontAlign(-1,-1,0); + g.fillRect(133, y-2, 165 ,y+18); + g.fillCircle(161, y+8, 10); + g.setColor(cBlack); + g.drawString(text, 135, y); +} + + +function drawData(key, y, c){ + try{ + _drawData(key, y, c); + } catch(ex){ + // Show last error - next try hopefully works. + } +} + + +function _drawData(key, y, c){ + key = key.toUpperCase() + var text = key; + var value = "ERR"; + var should_print= true; + + if(key == "STEPS"){ + text = "STEP"; + value = getSteps(); + + } else if(key == "BATTERY"){ + text = "BAT"; + value = E.getBattery() + "%"; + + } else if (key == "VREF"){ + value = E.getAnalogVRef().toFixed(2) + "V"; + + } else if(key == "HRM"){ + value = Math.round(Bangle.getHealthStatus("day").bpm); + + } else if (key == "TEMP"){ + var weather = getWeather(); + value = weather.temp; + + } else if (key == "HUMIDITY"){ + text = "HUM"; + var weather = getWeather(); + value = weather.hum; + + } else if (key == "ALTITUDE"){ + should_print= false; + text = "ALT"; + + // Immediately print something - avoid that its empty + printRow(text, "", y, c); + Bangle.getPressure().then(function(data){ + if(data && data.altitude){ + value = Math.round(data.altitude); + printRow(text, value, y, c); + } + }) + + } else if(key == "CORET"){ + value = locale.temp(parseInt(E.getTemperature())); + } + + // Print for all datapoints that are not async + if(should_print){ + printRow(text, value, y, c); + } +} + +function drawHorizontalBgLine(color, x1, x2, y, h){ + g.setColor(color); + + for(var i=0; i{ + data[h.day]+=h.bpm; + if (h.bpm) cnt[h.day]++; + }); + require("graph").drawBar(g, data, { + axes : true, + minx: 1, + gridx : 5, + gridy : 100, + width : 140, + height : 50, + x: 5, + y: 25 + }); + + // Plot step graph + var data = new Uint16Array(32); + health.readDailySummaries(new Date(), h=>data[h.day]+=h.steps/1000); + var gridY = parseInt(Math.max.apply(Math, data)/2); + gridY = gridY <= 0 ? 1 : gridY; + require("graph").drawBar(g, data, { + axes : true, + minx: 1, + gridx : 5, + gridy : gridY, + width : 140, + height : 50, + x: 5, + y: 115 + }); + + g.setFontAlign(1, 1, 0); + g.setFontAntonioMedium(); + g.setColor(cWhite); + g.drawString("M-HRM", 154, 27); + g.drawString("M-STEPS [K]", 154, 115); + + // Plot day + } else { + var data = new Uint16Array(24); + var cnt = new Uint8Array(24); + health.readDay(new Date(), h=>{ + data[h.hr]+=h.bpm; + if (h.bpm) cnt[h.hr]++; + }); + require("graph").drawBar(g, data, { + axes : true, + minx: 1, + gridx : 4, + gridy : 100, + width : 140, + height : 50, + x: 5, + y: 25 + }); + + // Plot step graph + var data = new Uint16Array(24); + health.readDay(new Date(), h=>data[h.hr]+=h.steps); + var gridY = parseInt(Math.max.apply(Math, data)/1000)*1000; + gridY = gridY <= 0 ? 1000 : gridY; + require("graph").drawBar(g, data, { + axes : true, + minx: 1, + gridx : 4, + gridy : gridY, + width : 140, + height : 50, + x: 5, + y: 115 + }); + + g.setFontAlign(1, 1, 0); + g.setFontAntonioMedium(); + g.setColor(cWhite); + g.drawString("D-HRM", 154, 27); + g.drawString("D-STEPS", 154, 115); + } +} function draw(){ - // First handle alarm to show this correctly afterwards handleAlarm(); // Next draw the watch face g.reset(); - g.clearRect(0, 24, g.getWidth(), g.getHeight()); + g.clearRect(0, 0, g.getWidth(), g.getHeight()); - // Draw background image - g.drawImage(backgroundImage, 0, 24); - - // Draw symbol - var bat = E.getBattery(); - var timeInMinutes = getCurrentTimeInMinutes(); - - var iconImg = - isAlarmEnabled() ? iconAlarm : - Bangle.isCharging() ? iconCharging : - bat < 30 ? iconNoBattery : - Bangle.isGPSOn() ? iconSatellite : - timeInMinutes % 4 == 0 ? iconSaturn : - timeInMinutes % 4 == 1 ? iconMars : - timeInMinutes % 4 == 2 ? iconMoon : - iconEarth; - g.drawImage(iconImg, 115, 115); - - // Alarm within symbol - g.setFontAlign(0,0,0); - g.setFontAntonioSmall(); - g.drawString(iconImg.text, 115+25, 102); - if(isAlarmEnabled() > 0){ - g.drawString(getAlarmMinutes(), 115+25, 115+25); + // Draw current lcars position + if(lcarsViewPos == 0){ + drawPosition0(); + } else if (lcarsViewPos == 1) { + drawPosition1(); } - // Write time - var currentDate = new Date(); - var timeStr = locale.time(currentDate,1); - g.setFontAlign(0,0,0); - g.setFontAntonioLarge(); - g.drawString(timeStr, 60, 55); - - // Write date - g.setFontAlign(-1,-1, 0); - g.setFontAntonioSmall(); - - var dayName = locale.dow(currentDate, true).toUpperCase(); - var day = currentDate.getDate(); - g.drawString(day, 100, 35); - g.drawString(dayName, 100, 55); - - // Draw battery - g.drawString("BAT:", 25, 98); - g.drawString(bat+ "%", 62, 98); - - // Draw steps - var steps = getSteps(); - g.drawString("STEP:", 25, 121); - g.drawString(steps, 62, 121); - - // Temperature - g.setFontAlign(-1,-1,0); - g.drawString("TEMP:", 25, 144); - g.drawString(Math.floor(E.getTemperature()) + "C", 62, 144); - // Queue draw in one minute queueDraw(); } + /* * Step counter via widget */ function getSteps() { - if (stepsWidget() !== undefined) - return stepsWidget().getSteps(); - return "???"; + var steps = 0; + let health; + try { + health = require("health"); + } catch(ex) { + return steps; + } + + health.readDay(new Date(), h=>steps+=h.steps); + return steps; } -function stepsWidget() { - if (WIDGETS.activepedom !== undefined) { - return WIDGETS.activepedom; - } else if (WIDGETS.wpedom !== undefined) { - return WIDGETS.wpedom; + +function getWeather(){ + var weather; + + try { + weather = require('weather').get(); + } catch(ex) { + // Return default } - return undefined; + + if (weather === undefined){ + weather = { + temp: "-", + hum: "-", + txt: "-", + wind: "-", + wdir: "-", + wrose: "-" + }; + } else { + weather.temp = locale.temp(Math.round(weather.temp-273.15)) + weather.hum = weather.hum + "%"; + } + + return weather; } @@ -191,7 +496,7 @@ function getCurrentTimeInMinutes(){ } function isAlarmEnabled(){ - return settings.alarm > 0; + return settings.alarm >= 0; } function getAlarmMinutes(){ @@ -216,65 +521,127 @@ function handleAlarm(){ .then(() => new Promise(resolve => setTimeout(resolve, t))) .then(() => Bangle.buzz(t, 1)) .then(() => new Promise(resolve => setTimeout(resolve, t))) - .then(() => Bangle.buzz(t, 1)); - - // Update alarm state to disabled - settings.alarm = -1; - Storage.writeJSON(filename, settings); + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, 5E3))) + .then(() => { + // Update alarm state to disabled + settings.alarm = -1; + storage.writeJSON(SETTINGS_FILE, settings); + }); } /* - * Swipe to set an alarm - */ -Bangle.on('swipe',function(dir) { - // Increase alarm - if(dir == -1){ - if(isAlarmEnabled()){ - settings.alarm += 5; - } else { - settings.alarm = getCurrentTimeInMinutes() + 5; - } - } - - // Decrease alarm - if(dir == +1){ - if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){ - settings.alarm -= 5; - } else { - settings.alarm = -1; - } - } - - // Update UI - draw(); - - // Update alarm state - Storage.writeJSON(filename, settings); -}); - - -/* - * Stop updates when LCD is off, restart when on + * Listeners */ Bangle.on('lcdPower',on=>{ if (on) { - draw(); // draw immediately, queue redraw + // Whenever we connect to Gadgetbridge, reading data from + // health failed. Therefore, we update only partially... + drawInfo(); + drawState(); } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } }); +Bangle.on('lock', function(isLocked) { + drawInfo(); +}); + +Bangle.on('charging',function(charging) { + drawState(); +}); + + +function increaseAlarm(){ + if(isAlarmEnabled()){ + settings.alarm += 5; + } else { + settings.alarm = getCurrentTimeInMinutes() + 5; + } + + storage.writeJSON(SETTINGS_FILE, settings); +} + + +function decreaseAlarm(){ + if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){ + settings.alarm -= 5; + } else { + settings.alarm = -1; + } + + storage.writeJSON(SETTINGS_FILE, settings); +} + +function feedback(){ + Bangle.buzz(40, 0.3); +} + +// Touch gestures to control clock. We don't use swipe to be compatible with the bangle ecosystem +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.2); + var right = g.getWidth() - left; + var upper = parseInt(g.getHeight() * 0.2); + var lower = g.getHeight() - upper; + + var is_left = e.x < left; + var is_right = e.x > right; + var is_upper = e.y < upper; + var is_lower = e.y > lower; + + if(is_left && lcarsViewPos == 1){ + feedback(); + lcarsViewPos = 0; + draw(); + return; + + } else if(is_right && lcarsViewPos == 0){ + feedback(); + lcarsViewPos = 1; + draw(); + return; + } + + if(lcarsViewPos == 0){ + if(is_upper){ + feedback(); + increaseAlarm(); + drawState(); + return; + } if(is_lower){ + feedback(); + decreaseAlarm(); + drawState(); + return; + } + } else if (lcarsViewPos == 1 && (is_upper || is_lower) && plotMonth != is_lower){ + feedback(); + plotMonth = is_lower; + draw(); + return; + } +}); + + +/* + * Lets start widgets, listen for btn etc. + */ // Show launcher when middle button pressed Bangle.setUI("clock"); - -// Load widgets - needed by draw 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="";} // Clear the screen once, at startup and draw clock g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); draw(); // After drawing the watch face, we can draw the widgets -Bangle.drawWidgets(); \ No newline at end of file +// Bangle.drawWidgets(); diff --git a/apps/lcars/lcars.icon.js b/apps/lcars/lcars.icon.js index c404728e0..22e98a39a 100644 --- a/apps/lcars/lcars.icon.js +++ b/apps/lcars/lcars.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwgeevPnAQsc+fPngCE+/fvoCEvAbIA4/AgFzEZwRBjwjNvBUBEZ3eCIMOEZtwCIMBEZuARYU5EZecTocHEZf0CIcBEbvgaggjKTwIAEbQpoHAAiSEeoYQHJQr1CCBJKEIgcBI4xKFaIdt3AOFgfuAYMeEYLRBj1pLQ4ICuYjBAgPbtoRHhu3AYN5VoMGzVpI49502AgPPVoM27dsK48N23cgE5CgOmzVoCI4LBzCSB8EP2wjJgILBAYMAhIjBsAjJzVwg47C7YRJEYhfBEZXmEZ53CI4q2BEAiVCkwjCNYaMGboQjDkBfDCAbdB04EBgyPDC4YAD/dt2wRCHIM5njXCCAcHboOmCIQ0B5/nfYT6DFIIjBeAcOvM8+EAjitFEYJEBAANzEYOeeowjCFgUDzwjB+YrDgAgBEYWcA4Mc+YjCvAQCgftEANuDIYOBEYXPNwIAIg4OCCgXkCBEOEZDvBEAhEB4AjF/inB8+OJQOOvILBoAjGU4IFDAQYjGbQIdCAQt4EY0DEZACDEYceEZACDC4bLBEZwCO")) +require("heatshrink").decompress(atob("mEwgeevPnAQsc+fPngCE+/fvoCEvAbIA4/AgFzEZwRBjwjNvBUBEZ3eCIMOEZtwCIMBEZuARYU5EZecTocHEZf0CIcBEbvgaggjKTwIAEbQpoHAAiSEeoYQHJQr1CCBJKEIgcBI4xKFaIdt3AOFgfuAYMeEYLRBj1pLQ4ICuYjBAgPbtoRHhu3AYN5VoMGzVpI49502AgPPVoM27dsK48N23cgE5CgOmzVoCI4LBzCSB8EP2wjJgILBAYMAhIjBsAjJzVwg47C7YRJEYhfBEZXmEZ53CI4q2BEAiVCkwjCNYaMGboQjDkBfDCAbdB04EBgyPDC4YAD/dt2wRCHIM5njXCCAcHboOmCIQ0B5/nfYT6DFIIjBeAcOvM8+EAjitFEYJEBAANzEYOeeowjCFgUDzwjB+YrDgAgBEYWcA4Mc+YjCvAQCgftEANuDIYOBEYXPNwIAIg4OCCgXkCBEOEZDvBEAhEB4AjF/inB8+OJQOOvILBoAjGU4IFDAQYjGbQIdCAQt4EY0DEZACDEYceEZACDC4bLBEZwCO")) \ No newline at end of file diff --git a/apps/lcars/lcars.png b/apps/lcars/lcars.png index 167352ef4..9264f93f2 100644 Binary files a/apps/lcars/lcars.png and b/apps/lcars/lcars.png differ diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js new file mode 100644 index 000000000..076dea4d1 --- /dev/null +++ b/apps/lcars/lcars.settings.js @@ -0,0 +1,54 @@ +(function(back) { + const SETTINGS_FILE = "lcars.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + alarm: -1, + dataRow1: "Battery", + dataRow2: "Steps", + dataRow3: "Temp" + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + function save() { + storage.write(SETTINGS_FILE, settings) + } + + var data_options = ["Steps", "Battery", "VREF", "HRM", "Temp", "Humidity", "Altitude", "CoreT"]; + + E.showMenu({ + '': { 'title': 'LCARS Clock' }, + '< Back': back, + 'Row 1': { + value: 0 | data_options.indexOf(settings.dataRow1), + min: 0, max: 7, + format: v => data_options[v], + onchange: v => { + settings.dataRow1 = data_options[v]; + save(); + }, + }, + 'Row 2': { + value: 0 | data_options.indexOf(settings.dataRow2), + min: 0, max: 7, + format: v => data_options[v], + onchange: v => { + settings.dataRow2 = data_options[v]; + save(); + }, + }, + 'Row 3': { + value: 0 | data_options.indexOf(settings.dataRow3), + min: 0, max: 7, + format: v => data_options[v], + onchange: v => { + settings.dataRow3 = data_options[v]; + save(); + }, + } + }); +}) diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json new file mode 100644 index 000000000..2d04ebdf6 --- /dev/null +++ b/apps/lcars/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "lcars", + "name": "LCARS Clock", + "shortName":"LCARS", + "icon": "lcars.png", + "version":"0.14", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "description": "Library Computer Access Retrieval System (LCARS) clock.", + "type": "clock", + "tags": "clock", + "screenshots": [{"url":"screenshot.png"}], + "storage": [ + {"name":"lcars.app.js","url":"lcars.app.js"}, + {"name":"lcars.img","url":"lcars.icon.js","evaluate":true}, + {"name":"lcars.settings.js","url":"lcars.settings.js"} + ] +} diff --git a/apps/lcars/screenshot.png b/apps/lcars/screenshot.png index 70db639eb..120229fba 100644 Binary files a/apps/lcars/screenshot.png and b/apps/lcars/screenshot.png differ diff --git a/apps/lcars/screenshot_2.png b/apps/lcars/screenshot_2.png new file mode 100644 index 000000000..52ad295c4 Binary files /dev/null and b/apps/lcars/screenshot_2.png differ diff --git a/apps/life/metadata.json b/apps/life/metadata.json new file mode 100644 index 000000000..5a40f0fb1 --- /dev/null +++ b/apps/life/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "life", + "name": "Game of Life", + "version": "0.04", + "description": "Conway's Game of Life - 16x16 board", + "icon": "life.png", + "tags": "game", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-game-of-life-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"life.app.js","url":"life.min.js"}, + {"name":"life.img","url":"life-icon.js","evaluate":true} + ] +} diff --git a/apps/lifeclk/metadata.json b/apps/lifeclk/metadata.json new file mode 100644 index 000000000..6b62860ae --- /dev/null +++ b/apps/lifeclk/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "lifeclk", + "name": "Game of Life Clock", + "shortName": "Conway's Clock", + "version": "0.06", + "description": "Modification and clockification of Conway's Game of Life", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"lifeclk.app.js","url":"app.min.js"}, + {"name":"lifeclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog new file mode 100644 index 000000000..7a7ecd027 --- /dev/null +++ b/apps/lightswitch/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Add the option to enable touching the widget only on clock and settings. diff --git a/apps/lightswitch/README.md b/apps/lightswitch/README.md new file mode 100644 index 000000000..d58de7ca4 --- /dev/null +++ b/apps/lightswitch/README.md @@ -0,0 +1,102 @@ +# Light Switch Widget + +Whis this widget I wanted to create a solution to quickly en-/disable the LCD backlight and even change the brightness. +In addition it shows the lock status with the option to personalize the lock icon with a tiny image. + +--- +### Control +--- +* __On / off__ + Single touch the widget to en-/disable the backlight. +* __Change brightness__ _(can be disabled)_ + First touch the widget, then quickly touch the screen again and drag up/down until you reach your wished brigthness. +* __Double tap to flash backlight__ _(can be disabled)_ + By defaut you can double tap on the right side of your bangle to flash the backlight for a short duration. + (While the backlight is active your bangle will be unlocked.) +* __Double tap to unlock__ _(disabled by default)_ + If a side is defined in the app settings, your bangle will be unlocked if you double tap on that side. + +--- +### Settings +--- +#### Widget - Change the apperance of the widget: +* __Bulb col__ + _red_ / _yellow_ / _green_ / __cyan__ / _blue_ / _magenta_ + Define the color used for the lightbulbs inner circle. + The selected color will be dimmed depending on the actual brightness value. +* __Image__ + __default__ / _random_ / _..._ + Set your favourite lock icon image. (If no image file is found _no image_ will be displayed.) + * _random_ -> Select a random image on each time the widget is drawn. + +#### Control - Change when and how to use the widget: +* __Touch__ + _on def clk_ / _on all clk_ / _clk+setting_ / _clk+launch_ / _except apps_ / __always on__ + Select when touching the widget is active to en-/disable the backlight. + * _on def clk_ -> only on your selected main clock face + * _on all clk_ -> on all apps of the type _clock_ + * _clk+setting_ -> on all apps of the type _clock_ and in the settings + * _clk+launch_ -> on all apps of the types _clock_ and _launch_ + * _except apps_ -> on all apps of the types _clock_ and _launch_ and in the settings + * _always on_ -> always enabled when the widget is displayed +* __Drag Delay__ + _off_ / _50ms_ / _100ms_ / _..._ / __500ms__ / _..._ / _1000ms_ + Change the maximum delay between first touch and re-touch/drag to change the brightness or disable changing the brightness completely. +* __Min Value__ + _1%_ / _2%_ / _..._ / __10%__ / _..._ / _100%_ + Set the minimal level of brightness you can change to. + +#### Unlock - Set double tap side to unlock: +* __TapSide__ + __off__ / _left_ / _right_ / _top_ / _bottom_ / _front_ / _back_ + +#### Flash - Change if and how to flash the backlight: +* __TapSide__ + _off_ / _left_ / __right__ / _top_ / _bottom_ / _front_ / _back_ + Set double tap side to flash the backlight or disable completely. +* __Tap__ + _on locked_ / _on unlocked_ / __always on__ + Select when a double tap is recognised. +* __Timeout__ + _0.5s_ / _1s_ / _..._ / __2s__ / _..._ / _10s_ + Change how long the backlight will be activated on a flash. +* __Min Value__ + _1%_ / _2%_ / _..._ / __20%__ / _..._ / _100%_ + Set the minimal level of brightness for the backlight on a flash. + +--- +### Images +--- + +| Lightbulb | Default lock icon | +|:-----------------------------:|:-----------------------:| +| ![](images/lightbulb.png) | ![](images/default.png) | +| ( _full_ / _dimmed_ / _off_ ) | ( _on_ / _off_ ) | + +Examples in default light and dark theme. + +| Lock | Heart | Invader | JS | Smiley | Skull | Storm | +|:----:|:-----:|:-------:|:--:|:------:|:-----:|:-----:| +| ![](images/image_lock.png) | ![](images/image_heart.png) | ![](images/image_invader.png) | ![](images/image_js.png) | ![](images/image_smiley.png) | ![](images/image_skull.png) | ![](images/image_storm.png) | + +This images are stored in a seperate file _(lightswitch.images.json)_. + +--- +### Worth Mentioning +--- +#### To do list +* Catch the touch and draw input related to this widget to prevent actions in the active app. + _(For now I have no idea how to achieve this, help is appreciated)_ +* Manage images for the lock icon through a _Customize and Upload App_ page. + +#### Requests, Bugs and Feedback +Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) or send me a [mail](mailto:banglejs@storm64.de). + +#### Thanks +Huge thanks to Gordon Williams and all the motivated developers. + +#### Creator +Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64)) + +#### License +[MIT License](LICENSE) diff --git a/apps/lightswitch/boot.js b/apps/lightswitch/boot.js new file mode 100644 index 000000000..d57679b56 --- /dev/null +++ b/apps/lightswitch/boot.js @@ -0,0 +1,17 @@ +// load settings +var settings = Object.assign({ + value: 1, + isOn: true +}, require("Storage").readJSON("lightswitch.json", true) || {}); + +// set brightness +Bangle.setLCDBrightness(settings.isOn ? settings.value : 0); + +// remove tap listener to prevent uncertainties +Bangle.removeListener("tap", require("lightswitch.js").tapListener); + +// add tap listener to unlock and/or flash backlight +if (settings.unlockSide || settings.tapSide) Bangle.on("tap", require("lightswitch.js").tapListener); + +// clear variable +settings = undefined; diff --git a/apps/lightswitch/images.json b/apps/lightswitch/images.json new file mode 100644 index 000000000..dcdb72470 --- /dev/null +++ b/apps/lightswitch/images.json @@ -0,0 +1,37 @@ +{ + "lock": { + "str": "BQcBAAEYxiA=", + "x": 9, + "y": 15, + }, + "heart": { + "str": "CQjBAQD4//+chAAACA4Pj+8=", + "x": 7, + "y": 14, + }, + "invader": { + "str": "DQqDASQASQASSEAAECSQEAEASQEkkkAQEgkgkAEkkkkkAgkkkggEEAAEEAAEgkAASQAAASQ=", + "x": 5, + "y": 13, + }, + "js": { + "str": "CAqBAd//2NfZ3tHfX78=", + "x": 7, + "y": 13, + }, + "skull": { + "str": "CQqBAcHAZTKcH/+OfMGfAA==", + "x": 7, + "y": 13, + }, + "smiley": { + "str": "CwqDASQAAASQNtsAQNttsANgMBsBsBgNgNtttsBsNsNgBsANgCBttgCSAAACQA==", + "x": 6, + "y": 13, + }, + "storm": { + "str": "CQmDASAAACBttgBgABgBttgCMAACQNsASRgASSBgCSSACSA=", + "x": 7, + "y": 13, + } +} diff --git a/apps/lightswitch/images/README.md b/apps/lightswitch/images/README.md new file mode 100644 index 000000000..5e2a71ce5 --- /dev/null +++ b/apps/lightswitch/images/README.md @@ -0,0 +1 @@ +# Light Switch Images diff --git a/apps/lightswitch/images/app.png b/apps/lightswitch/images/app.png new file mode 100644 index 000000000..233ef02e2 Binary files /dev/null and b/apps/lightswitch/images/app.png differ diff --git a/apps/lightswitch/images/default.png b/apps/lightswitch/images/default.png new file mode 100644 index 000000000..6fc371687 Binary files /dev/null and b/apps/lightswitch/images/default.png differ diff --git a/apps/lightswitch/images/image_heart.png b/apps/lightswitch/images/image_heart.png new file mode 100644 index 000000000..75935bbf8 Binary files /dev/null and b/apps/lightswitch/images/image_heart.png differ diff --git a/apps/lightswitch/images/image_invader.png b/apps/lightswitch/images/image_invader.png new file mode 100644 index 000000000..31e3e4fb4 Binary files /dev/null and b/apps/lightswitch/images/image_invader.png differ diff --git a/apps/lightswitch/images/image_js.png b/apps/lightswitch/images/image_js.png new file mode 100644 index 000000000..501b15f03 Binary files /dev/null and b/apps/lightswitch/images/image_js.png differ diff --git a/apps/lightswitch/images/image_lock.png b/apps/lightswitch/images/image_lock.png new file mode 100644 index 000000000..4315cd172 Binary files /dev/null and b/apps/lightswitch/images/image_lock.png differ diff --git a/apps/lightswitch/images/image_skull.png b/apps/lightswitch/images/image_skull.png new file mode 100644 index 000000000..0f6065b74 Binary files /dev/null and b/apps/lightswitch/images/image_skull.png differ diff --git a/apps/lightswitch/images/image_smiley.png b/apps/lightswitch/images/image_smiley.png new file mode 100644 index 000000000..f97533f1d Binary files /dev/null and b/apps/lightswitch/images/image_smiley.png differ diff --git a/apps/lightswitch/images/image_storm.png b/apps/lightswitch/images/image_storm.png new file mode 100644 index 000000000..e3bec672b Binary files /dev/null and b/apps/lightswitch/images/image_storm.png differ diff --git a/apps/lightswitch/images/lightbulb.png b/apps/lightswitch/images/lightbulb.png new file mode 100644 index 000000000..ed861ab91 Binary files /dev/null and b/apps/lightswitch/images/lightbulb.png differ diff --git a/apps/lightswitch/images/screenshot_1.png b/apps/lightswitch/images/screenshot_1.png new file mode 100644 index 000000000..3b62dfcd7 Binary files /dev/null and b/apps/lightswitch/images/screenshot_1.png differ diff --git a/apps/lightswitch/images/screenshot_2.png b/apps/lightswitch/images/screenshot_2.png new file mode 100644 index 000000000..f1d69a6ae Binary files /dev/null and b/apps/lightswitch/images/screenshot_2.png differ diff --git a/apps/lightswitch/images/screenshot_3.png b/apps/lightswitch/images/screenshot_3.png new file mode 100644 index 000000000..4523086e4 Binary files /dev/null and b/apps/lightswitch/images/screenshot_3.png differ diff --git a/apps/lightswitch/images/screenshot_4.png b/apps/lightswitch/images/screenshot_4.png new file mode 100644 index 000000000..6e4befd95 Binary files /dev/null and b/apps/lightswitch/images/screenshot_4.png differ diff --git a/apps/lightswitch/lib.js b/apps/lightswitch/lib.js new file mode 100644 index 000000000..eb720e69a --- /dev/null +++ b/apps/lightswitch/lib.js @@ -0,0 +1,124 @@ +// from boot accassible functions +exports = { + // listener function // + // tap listener to flash backlight + tapListener: function(data) { + // check for double tap and direction + if (data.double) { + // setup shortcut to this widget or load from storage + var w = global.WIDGETS ? WIDGETS.lightswitch : Object.assign({ + unlockSide: "", + tapSide: "right", + tapOn: "always", + }, require("Storage").readJSON("lightswitch.json", true) || {}); + + // cache lock status + var locked = Bangle.isLocked(); + + // check to unlock + if (locked && data.dir === w.unlockSide) Bangle.setLocked(); + + // check to flash + if (data.dir === w.tapSide && (w.tapOn === "always" || locked === (w.tapOn === "locked"))) require("lightswitch.js").flash(); + + // clear variables + w = undefined; + locked = undefined; + } + }, + + // external function // + // function to flash backlight + flash: function(tOut) { + // setup shortcut to this widget or load from storage + var w = global.WIDGETS ? WIDGETS.lightswitch : Object.assign({ + tOut: 3000, + minFlash: 0.2, + value: 1, + isOn: true + }, require("Storage").readJSON("lightswitch.json", true) || {}); + + // chack if locked, backlight off or actual value lower then minimal flash value + if (Bangle.isLocked() || !w.isOn || w.value < w.minFlash) { + + // set inner bulb and brightness + var setBrightness = function(w, value) { + if (w.drawInnerBulb) w.drawInnerBulb(value); + Bangle.setLCDBrightness(value); + }; + + // override timeout if defined + if (!tOut) tOut = w.tOut; + + // check lock state + if (Bangle.isLocked()) { + // cache options + var options = Bangle.getOptions(); + // set shortened lock and backlight timeout + Bangle.setOptions({ + lockTimeout: tOut, + backlightTimeout: tOut + }); + // unlock + Bangle.setLocked(false); + // set timeout to reset options + setTimeout(Bangle.setOptions, tOut + 100, options); + + // clear variable + options = undefined; + } else { + // set timeout to reset backlight + setTimeout((w, funct) => { + if (!Bangle.isLocked()) funct(w, w.isOn ? w.value : 0); + }, tOut, w, setBrightness); + } + + // enable backlight + setTimeout((w, funct) => { + funct(w, w.value < w.minFlash ? w.minFlash : w.value); + }, 10, w, setBrightness); + + // clear variable + setBrightness = undefined; + } + + // clear variable + w = undefined; + }, + + // external access to internal function // + // refference to widget function or set backlight and write to storage if not skipped + changeValue: function(value, skipWrite) { + // check if widgets are loaded + if (global.WIDGETS) { + // execute inside widget + WIDGETS.lightswitch.changeValue(value, skipWrite); + } else { + // load settings from storage + var filename = "lightswitch.json"; + var storage = require("Storage"); + var settings = Object.assign({ + value: 1, + isOn: true + }, storage.readJSON(filename, true) || {}); + + // check value + if (value) { + // set new value + settings.value = value; + } else { + // switch backlight status + settings.isOn = !settings.isOn; + } + // set brightness + Bangle.setLCDBrightness(settings.isOn ? settings.value : 0); + // write changes to storage if not skipped + if (!skipWrite) storage.writeJSON(filename, settings); + + // clear variables + filename = undefined; + storage = undefined; + settings = undefined; + } + } +}; diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json new file mode 100644 index 000000000..902b1536b --- /dev/null +++ b/apps/lightswitch/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "lightswitch", + "name": "Light Switch Widget", + "shortName": "Light Switch", + "version": "0.02", + "description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.", + "icon": "images/app.png", + "screenshots": [ + {"url": "images/screenshot_1.png"}, + {"url": "images/screenshot_2.png"}, + {"url": "images/screenshot_3.png"}, + {"url": "images/screenshot_4.png"} + ], + "type": "widget", + "tags": "tool,widget,brightness,lock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name": "lightswitch.boot.js", "url": "boot.js"}, + {"name": "lightswitch.js", "url": "lib.js"}, + {"name": "lightswitch.settings.js", "url": "settings.js"}, + {"name": "lightswitch.wid.js", "url": "widget.js"} + ], + "data": [ + {"name": "lightswitch.json"}, + {"name": "lightswitch.images.json", "url": "images.json"} + ] +} diff --git a/apps/lightswitch/settings.js b/apps/lightswitch/settings.js new file mode 100644 index 000000000..aac159148 --- /dev/null +++ b/apps/lightswitch/settings.js @@ -0,0 +1,155 @@ +(function(back) { + var filename = "lightswitch.json"; + + // set Storage and load settings + var storage = require("Storage"); + var settings = Object.assign({ + colors: "011", + image: "default", + touchOn: "clock,launch", + dragDelay: 500, + minValue: 0.1, + unlockSide: "", + tapSide: "right", + tapOn: "always", + tOut: 2000, + minFlash: 0.2 + }, storage.readJSON(filename, true) || {}); + var images = storage.readJSON(filename.replace(".", ".images."), true) || false; + + // write change to storage and widget + function writeSetting(key, value, drawWidgets) { + // reread settings to only change key + settings = Object.assign(settings, storage.readJSON(filename, true) || {}); + // change the value of key + settings[key] = value; + // write to storage + storage.writeJSON(filename, settings); + // check if widgets are loaded + if (global.WIDGETS) { + // setup shortcut to the widget + var w = WIDGETS.lightswitch; + // assign changes to widget + w = Object.assign(w, settings); + // redraw widgets if neccessary + if (drawWidgets) Bangle.drawWidgets(); + } + } + + // generate entry for circulating values + function getEntry(key) { + var entry = entries[key]; + // check for existing titles to decide value type + if (entry.value) { + // return entry for string value + return { + value: entry.value.indexOf(settings[key]), + format: v => entry.title ? entry.title[v] : entry.value[v], + onchange: function(v) { + this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v; + writeSetting(key, entry.value[v], entry.drawWidgets); + if (entry.exec) entry.exec(entry.value[v]); + } + }; + } else { + // return entry for numerical value + return { + value: settings[key] * entry.factor, + step: entry.step, + format: v => v > 0 ? v + entry.unit : "off", + onchange: function(v) { + this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v; + writeSetting(key, v / entry.factor, entry.drawWidgets); + }, + }; + } + } + + // define menu entries with circulating values + var entries = { + colors: { + title: ["red", "yellow", "green", "cyan", "blue", "magenta"], + value: ["100", "110", "010", "011", "001", "101"], + drawWidgets: true + }, + image: { + title: images ? undefined : ["no found"], + value: images ? ["default", "random"].concat(Object.keys(images)) : ["default"], + exec: function(value) { + // draw selected image in upper right corner + var x = 152, + y = 26, + i = images ? images[value] : false; + g.reset(); + if (!i) g.setColor(g.theme.bg); + g.drawImage(atob("Dw+BADAYYDDAY//v////////////////////////3/8A"), x + 4, y); + if (i) g.drawImage(atob(i.str), x + i.x, y - 9 + i.y); + i = undefined; + } + }, + touchOn: { + title: ["on def clk", "on all clk", "clk+launch", "clk+setting", "except apps", "always on"], + value: ["", "clock", "clock,setting.app.js", "clock,launch", "clock,setting.app.js,launch", "always"], + drawWidgets: true + }, + dragDelay: { + factor: 1, + unit: "ms", + min: 0, + max: 1000, + step: 50 + }, + minValue: { + factor: 100, + unit: "%", + min: 1, + max: 100, + step: 1 + }, + unlockSide: { + title: ["off", "left", "right", "top", "bottom", "front", "back"], + value: ["", "left", "right", "top", "bottom", "front", "back"] + }, + tapOn: { + title: ["on locked", "on unlocked", "always on"], + value: ["locked", "unlocked", "always"] + }, + tOut: { + factor: 0.001, + unit: "s", + min: 0.5, + max: 10, + step: 0.5 + } + }; + // copy duplicated entries + entries.tapSide = entries.unlockSide; + entries.minFlash = entries.minValue; + + // show main menu + function showMain() { + var mainMenu = E.showMenu({ + "": { + title: "Light Switch" + }, + "< Back": () => back(), + "-- Widget --------": 0, + "Bulb col": getEntry("colors"), + "Image": getEntry("image"), + "-- Control -------": 0, + "Touch": getEntry("touchOn"), + "Drag Delay": getEntry("dragDelay"), + "Min Value": getEntry("minValue"), + "-- Unlock --------": 0, + "TapSide": getEntry("unlockSide"), + "-- Flash ---------": 0, + "TapSide ": getEntry("tapSide"), + "Tap": getEntry("tapOn"), + "Timeout": getEntry("tOut"), + "Min Value ": getEntry("minFlash") + }); + } + + // draw main menu + showMain(); +}) diff --git a/apps/lightswitch/settings.json b/apps/lightswitch/settings.json new file mode 100644 index 000000000..3d88e2282 --- /dev/null +++ b/apps/lightswitch/settings.json @@ -0,0 +1,72 @@ +/*** Available settings for lightswitch *** + + * colors: string // colors used for the bulb + // set with g.setColor(val*col[0], val*col[1], val*col[2]) + "100" -> red + "110" -> yellow + "010" -> green + "011" -> cyan (default) + "001" -> blue + "101" -> magenta + + * image: string // + "default" -> + "random" -> + + * touchOn: string // select when widget touch is active + "" -> only on default clock + "clock" -> on all clocks + "clock,launch" -> on all clocks and lanchers (default) + "always" -> always + + * dragDelay: int // drag listener reset time in ms + // time until a drag is needed to activate backlight changing mode + 0 -> disabled + 500 -> (default) + + * minValue: float // minimal brightness level that can be set by dragging + 0.05 to 1, 0.1 as default + + * unlockSide: string // side of the watch to double tap on to flash backlight + 0/false/undefined -> backlight flash disabled + right/left/up/down/front/back -> side to tap on (default: right) + + * tapSide: string // side of the watch to double tap on to flash backlight + 0/false/undefined -> backlight flash disabled + right/left/up/down/front/back -> side to tap on (default: right) + + * tapOn: string // select when tap to flash backlight is active + "locked" -> only when locked + "unlocked" -> only when unlocked (default) + "always" -> always + + * tOut: int // backlight flash timeout in ms + 3000 (default) + + * minFlash: float // minimal brightness level when + 0.05 to 1, 0.2 as default + + *** Cached values *** + + * value: float // active brightness value (0-1) + 1 (default) + * isOn: bool // active backlight status + true (default) + + */ +{ + // settings + "colors": "011", + "image": "default", + "touchOn": "clock,launch", + "dragDelay": 500, + "minValue": 0.1, + "unlockSide": "", + "tapSide": "right", + "tapOn": "always", + "tOut": 2000, + "minFlash": 0.2, + // cached values + "value": 1, + "isOn": true +} diff --git a/apps/lightswitch/widget.js b/apps/lightswitch/widget.js new file mode 100644 index 000000000..119a114fe --- /dev/null +++ b/apps/lightswitch/widget.js @@ -0,0 +1,255 @@ +(function() { + // load settings + var settings = Object.assign({ + colors: "011", + image: "default", + touchOn: "clock,launch", + dragDelay: 500, + minValue: 0.1, + unlockSide: "", + tapSide: "right", + tapOn: "always", + tOut: 3000, + value: 1, + isOn: true + }, require("Storage").readJSON("lightswitch.json", true) || {}); + + // write widget with loaded settings + WIDGETS.lightswitch = Object.assign(settings, { + + // set area, sortorder, width and dragStatus + area: "tr", + sortorder: 10, + width: 23, + dragStatus: "off", + + // internal function // + // write settings to storage + writeSettings: function(changes) { + // define variables + var filename = "lightswitch.json"; + var storage = require("Storage"); + + // write changes into json file + storage.writeJSON(filename, Object.assign( + storage.readJSON(filename, true) || {}, changes + )); + + // clear variables + filename = undefined; + storage = undefined; + }, + + // internal function // + // draw inner bulb circle + drawInnerBulb: function(value) { + // check if active or value is set + if (value || this.isOn) { + // use set value or load from widget + value = value || this.value; + // calculate color + g.setColor( + value * this.colors[0], + value * this.colors[1], + value * this.colors[2] + ); + } else { + // backlight off + g.setColor(0); + } + // draw circle + g.drawImage(atob("CwuBAB8H8f9/////////f8fwfAA="), this.x + 6, this.y + 6); + }, + + // internal function // + // draw widget icon + drawIcon: function(locked) { + // define icons + var icons = { + bulb: "DxSBAAAAD4BgwYDCAIgAkAEgAkAEgAiAIYDBgwH8A/gH8A/gH8AfABwA", + shine: "FxeBAAgQIAgggBBBABAECAAALAABhAAEAAAAAAAAAAAAAAAHAABwAAAAAAAAAAAAAAAQABDAABoAAAgQBABABACACAIACAA=", + lock: "DxCBAAAAH8B/wMGBgwMGBgwf/H/8+Pnx8/fn78/fn/8f/A==", + image: "DxSBAA/gP+Dg4YDDAYYDDAYYDH/9////////////////////////+//g" + }; + // read images + var images = require("Storage").readJSON("lightswitch.images.json", true) || false; + + // select image if images are found + var image = (!images || image === "default") ? false : + (function(i) { + if (i === "random") { + i = Object.keys(images); + i = i[parseInt(Math.random() * i.length)]; + } + return images[i]; + })(this.image); + + // clear widget area + g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 24); + + // draw shine if backlight is active + if (this.isOn) g.drawImage(atob(icons.shine), this.x, this.y); + + // draw icon depending on lock status and image + g.drawImage(atob(!locked ? icons.bulb : image ? icons.image : icons.lock), this.x + 4, this.y + 4); + + // draw image on lock + if (locked && image) g.drawImage(atob(image.str), this.x + image.x, this.y + image.y); + + // draw bulb color depending on backlight status + if (!locked) this.drawInnerBulb(); + + // clear variables + icons = undefined; + images = undefined; + image = undefined; + }, + + // internal function // + // change or switch backlight and icon and write to storage if not skipped + changeValue: function(value, skipWrite) { + // check value + if (value) { + // set new value + this.value = value; + // check backlight status + if (this.isOn) { + // redraw only inner bulb circle + this.drawInnerBulb(); + } else { + // activate backlight + this.isOn = true; + // redraw complete widget icon + this.drawIcon(false); + } + } else { + // switch backlight status + this.isOn = !this.isOn; + // redraw widget icon + this.drawIcon(false); + } + // set brightness + Bangle.setLCDBrightness(this.isOn ? this.value : 0); + // write changes to storage if not skipped + if (!skipWrite) this.writeSettings({ + isOn: this.isOn, + value: this.value + }); + }, + + // listener function // + // drag listener for brightness change mode + dragListener: function(event) { + // setup shortcut to this widget + var w = WIDGETS.lightswitch; + + // first drag recognised + if (event.b && typeof w.dragStatus === "number") { + // reset drag timeout + clearTimeout(w.dragStatus); + // change drag status to indicate ongoing drag action + w.dragStatus = "ongoing"; + // feedback for brightness change mode + Bangle.buzz(50); + } + + // read y position, pleasant usable area 20-170 + var y = event.y; + y = y < 20 ? 0 : y > 170 ? 150 : y - 20; + // calculate brightness respecting minimal value in settings + var value = (1 - Math.round(y / 1.5) / 100) * (1 - w.minValue) + w.minValue; + + // change brigthness value, skip write to storage while still touching + w.changeValue(value, event.b); + + // on touch release remove drag listener and reset drag status to indicate stopped drag action + if (!event.b) { + Bangle.removeListener("drag", w.dragListener); + w.dragStatus = "off"; + } + + // clear variables + w = undefined; + y = undefined; + value = undefined; + }, + + // listener function // + // touch listener for light control + touchListener: function(button, cursor) { + // setup shortcut to this widget + var w = WIDGETS.lightswitch; + + // skip all if drag action ongoing + if (w.dragStatus === "off") { + + // check if inside widget area + if (!(!w || cursor.x < w.x || cursor.x > w.x + w.width || + cursor.y < w.y || cursor.y > w.y + 23)) { + // first touch feedback + Bangle.buzz(25); + // check if drag is disabled + if (w.dragDelay) { + // add drag listener + Bangle.on("drag", w.dragListener); + // set drag timeout + w.dragStatus = setTimeout((w) => { + // remove drag listener + Bangle.removeListener("drag", w.dragListener); + // clear drag timeout + if (typeof w.dragStatus === "number") clearTimeout(w.dragStatus); + // reset drag status to indicate stopped drag action + w.dragStatus = "off"; + }, w.dragDelay, w); + } + // switch backlight + w.changeValue(); + } + + } + + // clear variable + w = undefined; + }, + + // main widget function // + // display and setup/reset function + draw: function(locked) { + // setup shortcut to this widget + var w = WIDGETS.lightswitch; + + // set lcd brightness on unlocking + // all other cases are catched by the boot file + if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0); + + // read lock status + locked = Bangle.isLocked(); + + // remove listeners to prevent uncertainties + Bangle.removeListener("lock", w.draw); + Bangle.removeListener("touch", w.touchListener); + Bangle.removeListener("tap", require("lightswitch.js").tapListener); + + // draw widget icon + w.drawIcon(locked); + + // add lock listener + Bangle.on("lock", w.draw); + + // add touch listener to control the light depending on settings + if (w.touchOn === "always" || !global.__FILE__ || + w.touchOn.includes(__FILE__) || + w.touchOn.includes(require("Storage").readJSON(__FILE__.replace("app.js", "info")).type)) + Bangle.on("touch", w.touchListener); + + // add tap listener to unlock and/or flash backlight + if (w.unlockSide || w.tapSide) Bangle.on("tap", require("lightswitch.js").tapListener); + + // clear variables + w = undefined; + } + }); + + // clear variable + settings = undefined; +})() diff --git a/apps/limelight/ChangeLog b/apps/limelight/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/limelight/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/limelight/README.md b/apps/limelight/README.md new file mode 100644 index 000000000..49b858127 --- /dev/null +++ b/apps/limelight/README.md @@ -0,0 +1,19 @@ +# Limelight + *Simple configurable analogue clock based on the work of @Andreas_Rozek [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock)* + +![](screenshot_limelight.png) + +* Selection of different fonts +* Settings menu where you can select font, or switch to Vector font and try a range of sizes +* Reduction by 100 lines of code, demonstrating that there is no need for a custom widget draw method +* Full screen option (widgets are loaded but not displayed) + +![](screenshot_gochihand.png) +![](screenshot_monoton.png) +![](screenshot_grenadier.png) + +Many thanks for @Andreas_Rozek for his pioneering work on building an analogue clock toolkit for the Bangle 2. + +Limelight Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS +Forum](http://forum.espruino.com/microcosms/1424/) + diff --git a/apps/limelight/limelight.app.js b/apps/limelight/limelight.app.js new file mode 100644 index 000000000..20d79deeb --- /dev/null +++ b/apps/limelight/limelight.app.js @@ -0,0 +1,263 @@ +/* + * Limelight analoguce clock with bolted hands + * Based on the work of @Andreas_Rozek + * [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock) + * + * . Demonstrates simpler approach to establishing the available size of the appRect in relation + * to widgets, avoids having to take on the responsibility for managing the widget draw. + * . Demonstrates a settings menu and various configuration options + * . Demonstrates fullscreen verses, widgets and app area. + * + */ + +g.clear(); + +const SETTINGS_FILE = "limelight.json"; +var UPDATE_PERIOD; +var drawTimeout; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; + settings.secondhand = settings.secondhand||false; + settings.font = settings.font||"Limelight"; + settings.vector = settings.vector||false; + settings.fullscreen = settings.fullscreen||false; + settings.vector_size = settings.vector_size||42; + UPDATE_PERIOD = (settings.secondhand ? 1000 : 60000); +} + +loadSettings(); + +// if we are not full screen then load and draw the widgets so that Bangle.appRect gets set +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +// fonts.google.com +Graphics.prototype.setFontLimelight = function(scale) { + // Actual height 28 (28 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAeAAAAAD8AAAAAf4AAAAB/gAAAAH+AAAAAf4AAAAB/gAAAAD8AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAPwAAAAH8AAAAD+AAAAD+AAAAB/AAAAA/gAAAAfwAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAA/gAAAA//wAAAP//wAAB///wAAP///gAA///+AAH///8AAf///4AD////gAP///+AA////4AD////gAMAAAGAAwAAAYADAAABgAMAAAGAAwAAAwABgAADAAHAAAYAAOAADgAAeAA8AAAfh/AAAAf/wAAAAHgAAAAAAAAAAGAAAAAAYAAAAABAAAAAAMAAAAAAwAAAAAD///+AAf///4AB////gAH///+AAf///4AD////gAP///+AA////4AH////gAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgABAAAeAAOAAH4AAwAA/gAGAAP+AAYAB/4ADAAf/gAMAD/+AAwAf/4ADAH//gAMA//+AAwH//4ADB//9gAOP//GAA///wYAD//+BgAH//gGAAf/8AYAA//ABgAB/4AGAAD+AAYAADAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAHAAAwAAGAAHAAAMAAYAAAwADAEABgAMAwAGAAwDAAYADAMABgAMAwAGAAwDAAYAD////gAP///+AA////4AD////gAP///8AAf///wAB/7//AAD/H/4AAP4f/AAAPA/4AAAAA+AAAAAAAAAAAAAAAAAAAGAAAAAB8AAAAAPwAAAADzAAAAAcMAAAAHgwAAAA8DAAAAHAMAAAB4AwAAAOADAAADwAMAAAcAAwAAD///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD////gAP///+AAAAAMAAAAAAwAAAAADAAAAAAcAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAHAAAH4AOAAP/gAcAA+GAAwADAwABgAMDAAGAAwMAAYADAwABgAMDAAGAAwMAAYADA///gAMD//+AAwP//4ADA///AAMB//8AAwH//wADAP/+AAIAf/wAAAA/+AAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAD/8AAAA//8AAAH//4AAB///wAAH///gAA////AAH///8AAf///4AD////gAP///+AAwAAAYADAYABgAMBgAGAAwGAAYADAYABgAMBgAGAAwGAAwABgYADAAHAwAYAAMDgHAAAAHh4AAAAP/AAAAAHgAAAAAAAAAAAAAAAA+AAAAAD4AAAAAMAAAAAAwAAAAADAAAAAAMAA/+AAwB//4ADB///gAM///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD//+AAAP/wAAAA/wAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAA/g/wAAH/GDgAA/+wGAAD//AMAAf/4AwAB//gBgAP//AGAAx/+AYADD/4BgAMH/wGAAwP/AYACA/+BgAMB/8GAAwD/wYADAP/jgAMBf/+AAYH//wABgz//AADHH/4AAH4f/gAAOA/8AAAAB/AAAAAAwAAAAAAAAAAAAAAAAAeAAAAAH/AAAAB8eAAAAGAcBgAAwAwHAAGABgMAAYAGAYADAAIBgAMAAwGAAwADAYADAAYBgAMAAgGAAwAAAYAD////gAP///+AA////wAB////AAH///4AAP///AAA///8AAA///AAAB//4AAAB/+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAeAAAD8D8AAAf4f4AAB/h/gAAH+H+AAAf4f4AAB/h/gAAD8D8AAAHgHgAAAAAAAAAAAAAAA="), 46, atob("DQ0aExgZHRkbGBsbDQ=="), 40+(scale<<8)+(1<<16)); +} + +// fonts.google.com +Graphics.prototype.setFontGochiHand = function(scale) { + // Actual height 29 (31 - 3) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAB4AAAAAD4AAAAAB4AAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAH/gAAAD//gAAD///gAD///+AAH///AAAH//gAAAH/wAAAAHwAAAAAAAAwAAAAAP+AAAAA//gAAAB//wAAAD//4AAAD8P4AAAHwD8AAAHgB8AAAPgA8AAAPAA8AAAPAA8AAAPAA8AAAPgA8AAAPgA8AAAPgB8AAAHwB4AAAH4D4AAAD+PwAAAD//gAAAB//gAAAA/+AAAAAP8AAAAAAAAAAAAAAAAAAAcAAAAAA8AAAAAB8AAAAAD4AAAAAD4AAAAAHwAAAAAHgAAAAAPgAAAAAPgAAAAAf/AAAAAf//wAAAP//wAAAH//wAAAAf/wAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAA8AAAeAB8AAA+AD8AAB+AH8AAB4AP8AAD4AP8AADwAf8AADwB+8AAD4D88AAD4H48AAD//w+AAB//g+AAB//A+AAA/8A+AAAPwA+AAAAAA+AAAAAAcAAAAAAIAAAAAAAAAA8AAAAAB8AAAAAB8AAAAAB4AHgAAD4AHwAAD4AH4AADwPH8AADwfB8AADwfA8AAD4fA+AAD4fA+AAB//A+AAB//A+AAA//A8AAA//x8AAAPP/8AAAAH/4AAAAD/wAAAAB/gAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAH8AAAAAP+AAAAA/+AAAAB/+AAAAD8+AAAAP4+AAAB/weAAAD/weAAAD/8eAAAB///AAAA///AAAAH//8AAAAf//AAAAD//AAAAAf/AAAAAf+AAAAAfAAAAAAMAAAAAAAAAAAAAAAAAAPA/gAAAfh/wAAA/x/4AAA/x/4AAB/4/8AAB74B8AAB58A8AAB58A+AAB5+A+AAB4+A+AAB4+A+AAB4fA+AAB4fg+AAB4Pg8AAB4P58AAB4H/4AAB4D/4AAB4D/wAAAQA/gAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAB/+AAAAD//gAAAH//gAAAPwfwAAAPgf4AAAfAf4AAAeA/8AAAeA98AAAeA88AAAfB48AAAfB48AAAPB48AAAOB48AAAAB98AAAAB/8AAAAA/4AAAAA/wAAAAAfgAAA8AAAAAA8BAAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA+HgAAAA+HgAAAA+HgAAAAfHgAAAAf//+AAAf//+AAAP//+AAAH//8AAAAPwAAAAAHgAAAAAHwAAAAAHwAAAAAHwAAAAADwAAAAADgAAAAAAAAAAAAAAAAAAAB/AAAAP3/wAAAf//wAAA///4AAA//D8AAB9+B8AAB4+A8AAB4+A+AAB4+A+AAB4+A+AAB8+A+AAB8+A+AAA/+A8AAA//A8AAAf/x8AAAP//4AAAH//wAAAAD/gAAAAB/AAAAAAAAAAAAAAAAAAD/AAAAAD/gAAAAH/gAAAAP/wAAAAPHwAAAAPDwAAAAeDwAAAAeDwAAAAeDwAAAAeHwAAAAeHgAAAAePgAAAAefAAAAAf/AAAAAf///gAAf///wAAP///wAAP///gAAH8AAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BwAAAA8B4AAAA+D4AAAA8B4AAAAcB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), 46, atob("DQoYERUWFBYVFhcVDQ=="), 42+(scale<<8)+(1<<16)); +} + +// free for commercial use +// https://www.1001fonts.com/search.html?search=Grenadier+NF +Graphics.prototype.setFontGrenadierNF = function(scale) { + // Actual height 39 (39 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAB4AAAAAAPAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAEAAAAAAPgAAAAA/8AAAAB//gAAAD//4AAAP//wAAAf//AAAB//+AAAD//8AAAB//wAAAAP/gAAAAB+AAAAAAMAAAAAAAAAAAAAAAAB4AAAAAD/8AAAAB//4AAAA///wAAAP8D/AAAD8AD8AAA/AAPwAAPgAAfAAD4AAB8AAeAAAHgAHwAAA+AA8AAADwAHgAAAeAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APgAAAfAA8AAADwAHwAAA+AAeAAAHgAD4AAB8AAPgAAfAAA+AAHwAAD8AD8AAAP8D/AAAA///wAAAD//8AAAAH/+AAAAAD8AAAAAAAAAAAAAAAAAAABAAAAAAAcAAAAAAHwAAAAAB8AAAAAAfgAAAAAH/////AB/////4AP/////AB/////4AAAAAAAAAAAAAAAAAAAAADAAAAAAA4APAAAAPAB4AAAD4APAAAA/AB4AAAP4APAAAD/AB8AAA/4AHgAAPvAA8AAD54ADwAB+PAAfAAfh4AB8AH4PAAPwD+B4AA///gPAAD//wB4AAH/4APAAAP8AAAAAAAAAAAAAAAAAAAPAAAAHAB4AAAB4APAAAAPAB4PAAB4APB4AAPAB+/gAB4AH/8AAfAAf/wADwAB/fAA+AABD8APgAAAPwH4AAAA//+AAAAD//gAAAAH/4AAAAAP8AAAAAAAAAAAAAAAAAAAAAAYAAAAAAPAAAAAAH4AAAAAD/AAAAAB/4AAAAA//AAAAAf94AAAAH+PAAAAD/B4AAAB/gPAAAA/wB4AAAf8APAAAP////4AH/////AD/////4AAAAAPAAAAAAA4AAAAAAAAAAAAAAAAAAAIAAPAAAPAAB4AAf4AAPAA//gAB4AP/8AAPAB/3gAB4APg8AAfAB4DwADwAPAfAA+AB4B8APgAPAP4H4AB4A//+AAPAB//gAB4AH/4AAAAAP8AAAAAAAAAAAAAB4AAAAAD/4AAAAA//wAAAAf//AAAAP+H8AAAH+AHwAAB/gAfAAA/4AB4AAf+AAPAAP/wAA8AH+eAAHgB/ngAA8APw8AAHgB4HgAA8AMA8AAHgAADwAA8AAAeAAPgAAD4AB4AAAPgAfAAAA+AHwAAAH4D8AAAAf//AAAAB//wAAAAD/8AAAAAH8AAAAAAAAAAAAAAAAAAAAAAAYAAAAAAfAB4AAAP4APAAAH/AB4AAH/gAPAAD/wAB4AD/wAAPAB/4AAB4A/8AAAPA/8AAAB4f+AAAAPP+AAAAB//AAAAAP/gAAAAB/gAAAAAPwAAAAAB4AAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAB//AAAAAf/+AAAAH//4AAAB+AfgAA+fAB8AAP/wAHwAD/+AAeAA//gAB4APj8AAPAB4PAAB4APB4AAPAB4PAAB4APB4AAPAB8fgAB4AH/8AAPAAf/wADwAB/eAA+AADj4APgAAAPwD8AAAA///AAAAD//wAAAAP/4AAAAAf8AAAAAAAAAAAB4AAAAAB/4AAAAA//wAAAAP//AAAAD+H8AAAA/AHwAAAHgAfAAAB8AB4AAAPAAHgAADwAA8AAAeAAHgBADwAAeA4AeAADwfADwAAeP4AeAAD3+ADwAA9/gAfAAH/wAB4AB/4AAPgAf8AAA+AH+AAAD8B/gAAAP//wAAAA//4AAAAD/8AAAAAD+AAAAAAAAAAAAAAAAAAAAAAAeB4AAAADwPAAAAAeB4AAAAAAAAAAAAAAAA=="), 46, atob("Bg4kChURExEaFBoaBg=="), 45+(scale<<8)+(1<<16)); +} + +// fonts.google.com +Graphics.prototype.setFontMonoton = function(scale) { + // Actual height 38 (37 - 0) + g.setFontCustom(atob("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"), 46, atob("ChIiERcYGRwfGSAfCw=="), 40+(scale<<8)+(1<<16)); +} + +/* + * If only 1 widget is loaded at the top, then Bangle.appRect changes + * to report as if widgets were loaded at the bottom as well. The + * other option would be for Bangle.appRect to adjust for different + * combinations EG: no widgets, wigets on top, widgets on bottom and + * widgets on top and bottom areas, but it does not at present. + * + * Example of Bangle.appRect with 3 widges on the top, note h = 152, not 176 + * ={ x: 0, y: 24, w: 176, h: 152, x2: 175, y2: 175 } + * + * With the example below we are going assume that the bottom widget + * space is not used. + * + */ +const CenterX = g.getWidth()/2; +const CenterY = (g.getHeight()/2) + (Bangle.appRect.y/2); +const outerRadius = (g.getHeight() - Bangle.appRect.y)/2; + +if (settings.fullscreen) { + Bangle.loadWidgets(); + /* + * We load the widgets as some like widpedom accumualte the step count. + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * widgets area to the top bar doesn't get cleared. + */ + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +function debug(o) { + //console.log(o); +} + +debug("limelight.app.js"); +debug("CenterX=" + CenterX); +debug("CenterY=" + CenterY); +debug("outerRadius=" + outerRadius); +debug("y12=" + (CenterY - outerRadius)); +debug("y6=" + (CenterY + outerRadius)); + +let HourHandLength = outerRadius * 0.5; +let HourHandWidth = 2*5, halfHourHandWidth = HourHandWidth/2; + +let MinuteHandLength = outerRadius * 0.7; +let MinuteHandWidth = 2*3, halfMinuteHandWidth = MinuteHandWidth/2; + +let SecondHandLength = outerRadius * 0.9; +let SecondHandOffset = halfHourHandWidth + 10; + +let outerBoltRadius = halfHourHandWidth + 2, innerBoltRadius = outerBoltRadius - 4; +let HandOffset = outerBoltRadius + 4; + +let twoPi = 2*Math.PI, deg2rad = Math.PI/180; +let Pi = Math.PI; +let halfPi = Math.PI/2; + +let sin = Math.sin, cos = Math.cos; + +let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1]; + +let HandPolygon = [ + -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3], + sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0], + sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3], + -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0], +]; + +let HourHandPolygon = new Array(HandPolygon.length); +for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + HourHandPolygon[i] = halfHourHandWidth*HandPolygon[i]; + HourHandPolygon[i+1] = halfHourHandWidth*HandPolygon[i+1]; + if (i < l/2) { HourHandPolygon[i+1] -= HourHandLength; } + if (i > l/2) { HourHandPolygon[i+1] += HandOffset; } +} +let MinuteHandPolygon = new Array(HandPolygon.length); +for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + MinuteHandPolygon[i] = halfMinuteHandWidth*HandPolygon[i]; + MinuteHandPolygon[i+1] = halfMinuteHandWidth*HandPolygon[i+1]; + if (i < l/2) { MinuteHandPolygon[i+1] -= MinuteHandLength; } + if (i > l/2) { MinuteHandPolygon[i+1] += HandOffset; } +} + +/**** transforme polygon ****/ + +let transformedPolygon = new Array(HandPolygon.length); + +function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } +} + +/**** draw clock hands ****/ + +function drawClockHands () { + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + let Seconds = now.getSeconds(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + g.setColor(g.theme.fg); + + transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + if (settings.secondhand) { + g.setColor(g.theme.fg2); + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + } + + g.setColor(g.theme.fg); + g.fillCircle(CenterX,CenterY, outerBoltRadius); + + g.setColor(g.theme.bg); + g.drawCircle(CenterX,CenterY, outerBoltRadius); + g.fillCircle(CenterX,CenterY, innerBoltRadius); +} + +function setNumbersFont() { + if (settings.vector) { + g.setFont('Vector', settings.vector_size); + return; + } + + if (settings.font == "GochiHand") + g.setFontGochiHand(); + else if (settings.font == "Grenadier") + g.setFontGrenadierNF(); + else if (settings.font == "Monoton") + g.setFontMonoton(); + else + g.setFontLimelight(); +} + +function drawNumbers() { + g.setColor(g.theme.fg); + setNumbersFont(); + + g.setFontAlign(0,-1); + g.drawString('12', CenterX, CenterY - outerRadius); + + g.setFontAlign(1,0); + g.drawString('3', CenterX + outerRadius, CenterY); + + g.setFontAlign(0,1); + g.drawString('6', CenterX, CenterY + outerRadius); + + g.setFontAlign(-1,0); + g.drawString('9', CenterX - outerRadius,CenterY); +} + +function draw() { + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + drawClockHands(); + drawNumbers(); + queueDraw(); +} + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, UPDATE_PERIOD - (Date.now() % UPDATE_PERIOD)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI('clock'); +draw(); diff --git a/apps/limelight/limelight.icon.js b/apps/limelight/limelight.icon.js new file mode 100644 index 000000000..f7e74db90 --- /dev/null +++ b/apps/limelight/limelight.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lksgIqngf/wAFC//+AgUch/4AgMBwAQEh/8Dgf/4AKOEAQKCAYUB//gAoU/DQkPBQYVBGx5SDBQIbDBR0GEAlgFYcHGwh4B+CDHRwL04")) \ No newline at end of file diff --git a/apps/limelight/limelight.png b/apps/limelight/limelight.png new file mode 100644 index 000000000..b1744b28e Binary files /dev/null and b/apps/limelight/limelight.png differ diff --git a/apps/limelight/limelight.settings.js b/apps/limelight/limelight.settings.js new file mode 100644 index 000000000..aacea2f86 --- /dev/null +++ b/apps/limelight/limelight.settings.js @@ -0,0 +1,78 @@ +(function(back) { + const SETTINGS_FILE = "limelight.json"; + + // initialize with default settings... + let s = { + 'vector_size': 42, + 'vector': false, + 'font': "Limelight", + 'secondhand': false, + 'fullscreen': false + } + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = storage.readJSON(SETTINGS_FILE, 1) || {} + const saved = settings || {} + + // copy settings into variable + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var font_options = ["Limelight","GochiHand","Grenadier","Monoton"]; + + E.showMenu({ + '': { 'title': 'Limelight Clock' }, + '< Back': back, + 'Full Screen': { + value: s.fullscreen, + format: () => (s.fullscreen ? 'Yes' : 'No'), + onchange: () => { + s.fullscreen = !s.fullscreen; + save(); + }, + }, + 'Font': { + value: 0 | font_options.indexOf(s.font), + min: 0, max: 3, + format: v => font_options[v], + onchange: v => { + s.font = font_options[v]; + save(); + }, + }, + 'Vector Font': { + value: s.vector, + format: () => (s.vector ? 'Yes' : 'No'), + onchange: () => { + s.vector = !s.vector; + save(); + }, + }, + 'Vector Size': { + value: s.vector_size, + min: 24, + max: 56, + step: 6, + onchange: v => { + s.vector_size = v; + save(); + } + }, + 'Second Hand': { + value: s.secondhand, + format: () => (s.secondhand ? 'Yes' : 'No'), + onchange: () => { + s.secondhand = !s.secondhand; + save(); + }, + } + }); +}) diff --git a/apps/limelight/metadata.json b/apps/limelight/metadata.json new file mode 100644 index 000000000..7c3736e1a --- /dev/null +++ b/apps/limelight/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "limelight", + "name": "Limelight", + "version": "0.01", + "description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)", + "icon": "limelight.png", + "readme":"README.md", + "screenshots": [{"url":"screenshot_limelight.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"limelight.app.js","url":"limelight.app.js"}, + {"name":"limelight.settings.js","url":"limelight.settings.js"}, + {"name":"limelight.img","url":"limelight.icon.js","evaluate":true} + ] +} diff --git a/apps/limelight/screenshot_gochihand.png b/apps/limelight/screenshot_gochihand.png new file mode 100644 index 000000000..244b008dc Binary files /dev/null and b/apps/limelight/screenshot_gochihand.png differ diff --git a/apps/limelight/screenshot_grenadier.png b/apps/limelight/screenshot_grenadier.png new file mode 100644 index 000000000..a55896297 Binary files /dev/null and b/apps/limelight/screenshot_grenadier.png differ diff --git a/apps/limelight/screenshot_limelight.png b/apps/limelight/screenshot_limelight.png new file mode 100644 index 000000000..7b12e4cc2 Binary files /dev/null and b/apps/limelight/screenshot_limelight.png differ diff --git a/apps/limelight/screenshot_monoton.png b/apps/limelight/screenshot_monoton.png new file mode 100644 index 000000000..e75b11f5d Binary files /dev/null and b/apps/limelight/screenshot_monoton.png differ diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 288dc6dde..39b825e02 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -10,3 +10,9 @@ 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 +0.15: Fixed sv_SE formatting, long date does not work well for Bangle.js2 + Added Swedish localisation with English text \ No newline at end of file diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 9e2624b77..428e0c773 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: @@ -61,7 +80,7 @@ timePattern / datePattern: %S second (00..60) %p locale's equivalent of either AM or PM; blank if not known %P like %p, but lower case -*/ +*/ var locales = { "en_GB": { // this is default @@ -130,12 +149,13 @@ 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", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus", "< Back": "< Zurück" } + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus", + "< Back": "< Zurück", "Delete": "Löschen", "Mark Unread": "Als ungelesen markieren" } }, "en_US": { lang: "en_US", @@ -184,12 +204,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", @@ -238,13 +276,31 @@ var locales = { temperature: "°C", ampm: { 0: "fm", 1: "em" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, - datePattern: { 0: "%A %B %d %Y", "1": "%Y-%m-%d" }, // söndag 1 mars 2020 // 2020-03-01 + datePattern: { 0: "%b %d %Y", "1": "%Y-%m-%d" }, // feb 1 2020 // 2020-03-01 abmonth: "jan,feb,mars,apr,maj,juni,juli,aug,sep,okt,nov,dec", month: "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december", abday: "sön,mån,tis,ons,tors,fre,lör", day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag", trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "on", off: "off" } }, + "en_SE": { // Swedish localisation with English text + lang: "en_SE", + decimal_point: ",", + thousands_sep: ".", + currency_symbol: "kr", + int_curr_symbol: "SKR", + speed: 'kmh', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%B %d %Y", "1": "%Y-%m-%d" }, // March 1 2020 // 2020-03-01 + abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + month: "January,February,March,April,May,June,July,August,September,October,November,December", + abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", + day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", + // No translation for english... + }, "en_NZ": { lang: "en_NZ", decimal_point: ".", @@ -290,13 +346,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, 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.03.20 abmonth: "Jän,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Jänner,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" } + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus", + "< Back": "< Zurück", "Delete": "Löschen", "Mark Unread": "Als ungelesen markieren" } }, "en_IL": { lang: "en_IL", @@ -332,7 +390,8 @@ var locales = { month: "enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre", abday: "dom,lun,mar,mié,jue,vie,sáb", day: "domingo,lunes,martes,miércoles,jueves,viernes,sábado", - trans: { yes: "sí", Yes: "Sí", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + trans: { yes: "sí", Yes: "Sí", no: "no", No: "No", ok: "ok", on: "on", off: "off", + "< Back": "< Atrás", "Delete": "Borrar ", "Mark Unread": "Marcar como no leído" } }, "fr_BE": { lang: "fr_BE", @@ -416,12 +475,12 @@ var locales = { distance: { "0": "m", "1": "km" }, temperature: '°C', ampm: { 0: "", 1: "" }, - timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00 - datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020 + timePattern: { 0: "%HH:%MM.%SS ", 1: "%HH:%MM" }, // 17:00.00 // 17:00 + datePattern: { 0: "%d %b %Y", "1": "%d/%m/%Y" }, // 1 marzo 2020 // 01/03/2020 abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic", month: "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre", abday: "dom,lun,mar,mer,gio,ven,sab", - day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì, sabato", + day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì,sabato", trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" } }, "it_IT": { @@ -434,12 +493,12 @@ var locales = { distance: { "0": "m", "1": "km" }, temperature: '°C', ampm: { 0: "", 1: "" }, - timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00 - datePattern: { 0: "%A %B %d %Y", "1": "%d/%m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020 + timePattern: { 0: "%HH:%MM.%SS ", 1: "%HH:%MM" }, // 17:00.00 // 17:00 + datePattern: { 0: "%d %b %Y", "1": "%d/%m/%Y" }, // 1 marzo 2020 // 01/03/2020 abmonth: "gen,feb,mar,apr,mag,giu,lug,ago,set,ott,nov,dic", month: "gennaio,febbraio,marzo,aprile,maggio,giugno,luglio,agosto,settembre,ottobre,novembre,dicembre", abday: "dom,lun,mar,mer,gio,ven,sab", - day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì, sabato", + day: "domenica,lunedì,martedì,mercoledì,giovedì,venerdì,sabato", trans: { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" } }, "wae_CH": { @@ -548,7 +607,7 @@ var locales = { month: "leden,únor,březen,duben,květen,červen,červenec,srpen,září,říjen,listopad,prosinec", abday: "ne,po,út,st,čt,pá,so", day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota", - trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "na", off: "poza" } + trans: { yes: "ano", Yes: "Ano", no: "ne", No: "Ne", ok: "ok", on: "zap", off: "vyp" } }, "sl_SI": { lang: "sl_SI", @@ -586,6 +645,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/locale/metadata.json b/apps/locale/metadata.json new file mode 100644 index 000000000..c8908c7a7 --- /dev/null +++ b/apps/locale/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "locale", + "name": "Languages", + "version": "0.15", + "description": "Translations for different countries", + "icon": "locale.png", + "type": "locale", + "tags": "tool,system,locale,translate", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "locale.html", + "storage": [ + {"name":"locale"} + ], + "sortorder": -10 +} diff --git a/apps/ltherm/README.md b/apps/ltherm/README.md new file mode 100644 index 000000000..b68cb1fc1 --- /dev/null +++ b/apps/ltherm/README.md @@ -0,0 +1,3 @@ +# Thermometer + +Localized Bangle.js 2 thermometer app. It also starts maintaining an average of the temperature to help lower the margin of error after 10 consecutive readings; due to the low quality die-thermometer. diff --git a/apps/ltherm/app.js b/apps/ltherm/app.js new file mode 100644 index 000000000..7accae2ed --- /dev/null +++ b/apps/ltherm/app.js @@ -0,0 +1,25 @@ +function drawTemperature() { + g.reset(1).clearRect(0,24,g.getWidth(),g.getHeight()); + g.setFont("6x8",2).setFontAlign(0,0); + var x = g.getWidth()/2; + var y = g.getHeight()/2 + 10; + g.drawString("Temp", x, y - 45); + g.setFontVector(70).setFontAlign(0,0); + var h = E.getTemperature(); + if (avg.length < 10) { + avg[avg.length] = h; + } else { + avg.shift(); + avg[avg.length] = h; + h = ((avg[0] + avg[1] + avg[2] + avg[3] + avg[4] + avg[5] + avg[6] + avg[7] + avg[8] + avg[9]) / 10); + } + var t = require('locale').temp(h); + g.drawString(t, x, y); +} +const avg = []; +setInterval(function() { + drawTemperature(); +}, 2000); +E.showMessage("Loading..."); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/ltherm/icon.js b/apps/ltherm/icon.js new file mode 100644 index 000000000..4f3cc4b0a --- /dev/null +++ b/apps/ltherm/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AChGIxGAC6eIAQgARFgUIC9ReCAYJgSC7BHDF6gUBC6ovWI/5Hga/6P/ABsCkABDC/4XxkQXDkQuSAQwXPDQkAC6BBCkQDDC6MCmczFoIXQCQQXBDgQXP2EA2YXBncAhYXR3YXB3YXRCQWznYcCC6ICBAYYXPhYrBApAwPFyQqCIoYuRLwZgDAH4A/")) diff --git a/apps/ltherm/metadata.json b/apps/ltherm/metadata.json new file mode 100644 index 000000000..83b295a3d --- /dev/null +++ b/apps/ltherm/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "ltherm", + "name": "Localized Thermometer", + "shortName": "Thermometer", + "version": "0.01", + "description": "Displays the current temperature in localized units.", + "icon": "thermf.png", + "tags": "tool", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"ltherm.app.js","url":"app.js"}, + {"name":"ltherm.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/ltherm/thermf.png b/apps/ltherm/thermf.png new file mode 100644 index 000000000..bb33cb939 Binary files /dev/null and b/apps/ltherm/thermf.png differ diff --git a/apps/magnav/metadata.json b/apps/magnav/metadata.json new file mode 100644 index 000000000..cba9a1ac3 --- /dev/null +++ b/apps/magnav/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "magnav", + "name": "Navigation Compass", + "version": "0.05", + "description": "Compass with linear display as for GPSNAV. Has Tilt compensation and remembers calibration.", + "screenshots": [{"url":"screenshot-b2.png"},{"url":"screenshot-light-b2.png"}], + "icon": "magnav.png", + "tags": "tool,outdoors", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"magnav.app.js","url":"magnav_b1.js","supports":["BANGLEJS"]}, + {"name":"magnav.app.js","url":"magnav_b2.js","supports":["BANGLEJS2"]}, + {"name":"magnav.img","url":"magnav-icon.js","evaluate":true} + ], + "data": [{"name":"magnav.json"}] +} diff --git a/apps/mandel/metadata.json b/apps/mandel/metadata.json new file mode 100644 index 000000000..da616a38d --- /dev/null +++ b/apps/mandel/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mandel", + "name": "Mandelbrot", + "shortName": "Mandel", + "version": "0.01", + "description": "Draw a zoomable Mandelbrot set", + "icon": "mandel.png", + "tags": "game", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"mandel.app.js","url":"mandel.min.js"}, + {"name":"mandel.img","url":"mandel-icon.js","evaluate":true} + ] +} 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/mandelbrotclock/metadata.json b/apps/mandelbrotclock/metadata.json new file mode 100644 index 000000000..852855184 --- /dev/null +++ b/apps/mandelbrotclock/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "mandelbrotclock", + "name": "Mandelbrot Clock", + "version": "0.01", + "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": "mandelbrotclock.app.js", "url": "mandelbrotclock.js" }, + { + "name": "mandelbrotclock.img", + "url": "mandelbrotclock-icon.js", + "evaluate": true + } + ] +} 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/marioclock/metadata.json b/apps/marioclock/metadata.json new file mode 100644 index 000000000..a0282405e --- /dev/null +++ b/apps/marioclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "marioclock", + "name": "Mario Clock", + "version": "0.15", + "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", + "icon": "marioclock.png", + "type": "clock", + "tags": "clock,mario,retro", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": false, + "screenshots": [{"url":"bangle1-mario-clock-screenshot.png"}], + "storage": [ + {"name":"marioclock.app.js","url":"marioclock-app.js"}, + {"name":"marioclock.img","url":"marioclock-icon.js","evaluate":true} + ] +} diff --git a/apps/matrixclock/metadata.json b/apps/matrixclock/metadata.json new file mode 100644 index 000000000..c4a72988a --- /dev/null +++ b/apps/matrixclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "matrixclock", + "name": "Matrix Clock", + "version": "0.02", + "description": "inspired by The Matrix, a clock of the same style", + "icon": "matrixclock.png", + "screenshots": [{"url":"screenshot_matrix.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"matrixclock.app.js","url":"matrixclock.js"}, + {"name":"matrixclock.img","url":"matrixclock-icon.js","evaluate":true} + ] +} diff --git a/apps/mclock/metadata.json b/apps/mclock/metadata.json new file mode 100644 index 000000000..513f823a1 --- /dev/null +++ b/apps/mclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "mclock", + "name": "Morphing Clock", + "version": "0.07", + "description": "7 segment clock that morphs between minutes and hours", + "icon": "clock-morphing.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-morphing-clock-screenshot.png"}], + "storage": [ + {"name":"mclock.app.js","url":"clock-morphing.js"}, + {"name":"mclock.img","url":"clock-morphing-icon.js","evaluate":true} + ], + "sortorder": -9 +} diff --git a/apps/mclockplus/ChangeLog b/apps/mclockplus/ChangeLog index a1cecc698..097545ba8 100644 --- a/apps/mclockplus/ChangeLog +++ b/apps/mclockplus/ChangeLog @@ -1,2 +1,3 @@ 0.01: Created app 0.02: Use Bangle.setUI for button/launcher handling +0.03: Allow widgets to detect this is a clock diff --git a/apps/mclockplus/mclockplus.app.js b/apps/mclockplus/mclockplus.app.js index 4c74ce1be..970397478 100644 --- a/apps/mclockplus/mclockplus.app.js +++ b/apps/mclockplus/mclockplus.app.js @@ -304,15 +304,14 @@ Bangle.on('lcdPower',function(on) { }); g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); // Update time once a second timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); - // Start stopwatch when BTN3 is pressed setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"}); B3 = 1; // BTN3 is bound to start the stopwatch diff --git a/apps/mclockplus/metadata.json b/apps/mclockplus/metadata.json new file mode 100644 index 000000000..49cb33f52 --- /dev/null +++ b/apps/mclockplus/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "mclockplus", + "name": "Morph Clock+", + "shortName": "Morph Clock+", + "version": "0.03", + "description": "Morphing Clock with more readable seconds and date and additional stopwatch", + "icon": "mclockplus.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"mclockplus.app.js","url":"mclockplus.app.js"}, + {"name":"mclockplus.img","url":"mclockplus-icon.js","evaluate":true} + ] +} diff --git a/apps/menusmall/metadata.json b/apps/menusmall/metadata.json new file mode 100644 index 000000000..aafb7da28 --- /dev/null +++ b/apps/menusmall/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "menusmall", + "name": "Small Menus", + "version": "0.02", + "description": "Replace Bangle.js 2's menus with a version that contains smaller text", + "icon": "app.png", + "type": "boot", + "tags": "system", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"menusmall.boot.js","url":"boot.js"} + ] +} diff --git a/apps/menuwheel/ChangeLog b/apps/menuwheel/ChangeLog new file mode 100644 index 000000000..050cf2049 --- /dev/null +++ b/apps/menuwheel/ChangeLog @@ -0,0 +1,2 @@ +0.01: New menu! +0.02: Clean up touch handler in setUI 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..deb15264d --- /dev/null +++ b/apps/menuwheel/boot.js @@ -0,0 +1,215 @@ +E.showMenu = function(items) { + g.clearRect(Bangle.appRect); // clear screen if no menu supplied + 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(); + } + } + Bangle.on('touch', Bangle.backHandler); + } + return l; +}; +// setUI now also needs to clear up our back button touch handler +Bangle.setUI = (old => function() { + if (Bangle.backHandler) Bangle.removeListener("touch", Bangle.backHandler); + delete Bangle.backHandler; + return old.apply(this, arguments); +})(Bangle.setUI); \ No newline at end of file diff --git a/apps/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/metadata.json b/apps/menuwheel/metadata.json new file mode 100644 index 000000000..1ad042344 --- /dev/null +++ b/apps/menuwheel/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "menuwheel", + "name": "Wheel Menus", + "version": "0.02", + "description": "Replace Bangle.js 2's menus with a version that contains variable-size text and a back button", + "readme": "README.md", + "icon": "icon.png", + "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"} + ] +} 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..522534af0 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -8,3 +8,23 @@ 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 +0.14: Hide widget when all unread notifications are dismissed from phone +0.15: Don't buzz when Quiet Mode is active +0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147) +0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font +0.18: Use app-specific icon colors + Spread message action buttons out + Back button now goes back to list of messages + If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267) diff --git a/apps/messages/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..3e692a0cc 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 && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { 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,17 +80,52 @@ 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("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); + if (s=="gmail") return getNotificationImage(); + if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); + if (s=="mail") return getNotificationImage(); + 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 getMessageImageCol(msg,def) { + return { + // generic colors, using B2-safe colors + "calendar": "#f00", + "mail": "#ff0", + "music": "#f0f", + "phone": "#0f0", + "sms message": "#0ff", + // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) + // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) + "facebook": "#4267b2", + "gmail": "#ea4335", + "google home": "#fbbc05", + "hangouts": "#1ba261", + "instagram": "#dd2a7b", + "messenger": "#0078ff", + "outlook mail": "#0072c6", + "skype": "#00aff0", + "slack": "#e51670", + "telegram": "#0088cc", + "twitter": "#1da1f2", + "whatsapp": "#4fce5d", + "wordfeud": "#dcc8bd", + }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); } - function showMapMessage(msg) { var m; @@ -121,7 +168,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 +182,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 +199,92 @@ 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:1,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found + if (msg.src=="Maps") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMapMessage(msg); + } + 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(); - checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:1}); + msg.new = false; saveMessages(); // read mail + cancelReloadTimeout(); // don't auto-reload to clock now + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); }} // back ]; if (msg.positive) { + buttons.push({fillx:1}); buttons.push({type:"btn", src:getPosImage(), cb:()=>{ msg.new = false; saveMessages(); + cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); }}); } if (msg.negative) { + buttons.push({fillx:1}); 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}); }}); } + var bodyFont = fontMedium; + lines = g.setFont(bodyFont).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), col:getMessageImageCol(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||/*LANG*/"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:bodyFont, label:body, fillx:1, filly:1, pad:2 }, {type:"h",fillx:1, c: buttons} ]}); g.clearRect(Bangle.appRect); @@ -229,10 +302,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 +317,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,11 +333,13 @@ 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) { - g.drawImage(img, x+24, r.y+24, {rotate:0}); // force centering + var fg = g.getColor(); + g.setColor(getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering + .setColor(fg); // only color the icon x += 50; } var m = msg.title+"\n"+msg.body; @@ -286,9 +362,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..32dff78ba 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 @@ -26,18 +30,33 @@ exports.pushMessage = function(event) { require("Storage").writeJSON("messages.json",messages); // if in app, process immediately if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); + // if we've removed the last new message, hide the widget + if (event.t=="remove" && !messages.some(m=>m.new)) { + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide(); + } // 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 + var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; + if (!quiet && 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 (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know + if (loadMessages) return load("messages.app.js"); + if (!quiet && (!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/metadata.json b/apps/messages/metadata.json new file mode 100644 index 000000000..901419913 --- /dev/null +++ b/apps/messages/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "messages", + "name": "Messages", + "version": "0.18", + "description": "App to display notifications from iOS and Gadgetbridge", + "icon": "app.png", + "type": "app", + "tags": "tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"messages.app.js","url":"app.js"}, + {"name":"messages.settings.js","url":"settings.js"}, + {"name":"messages.img","url":"app-icon.js","evaluate":true}, + {"name":"messages.wid.js","url":"widget.js"}, + {"name":"messages","url":"lib.js"} + ], + "data": [{"name":"messages.json"},{"name":"messages.settings.json"}], + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}], + "sortorder": -9 +} 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..c865a37fb 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) { @@ -11,12 +12,12 @@ require('Storage').writeJSON("messages.settings.json", settings); } - var vibPatterns = ["Off", ".", "-", "--", "-.-", "---"]; + var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; var currentVib = settings().vibrate; var mainmenu = { - "" : { "title" : "Messages" }, + "" : { "title" : /*LANG*/"Messages" }, "< Back" : back, - 'Vibrate': { + /*LANG*/'Vibrate': { value: Math.max(0,vibPatterns.indexOf(settings().vibrate)), min: 0, max: vibPatterns.length, format: v => vibPatterns[v]||"Off", @@ -24,12 +25,18 @@ updateSetting("vibrate", vibPatterns[v]); } }, - 'Repeat': { + /*LANG*/'Repeat': { value: settings().repeat, min: 2, max: 10, format: v => v+"s", onchange: v => updateSetting("repeat", v) }, + /*LANG*/'Unread timer': { + value: settings().unreadTimeout, + min: 0, max: 240, step : 10, + format: v => v?v+"s":/*LANG*/"Off", + onchange: v => updateSetting("unreadTimeout", v) + }, }; E.showMenu(mainmenu); }) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index 3a22b40fd..1239ef262 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,10 +1,10 @@ - -WIDGETS["messages"]={area:"tl",width:0,draw:function() { +WIDGETS["messages"]={area:"tl", width:0, iconwidth:23, +draw:function() { + Bangle.removeListener('touch', this.touch); if (!this.width) return; var c = (Date.now()-this.t)/1000; - g.reset().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+this.iconwidth); + g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y); //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute let settings = require('Storage').readJSON("messages.settings.json", true) || {}; if (settings.repeat===undefined) settings.repeat = 4; @@ -13,10 +13,12 @@ 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 - WIDGETS["messages"].width=64; + if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing + WIDGETS["messages"].width=this.iconwidth; Bangle.drawWidgets(); Bangle.setLCDPower(1);// turns screen on },hide:function() { @@ -25,6 +27,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { WIDGETS["messages"].width=0; Bangle.drawWidgets(); },buzz:function() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return; // never buzz during Quiet Mode let v = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || "."; function b() { var c = v[0]; @@ -33,4 +36,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+w.iconwidth) 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); +})(); \ No newline at end of file diff --git a/apps/metronome/metadata.json b/apps/metronome/metadata.json new file mode 100644 index 000000000..7f8582ca5 --- /dev/null +++ b/apps/metronome/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "metronome", + "name": "Metronome", + "version": "0.07", + "readme": "README.md", + "description": "Makes the watch blinking and vibrating with a given rate", + "icon": "metronome_icon.png", + "tags": "tool", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-metronome-screenshot.png"}], + "storage": [ + {"name":"metronome.app.js","url":"metronome.js"}, + {"name":"metronome.img","url":"metronome-icon.js","evaluate":true}, + {"name":"metronome.settings.js","url":"settings.js"} + ] +} diff --git a/apps/miclock/metadata.json b/apps/miclock/metadata.json new file mode 100644 index 000000000..6eece46b0 --- /dev/null +++ b/apps/miclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "miclock", + "name": "Mixed Clock", + "version": "0.05", + "description": "A mix of analog and digital Clock", + "icon": "clock-mixed.png", + "type": "clock", + "tags": "clock", + "screenshots": [{"url":"bangle1-mixed-clock-screenshot.png"}], + "supports": ["BANGLEJS"], + "allow_emulator": true, + "storage": [ + {"name":"miclock.app.js","url":"clock-mixed.js"}, + {"name":"miclock.img","url":"clock-mixed-icon.js","evaluate":true} + ] +} diff --git a/apps/miclock2/metadata.json b/apps/miclock2/metadata.json new file mode 100644 index 000000000..dc1b49822 --- /dev/null +++ b/apps/miclock2/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "miclock2", + "name": "Mixed Clock 2", + "version": "0.01", + "description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.", + "icon": "clock-mixed.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-mixed-clock-2-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"miclock2.app.js","url":"clock-mixed.js"}, + {"name":"miclock2.img","url":"clock-mixed-icon.js","evaluate":true} + ] +} diff --git a/apps/minimal_clock/LICENSE b/apps/minimal_clock/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/minimal_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/minimal_clock/README.md b/apps/minimal_clock/README.md new file mode 100644 index 000000000..7e8700c0d --- /dev/null +++ b/apps/minimal_clock/README.md @@ -0,0 +1,15 @@ +# Minimal Analog Clock # + +This app displays the perhaps most basic analog clock one can think of - just +some clock hands and no clock face. It considers the currently configured +"theme" (and may therefore look different than shown in the screenshot on your +watch depending on which theme you prefer). + +![](app-screenshot.png) + +This clock also acts as an example for the building blocks found in the author's +[GitHub repository](https://github.com/rozek/banglejs-2-activities) + +## License ## + +[MIT License](LICENSE) diff --git a/apps/minimal_clock/app-icon.js b/apps/minimal_clock/app-icon.js new file mode 100644 index 000000000..53ffd52d7 --- /dev/null +++ b/apps/minimal_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/AEBhCjgCBgeAgF8AoXggHwCIXwgfADAX8h4TBAAM+jwkDj/4AocPDwIACgdgBYgoCAAMEuB+/AH4=")) \ No newline at end of file diff --git a/apps/minimal_clock/app-icon.png b/apps/minimal_clock/app-icon.png new file mode 100644 index 000000000..69788442a Binary files /dev/null and b/apps/minimal_clock/app-icon.png differ diff --git a/apps/minimal_clock/app-screenshot.png b/apps/minimal_clock/app-screenshot.png new file mode 100644 index 000000000..1ec4901ac Binary files /dev/null and b/apps/minimal_clock/app-screenshot.png differ diff --git a/apps/minimal_clock/app.js b/apps/minimal_clock/app.js new file mode 100644 index 000000000..d78790347 --- /dev/null +++ b/apps/minimal_clock/app.js @@ -0,0 +1,230 @@ + let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; + let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; + + let outerRadius = Math.min(CenterX,CenterY) * 0.9; + + Bangle.loadWidgets(); + +/**** updateClockFaceSize ****/ + + function updateClockFaceSize () { + CenterX = ScreenWidth/2; + CenterY = ScreenHeight/2; + + outerRadius = Math.min(CenterX,CenterY) * 0.9; + + if (global.WIDGETS == null) { return; } + + let WidgetLayouts = { + tl:{ x:0, y:0, Direction:0 }, + tr:{ x:ScreenWidth-1, y:0, Direction:1 }, + bl:{ x:0, y:ScreenHeight-24, Direction:0 }, + br:{ x:ScreenWidth-1, y:ScreenHeight-24, Direction:1 } + }; + + for (let Widget of WIDGETS) { + let WidgetLayout = WidgetLayouts[Widget.area]; // reference, not copy! + if (WidgetLayout == null) { continue; } + + Widget.x = WidgetLayout.x - WidgetLayout.Direction * Widget.width; + Widget.y = WidgetLayout.y; + + WidgetLayout.x += Widget.width * (1-2*WidgetLayout.Direction); + } + + let x,y, dx,dy; + let cx = CenterX, cy = CenterY, r = outerRadius, r2 = r*r; + + x = WidgetLayouts.tl.x; y = WidgetLayouts.tl.y+24; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + x = WidgetLayouts.tr.x; y = WidgetLayouts.tr.y+24; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + x = WidgetLayouts.bl.x; y = WidgetLayouts.bl.y; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + x = WidgetLayouts.br.x; y = WidgetLayouts.br.y; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + CenterX = cx; CenterY = cy; outerRadius = r * 0.9; + } + + updateClockFaceSize(); + +/**** custom version of Bangle.drawWidgets (does not clear the widget areas) ****/ + + Bangle.drawWidgets = function () { + var w = g.getWidth(), h = g.getHeight(); + + var pos = { + tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left + tr:{x:w-1, y:0, r:1, c:0}, + bl:{x:0, y:h-24, r:0, c:0}, + br:{x:w-1, y:h-24, r:1, c:0} + }; + + if (global.WIDGETS) { + for (var wd of WIDGETS) { + var p = pos[wd.area]; + if (!p) continue; + + wd.x = p.x - p.r*wd.width; + wd.y = p.y; + + p.x += wd.width*(1-2*p.r); + p.c++; + } + + g.reset(); // also loads the current theme + + if (pos.tl.c || pos.tr.c) { + g.setClipRect(0,h-24,w-1,h-1); + g.reset(); // also (re)loads the current theme + } + + if (pos.bl.c || pos.br.c) { + g.setClipRect(0,h-24,w-1,h-1); + g.reset(); // also (re)loads the current theme + } + + try { + for (wd of WIDGETS) { + g.clearRect(wd.x,wd.y, wd.x+wd.width-1,23); + wd.draw(wd); + } + } catch (e) { print(e); } + } + }; + + let HourHandLength = outerRadius * 0.5; + let HourHandWidth = 2*5, halfHourHandWidth = HourHandWidth/2; + + let MinuteHandLength = outerRadius * 0.7; + let MinuteHandWidth = 2*3, halfMinuteHandWidth = MinuteHandWidth/2; + + let SecondHandLength = outerRadius * 0.9; + let SecondHandOffset = halfHourHandWidth + 10; + + let outerBoltRadius = halfHourHandWidth + 2, innerBoltRadius = outerBoltRadius - 4; + let HandOffset = outerBoltRadius + 4; + + let twoPi = 2*Math.PI, deg2rad = Math.PI/180; + let Pi = Math.PI; + let halfPi = Math.PI/2; + + let sin = Math.sin, cos = Math.cos; + + let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1]; + + let HandPolygon = [ + -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3], + sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0], + sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3], + -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0], + ]; + + let HourHandPolygon = new Array(HandPolygon.length); + for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + HourHandPolygon[i] = halfHourHandWidth*HandPolygon[i]; + HourHandPolygon[i+1] = halfHourHandWidth*HandPolygon[i+1]; + if (i < l/2) { HourHandPolygon[i+1] -= HourHandLength; } + if (i > l/2) { HourHandPolygon[i+1] += HandOffset; } + } + let MinuteHandPolygon = new Array(HandPolygon.length); + for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + MinuteHandPolygon[i] = halfMinuteHandWidth*HandPolygon[i]; + MinuteHandPolygon[i+1] = halfMinuteHandWidth*HandPolygon[i+1]; + if (i < l/2) { MinuteHandPolygon[i+1] -= MinuteHandLength; } + if (i > l/2) { MinuteHandPolygon[i+1] += HandOffset; } + } + +/**** transforme polygon ****/ + + let transformedPolygon = new Array(HandPolygon.length); + + function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } + } + +/**** draw clock hands ****/ + + function drawClockHands () { + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + let Seconds = now.getSeconds(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + g.setColor(g.theme.fg); + + transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + g.setColor(g.theme.fg2); + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + + g.setColor(g.theme.fg); + g.fillCircle(CenterX,CenterY, outerBoltRadius); + + g.setColor(g.theme.bg); + g.drawCircle(CenterX,CenterY, outerBoltRadius); + g.fillCircle(CenterX,CenterY, innerBoltRadius); + } + +/**** refreshDisplay ****/ + + let Timer; + function refreshDisplay () { + g.clear(true); // also loads current theme + + Bangle.drawWidgets(); + + drawClockHands(); + + let Pause = 1000 - (Date.now() % 1000); + Timer = setTimeout(refreshDisplay,Pause); + } + + setTimeout(refreshDisplay, 500); // enqueue first draw request + + Bangle.on('lcdPower', (on) => { + if (on) { + if (Timer != null) { clearTimeout(Timer); Timer = undefined; } + refreshDisplay(); + } + }); + + Bangle.loadWidgets(); + + Bangle.setUI('clock'); diff --git a/apps/minimal_clock/metadata.json b/apps/minimal_clock/metadata.json new file mode 100644 index 000000000..1702d97a9 --- /dev/null +++ b/apps/minimal_clock/metadata.json @@ -0,0 +1,17 @@ +{ "id": "minimal_clock", + "name": "Minimal Analog Clock", + "shortName":"Minimal Clock", + "version":"0.03", + "description": "a minimal analog clock - just with some hands and no clock face", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"app-screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"minimal_clock.app.js","url":"app.js"}, + {"name":"minimal_clock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/minionclk/metadata.json b/apps/minionclk/metadata.json new file mode 100644 index 000000000..44fc2a82d --- /dev/null +++ b/apps/minionclk/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "minionclk", + "name": "Minion clock", + "version": "0.05", + "description": "Minion themed clock.", + "icon": "minionclk.png", + "type": "clock", + "tags": "clock,minion", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-minion-clock-screenshot.png"}], + "storage": [ + {"name":"minionclk.app.js","url":"app.js"}, + {"name":"minionclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/miplant/metadata.json b/apps/miplant/metadata.json new file mode 100644 index 000000000..a949190c1 --- /dev/null +++ b/apps/miplant/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "miplant", + "name": "Xiaomi Plant Sensor", + "shortName": "Mi Plant", + "version": "0.02", + "description": "Reads and displays data from Xiaomi bluetooth plant moisture sensors", + "icon": "app.png", + "tags": "xiaomi,mi,plant,ble,bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"miplant.app.js","url":"app.js"}, + {"name":"miplant.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog new file mode 100644 index 000000000..939ac3b5d --- /dev/null +++ b/apps/mmind/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/mmind/README.md b/apps/mmind/README.md new file mode 100644 index 000000000..8060b95f6 --- /dev/null +++ b/apps/mmind/README.md @@ -0,0 +1,31 @@ +# Mastermind + +Play the classic mind game mastermind on your Bangle 2. + +![](screenshot_mmind.png) + + +## Game +The game will start when run. +Four colors pins are randomly chosen and kept secret. +You need to find the secret by scoring your choice within 6 turns. +The game makes use of touch features. + + +## Play +Select one of the dots, the color menu will show, select a colour for the pin. +If all pins are chosen with a color the red button will turn green. +Hit the green button and your play will be scored and listed from the top. +The first digit shows the number of pins with the correct color and in the right place. +The second digit gives the number of pins with the correct color but in the wrong place. +There are six turns to get the correct secret. +The blue button will start a new game. + + +## Requests +This is the first version, things to add are: +Add a menu to change game options like the number of colors, allow double colors, 5 pins per row. Add feature to drag screen up and down to see more scores. Timer and high score. +Any other fearures or remarks, let me know @psbest. + +## Creator +This game is created by Peter Slendebroek. diff --git a/apps/mmind/metadata.json b/apps/mmind/metadata.json new file mode 100644 index 000000000..c2ed474b6 --- /dev/null +++ b/apps/mmind/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "mmind", + "name": "Classic Mind Game", + "shortName":"Master Mind", + "icon": "mmind.png", + "version":"0.01", + "description": "This is the classic game for masterminds", + "screenshots": [{"url":"screenshot_mmind.png"}], + "type": "app", + "tags": "game", + "readme":"README.md", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"mmind.app.js","url":"mmind.app.js"}, + {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} + ] +} diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js new file mode 100644 index 000000000..e7def025d --- /dev/null +++ b/apps/mmind/mmind.app.js @@ -0,0 +1,198 @@ +//MMind + +//set vars +const H = g.getWidth(); +const W = g.getHeight(); +var touch_actions = []; +var cols = ["#FF0000","#00FF00","#0000FF", "#FF00FF", "#FFFF00", "#00FFFF", "#000000","#FFFFFF"]; +var turn = 0; +var col_menu = false; +//pinsRow = 6; +//pinsThick = 10; +//pinsRow = 5; +//pinsThick = 10; +var pinsRow = 4; +var pinsThick = 10; +var play = [-1, -1, -1, -1]; + +var pinsCol = 5; +var playx = -1; +var sx = (W - 30 )/pinsRow; +var sy = (H - 20 )/7; +var touch_actions = []; +var secret = []; +var secret_no_dub = true; +var endgame = false; + +g.clear(); +g.setColor("#FFFFFF"); +g.fillRect(0, 0, H, W); +g.setFont("Vector12",45); + +function draw() { + touch_actions = []; + g.clear(); + g.setColor("#FFFFFF"); + g.fillRect(0, 0, H, W); + g.setColor("#000000"); + //draw scores + for (y=0;y= 0) s = Math.round(Math.random()*pinsCol); + secret[i]= s; + } + } + +function score() { + bScore = 0; + wScore = 0; + for (i=0; i touch_actions[i][0][0] && e.x < touch_actions[i][0][2] && + e.y > touch_actions[i][0][1] && e.y < touch_actions[i][0][3]) { + // a action is hit, add acctions here, todo: start, stop, new, etc. + switch (touch_actions[i][1][0]) { + case 1: + //get pins col menu + col_menu = 1; + playx = touch_actions[i][1][1]; + break; + case 2: + //copy choice col to play + play[playx] = touch_actions[i][1][1]; + col_menu = 0; + break; + case 3: + //score play + var sc; + sc = score(); + game.push([play, sc]); + play = [-1,-1,-1,-1]; + turn+=1; + if (turn==6 || sc[0]==pinsRow) { + play = secret; + col_menu = 0; + endgame = true; + } + break; + case 4: + //new game + play = [-1,-1,-1,-1]; + game = []; + endgame=false; + break; + } + } + } + //console.log(touch_actions[i][1][0], touch_actions[i][1][1]); + + draw(); + } +); + + +game = []; +get_secret(); +draw(); +//Bangle.loadWidgets(); +//Bangle.drawWidgets(); + + + + + diff --git a/apps/mmind/mmind.icon.js b/apps/mmind/mmind.icon.js new file mode 100644 index 000000000..17c28ba0f --- /dev/null +++ b/apps/mmind/mmind.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+64A/AEOBq2sBAusqwJHCaQFDAYlP2m0yGBCIkSj0eiWHBIkDgsFgYTE01v3O5t4mC1krgAEBq0ACYQuCAANsHIcxFwIwCEocsFwIwCBIYuCAANQF4QwBOgQABAgNIF4ZgELwQvCHIcCF4cEKwYvEt45DF4QwCL5YvFL5ITDF6OstheCvTjEjAuBjDJFX4UEq4TEyguBygTEF4dWBIeskkkqwQDDgUGgwaEBIUBgITHkslCYeBd4MrqwDBAgIuBcwRVGNIVs0oJEv3S6V+CYmIisjkcVZAYpBgDyBAAJFBFwTlGZIolDqouBGAQJDFwQABmRfCFAICCGwXXhgvDMAheCfI1UF4eoKwYvEiovHSoJfLF4pfJCYYvN1gwBAYMSLwVcbQmQFwOQZIq/C1GACYkcFwMcCYQoCLYNWF4KPBDgNWmIkEBIVPp5TDBIdWqoTHmUyCYlWRQTwCD4wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHmy2QJH6PRBI/Q6AkOCAIAFBINDjwABGInR3+53O/GIu72gABGJnQCAQAE69oFwQABCYfFFwIwCBIfCDIe7FIus1gvXLwQACLw4aCAAkAgAvcL4gvLq1WF5uyFwdoCYfLF4fLDpHCX6owBtFoxoUF6PF4ruFDwPC4XJFxbSCAAwVNAH4ARA")) diff --git a/apps/mmind/mmind.info b/apps/mmind/mmind.info new file mode 100644 index 000000000..2e79822b1 --- /dev/null +++ b/apps/mmind/mmind.info @@ -0,0 +1,17 @@ + { + "id": "mmind", + "name": "Classic Mind Game", + "shortName":"Master Mind", + "icon": "mmind.png", + "version":"0.01", + "description": "This is the classic game for masterminds", + "type": "game", + "tags": "mastermind, game, classic", + "readme":"README.md", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"mmind.app.js","url":"mmind.app.js"}, + {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} + ] + } diff --git a/apps/mmind/mmind.png b/apps/mmind/mmind.png new file mode 100644 index 000000000..14a3ef7c6 Binary files /dev/null and b/apps/mmind/mmind.png differ diff --git a/apps/mmind/screenshot_mmind.png b/apps/mmind/screenshot_mmind.png new file mode 100644 index 000000000..5c886e7e8 Binary files /dev/null and b/apps/mmind/screenshot_mmind.png differ diff --git a/apps/mmonday/manic-monday-icon.js b/apps/mmonday/manic-monday-icon.js index feba5fe86..2b1ee7f79 100644 --- a/apps/mmonday/manic-monday-icon.js +++ b/apps/mmonday/manic-monday-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) +require("heatshrink").decompress(atob("mEwwIHEgPAAocP+AFDv4FDgf/Aoc/AocB/4FDh4FEv4FEAgIFIDgQFR+FwAoeAAof8gAFDLoIFC/wyBAoQ4CAoXgAoh0CAtybCAoJPBAoahDAoMHAoicBAoM54EfAoJqCAoQUBAoYUBAoYCBAoXgZAIFC4AFCCgOAYYI1CZIRHB/AFDcwmAAoj9Dj6mCdoQaBAAYWDgA")) diff --git a/apps/mmonday/metadata.json b/apps/mmonday/metadata.json new file mode 100644 index 000000000..bc101a84c --- /dev/null +++ b/apps/mmonday/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "mmonday", + "name": "Manic Monday Tone", + "version": "0.02", + "description": "The Bangles make a comeback", + "icon": "manic-monday-icon.png", + "tags": "sound", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"mmonday.app.js","url":"manic-monday.js"}, + {"name":"mmonday.img","url":"manic-monday-icon.js","evaluate":true} + ] +} diff --git a/apps/moonphase/metadata.json b/apps/moonphase/metadata.json new file mode 100644 index 000000000..548518338 --- /dev/null +++ b/apps/moonphase/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "moonphase", + "name": "Moonphase", + "version": "0.02", + "description": "Shows current moon phase. Now with GPS function.", + "icon": "app.png", + "tags": "", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-moon-phase-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"moonphase.app.js","url":"app.js"}, + {"name":"moonphase.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/morse/metadata.json b/apps/morse/metadata.json new file mode 100644 index 000000000..17b73b9cf --- /dev/null +++ b/apps/morse/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "morse", + "name": "Morse Code", + "version": "0.01", + "description": "Learn morse code by hearing/seeing/feeling the code. Tap to toggle buzz!", + "icon": "morse-code.png", + "type": "app", + "tags": "morse,sound,visual,input", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"morse.app.js","url":"morse-code.js"}, + {"name":"morse.img","url":"morse-code-icon.js","evaluate":true} + ] +} diff --git a/apps/multiclock/metadata.json b/apps/multiclock/metadata.json new file mode 100644 index 000000000..197e6631c --- /dev/null +++ b/apps/multiclock/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "multiclock", + "name": "Multi Clock", + "version": "0.09", + "description": "Clock with multiple faces. Switch between faces with BTN1 & BTN3 (Bangle 2 touch top-right, bottom right). For best display set theme Background 2 to cyan or some other bright colour in settings.", + "screenshots": [{"url":"screen-ana.png"},{"url":"screen-big.png"},{"url":"screen-td.png"},{"url":"screen-nifty.png"},{"url":"screen-word.png"},{"url":"screen-sec.png"}], + "icon": "multiclock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"multiclock.app.js","url":"multiclock.app.js"}, + {"name":"big.face.js","url":"big.face.js"}, + {"name":"ana.face.js","url":"ana.face.js"}, + {"name":"digi.face.js","url":"digi.face.js"}, + {"name":"txt.face.js","url":"txt.face.js"}, + {"name":"dk.face.js","url":"dk.face.js"}, + {"name":"nifty.face.js","url":"nifty.face.js"}, + {"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true} + ] +} diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog new file mode 100644 index 000000000..653f859ae --- /dev/null +++ b/apps/mylocation/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Enhanced icon, make it bolder 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/metadata.json b/apps/mylocation/metadata.json new file mode 100644 index 000000000..b26a97290 --- /dev/null +++ b/apps/mylocation/metadata.json @@ -0,0 +1,19 @@ +{ "id": "mylocation", + "name": "My Location", + "shortName":"My Location", + "icon": "mylocation.png", + "type": "app", + "screenshots": [{"url":"screenshot_1.png"}], + "version":"0.02", + "description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README", + "readme": "README.md", + "tags": "tool,utility", + "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"} + ] +} 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..b79f5875f --- /dev/null +++ b/apps/mylocation/mylocation.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///gH4AYPO/QPDgNVqtADY/1BYNfBQ0PBQIAB+ALFmoLDrgLF6oLDq4KEgYKDBYPABYcNBYlVuAuIGAwuEAANUBYYKFHgg6Bq4ZCr4DBHgQLBvWq2te1WlBYZGBBYOr1Wq1qSDBYNqBIILDKgQLLgoLHqBqDBfJHLBZBrOgKPCBYiPCU4NaBYe1WYrABBQLCCfgYGCrwVBa4kAirvKNgIAErgLDKgIAEKQQ8EAAY6DBZhIDIww8GHQg8GHQgwGFwowEFwx5EOog8GHQ0AlWpBYNq1AKFWIILBAYOgBYbICytWAgQKCgTgDcwYXGAAgvGAAY8EEgYWGBgoVEA==")) diff --git a/apps/mylocation/mylocation.png b/apps/mylocation/mylocation.png new file mode 100644 index 000000000..038ee177e 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/mysticclock/metadata.json b/apps/mysticclock/metadata.json new file mode 100644 index 000000000..571a55ecd --- /dev/null +++ b/apps/mysticclock/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "mysticclock", + "name": "Mystic Clock", + "version": "1.01", + "description": "A retro-inspired watchface featuring time, date, and an interactive data display line.", + "icon": "mystic-clock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-mystic-clock-screenshot.png"}], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"mysticclock.app.js","url":"mystic-clock-app.js"}, + {"name":"mysticclock.settings.js","url":"mystic-clock-settings.js"}, + {"name":"mysticclock.img","url":"mystic-clock-icon.js","evaluate":true} + ] +} diff --git a/apps/mysticdock/ChangeLog b/apps/mysticdock/ChangeLog index 34fe53627..eacafb944 100644 --- a/apps/mysticdock/ChangeLog +++ b/apps/mysticdock/ChangeLog @@ -1 +1 @@ -1.00: First published version. +0.01: First published version. diff --git a/apps/mysticdock/metadata.json b/apps/mysticdock/metadata.json new file mode 100644 index 000000000..54ebedd93 --- /dev/null +++ b/apps/mysticdock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "mysticdock", + "name": "Mystic Dock", + "version": "0.01", + "description": "A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line.", + "icon": "mystic-dock.png", + "type": "dock", + "tags": "dock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"mysticdock.app.js","url":"mystic-dock-app.js"}, + {"name":"mysticdock.boot.js","url":"mystic-dock-boot.js"}, + {"name":"mysticdock.settings.js","url":"mystic-dock-settings.js"}, + {"name":"mysticdock.img","url":"mystic-dock-icon.js","evaluate":true} + ] +} 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/mywelcome/metadata.json b/apps/mywelcome/metadata.json new file mode 100644 index 000000000..b6d37d2e1 --- /dev/null +++ b/apps/mywelcome/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "mywelcome", + "name": "Customised Welcome", + "shortName": "My Welcome", + "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","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-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} + ], + "data": [{"name":"mywelcome.json"}] +} diff --git a/apps/nato/metadata.json b/apps/nato/metadata.json new file mode 100644 index 000000000..49366e6e7 --- /dev/null +++ b/apps/nato/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "nato", + "name": "NATO Alphabet", + "shortName": "NATOAlphabet", + "version": "0.01", + "description": "Learn the NATO Phonetic alphabet plus some numbers.", + "icon": "nato.png", + "type": "app", + "tags": "app,learn,visual", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-NATO-alphabet-screenshot.png"},{"url":"bangle1-NATO-alphabet-screenshot2.png"}], + "storage": [ + {"name":"nato.app.js","url":"nato.js"}, + {"name":"nato.img","url":"nato-icon.js","evaluate":true} + ] +} diff --git a/apps/ncfrun/metadata.json b/apps/ncfrun/metadata.json new file mode 100644 index 000000000..831ae3d4e --- /dev/null +++ b/apps/ncfrun/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "ncfrun", + "name": "NCEU 5K Fun Run", + "version": "0.01", + "description": "Display a map of the NodeConf EU 2019 5K Fun Run route and your location on it", + "icon": "nceu-funrun.png", + "tags": "health", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"ncfrun.app.js","url":"nceu-funrun.js"}, + {"name":"ncfrun.img","url":"nceu-funrun-icon.js","evaluate":true} + ] +} diff --git a/apps/ncrclk/metadata.json b/apps/ncrclk/metadata.json new file mode 100644 index 000000000..b50b554e1 --- /dev/null +++ b/apps/ncrclk/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "ncrclk", + "name": "NCR Clock", + "shortName": "NCR Clock", + "version": "0.02", + "description": "NodeConf Remote clock", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"ncrclk.app.js","url":"app.js"}, + {"name":"ncrclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/ncstart/metadata.json b/apps/ncstart/metadata.json new file mode 100644 index 000000000..d2b3e2196 --- /dev/null +++ b/apps/ncstart/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "ncstart", + "name": "NCEU Startup", + "version": "0.06", + "description": "NodeConfEU 2019 'First Start' Sequence", + "icon": "start.png", + "tags": "start,welcome", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"ncstart.app.js","url":"start.js"}, + {"name":"ncstart.boot.js","url":"boot.js"}, + {"name":"ncstart.settings.js","url":"settings.js"}, + {"name":"ncstart.img","url":"start-icon.js","evaluate":true}, + {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, + {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, + {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true}, + {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true}, + {"name":"nc-tf.img","url":"start-tf.js","evaluate":true} + ], + "data": [{"name":"ncstart.json"}] +} diff --git a/apps/nixie/metadata.json b/apps/nixie/metadata.json new file mode 100644 index 000000000..50f02712b --- /dev/null +++ b/apps/nixie/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "nixie", + "name": "Nixie Clock", + "shortName": "Nixie", + "version": "0.01", + "description": "A nixie tube clock for both Bangle 1 and 2.", + "icon": "nixie.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"nixie.app.js","url":"app.js"}, + {"name":"nixie.img","url":"app-icon.js","evaluate":true}, + {"name":"m_vatch.js","url":"m_vatch.js"} + ] +} diff --git a/apps/notanalog/ChangeLog b/apps/notanalog/ChangeLog new file mode 100644 index 000000000..aedbeb684 --- /dev/null +++ b/apps/notanalog/ChangeLog @@ -0,0 +1 @@ +0.01: Launch app. diff --git a/apps/notanalog/README.md b/apps/notanalog/README.md new file mode 100644 index 000000000..22097f0fc --- /dev/null +++ b/apps/notanalog/README.md @@ -0,0 +1,32 @@ +# NotAnalog +An analog watch face for people (like me) that can not read analog watch faces. +It looks like an analog clock, but its not! It shows the time digital - check the +4 numbers on the watch face ;) + +The red hand shows the number of steps (0 = 0°, 2.5k = 90°, 5k = 180°, ...) and the +black one the battery level (100% = 0°, 75% = 270°, 50% = 180°, ...). +The selected theme is also respected. Note that this watch face is in fullscreen +mode, but widgets are still loaded in background. + +## Other features +- Set a timer - simply touch top (+5min.) or bottom (-5 min.). +- If the weather is available through the weather app, the outside temp. will be shown. +- If the battery is charged, the icons will change. +- If you have done more than 10k steps, the red hand and icon will turn green. +- Shows current lock status of your bangle va a colored dot in the middle. +- Sows symbol if GPS is on. + +## Screenshots +![](screenshot_1.png) +![](screenshot_2.png) +![](screenshot_3.png) + + +# Thanks +Thanks to the multiclock from https://github.com/jeffmer/BangleApps/ +which helped a lot for this development. + +Icons from by Freepik - Flaticon + +## Contributors +- [David Peer](https://github.com/peerdavid). \ No newline at end of file diff --git a/apps/notanalog/metadata.json b/apps/notanalog/metadata.json new file mode 100644 index 000000000..a2ce9260d --- /dev/null +++ b/apps/notanalog/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "notanalog", + "name": "Not Analog", + "shortName":"Not Analog", + "icon": "notanalog.png", + "version":"0.1", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "description": "An analog watch face for people that can not read analog watch faces.", + "type": "clock", + "tags": "clock", + "screenshots": [ + {"url":"screenshot_1.png"}, + {"url":"screenshot_2.png"} + ], + "storage": [ + {"name":"notanalog.app.js","url":"notanalog.app.js"}, + {"name":"notanalog.img","url":"notanalog.icon.js","evaluate":true} + ] +} diff --git a/apps/notanalog/notanalog.app.js b/apps/notanalog/notanalog.app.js new file mode 100644 index 000000000..ad8adfe83 --- /dev/null +++ b/apps/notanalog/notanalog.app.js @@ -0,0 +1,429 @@ +/** + * NOT ANALOG CLOCK + */ + +const locale = require('locale'); +const storage = require('Storage') +const SETTINGS_FILE = "notanalog.setting.json"; +let settings = { + alarm: -1, +}; +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key] +} + +/* + * Set some important constants such as width, height and center + */ +var W = g.getWidth(),R=W/2; +var H = g.getHeight(); +var cx = W/2; +var cy = H/2; +var drawTimeout; + +var state = { + color: "#ff0000", + steps: 0, + maxSteps: 10000, + bat: 0, + has_weather: false, + temp: "-" +} + +var chargeImg = { + width : 32, height : 32, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("AAAMAAAAHgAAADMAAABjAAAAxgAAD44AAB8cAAA7uAAAcfAMAODgPgDAcHMBgDjjAYAdxgGBj4wBg8cYAYZjsAGGYeABg8DgAQGAYAMAAOAHgAHAB8ADgAzgBwAYc/4AGD/4AAw4AAAOcAAAH8AAADmAAABwAAAA4AAAAMAAAAA=")) +}; + +var alarmImg = { + width : 32, height : 32, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("AA/wAAAP8AAAD/AAAAGAAAABgAAAA8AABh/4YAd//uAH+B/gA+AHwAOAAcAHAPDgDgD4cA4A/HAcAP44HAD+OBwA/zgYAP8YGAD/GBj//xgc//84HH/+OBx//jgOP/xwDh/4cAcP8OAHg8HgA8ADwAHwD4AA//8AAD/8AAAP8AA=")) +}; + +var stepsImg = { + width : 32, height : 32, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("AcAAAAPwAAAH8AAAB/gAAAf4AAAH/AAAD/wAAAf8AAAH/AfAB/wP4Af8H+AH/B/gB/wf4AP8P+AD+D/gAfg/4AGAP+AAPD/gAPw/4AD+P+AAfj/AAH4/wAB+H8AAPAeAAAAwAAAAPgAAAH8AAAB/AAAAfgAAAH4AAAA8AAAAOAA=")) +}; + +var gpsImg = { + width : 32, height : 32, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("AAAMAAAAD4AAAAHAAAAA4AAADjABAA8YAYADmAPAAcwD4DzMB/B8zAf4fAAH/HwAB/74AAf/wAAH/4AAB//AAAP/4AAD//AAA//4AAH//AAA//4AAH//AAA//4ABH/4AAYP4AAHgAAAB/AAAA/4AAAP+AAAD/gAAP//gAD//4AA=")) +}; + + +/* + * Based on the great multi clock from https://github.com/jeffmer/BangleApps/ + */ +Graphics.prototype.drawRotRect = function(w, r1, r2, angle) { + angle = angle % 360; + var w2=w/2, h=r2-r1, theta=angle*Math.PI/180; + return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0], + {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta})); +}; + +// The following font was used: +// +Graphics.prototype.setTimeFont = function(scale) { + // Actual height 26 (26 - 1) + this.setFontCustom(atob("AAAAAAAAAD4AAAAD4AAAAD4AAAAD4AAAAB4AAAAAAAAAAAAAAAAD4AAAD/4AAD//4AD///4Af///gAf//gAAf/gAAAfwAAAAQAAAAAAAAAAAAAAAAAB//+AAH///gAP///wAP///wAfwAP4AfAAD4AfAAD4AfAAD4AfAAD4AfAAD4AfgAH4AP///wAP///wAH///gAB//+AAAP/wAAAAAAAAAAAAAAf///4Af///4Af///4Af///4Af///4AAAAAAAAAAAAAAAAAAAB+AD4AH+AP4AP+Af4AP+B/4AfwD/4AfAH/4AfAf74AfA/z4AfD/j4Af/+D4AP/8D4AH/wD4AD/gD4AB+AD4AAAAAAAB8B8AAD8B/AAH8B/gAP8B/wAf8A/4AfAAD4AfB8D4AfB8D4AfD8D4Af3/P4AP///wAP///wAH///gAB/H+AAAAAAAAAAAAAAAAB+AAAAP+AAAA/+AAAD/+AAAP/+AAA/8+AAD/w+AAf/A+AAf///4Af///4Af///4Af///4AD///4AAAA+AAAAA+AAAAAAAAAAAYAAf/8/AAf/8/gAf/8/wAf/8/wAfD4H4AfD4D4AfD4D4AfD4D4AfD8H4AfD//4AfB//wAfA//gAeAf/AAAAH8AAAAAAAAAAAAAAB//+AAD///AAH///gAP///wAf///4AfD4D4AfD4D4AfD4D4AfD4D4Afz+P4AP5//wAP5//wAH4//gAB4P+AAAAAAAAAAAAAAfAAAAAfAAAAAfAAAIAfAAB4AfAAP4AfAB/4AfAP/4AfA//4AfH//AAf//4AAf/+AAAf/wAAAf+AAAAfwAAAAAAAAAAAAAAAAB8P+AAH///gAP///wAP///wAf/+P4AfH4D4AfD4D4AfD4D4Afn8D4Af///4AP///wAH///gAD/f/AAA4P+AAAAAAAAAAAQAAB/w+AAH/4/gAP/8/wAP/+/wAfh+P4AfA/D4AfAfD4AfAfD4AfA+H4Af///4AP///wAH///gAD///AAA//8AAAAAAAAAAAAAAAB8D4AAB8D4AAB8D4AAB8D4AAA8B4AAAAAAA=="), 46, atob("BwsSCBAQEBAQEBAQBw=="), 36+(scale<<8)+(1<<16)); + return this; +}; + +Graphics.prototype.setNormalFont = function(scale) { + // Actual height 19 (18 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAD/5wP/3A/+cAAAAPwAA/AADAAAAgAA/AAD8AAIAAAAAAAxgADGAA/+AD/4ADGAAMYAD/4AP/gAMYAAxgAB84AP7wD3jwPHPA+f8A+/AB54AAAAB+AAP8BAwwcDnHwP8/AfPwAD8AA/AAPxwB+fgPh/A4GMCAfwAA+AAAAA+/AH/+A//8DjhwOOHA4/8Dj/wAP/AA4AADgAAAAAAAAD8AAPwAAwAAAAAAB/4Af/4D//wPAPA4AcDgBwAAAAAAADgBwOAHA//8B//gD/8AD/AAoAAGwAA/gAD+AAHwAAbAAAAAAAAAAAAAABgAAGAAAYAA/+AD/4AAYAABgAAGAAAYAAAByAAH4AAfAAAAAGAAA4AADgAAOAAA4AAAAAAAAAAAAAABwAAHAAAcAAAAAA/AD/8D//gP+AA8AAAAAAD/8Af/4D//wOAHA4AcDgBwPAPA//8B//gB/4AAAAAAAAP//A//8D//wAAAADAMA8DwHw/A+H8Dg/wOP3A/8cB/hwD4HAAAAAYEAHw8AfD4D4HwOOHA44cD//wH/+APvwAAAAAB4AAfgAH+AB/4AfjgD//wP//A//8AAOAAA4AAAAD/vAP++A/58DnBwOcHA5/8Dj/gOH8AAHAA//AH/+A//8DnhwOcHA548D7/wHv+AOPgAAAAOAAA4AADgBwOA/A4f8Dv/AP/gA/wAD4AAAAAAACAA9/AH/+A//8DnBwOcHA//8B//gD38AAHAA/HAH++A/98DhzwOHHA4c8D//wH/+AP/wAAAAAAAAA4cADhwAOHAA4cgDh+AOHwAAAABgAAPAAB8AAH4AA5wAHDgAYGAAAQAAAAAGYAAZgABmAAGYAAZgABmAAGYAAZgABmAAAAAAAQAGDgAcOAA5wAB+AAHwAAPAAAYAAAAADwAAfgAD8AAOD3A4fcDz9wP+AAfwAAcAAAAAAP/wB//gP//A4A8Dn5wOf3A5/cD/9wH/3AP+cAABwAAAAAH8AP/wP//A/84D/zgP//AH/8AAfwAABAAAAD//wP//A//8DjhwOOHA488D//wH/+APngA//AH/+A//8DgBwOAHA8A8D8PwHw+AHDgAAAAAAAA//8D//wP//A4AcDgBwPAPA//8B//gD/4AAAAD//wP//A//8DjhwOOHA44cDjhwOOHAAAAD//wP//A//8DjgAOOAA44ADjgAOOAAP/wB//gP//A4AcDhxwOHPA/f8B9/gDn4AAAAAAAAP//A//8D//wAOAAA4AADgAP//A//8D//wAAAA//8D//wP//AAAAAAPAAA+AAD8AABwAAHAAA8D//wP/+A//gAAAAAAAA//8D//wP//AD8AA/8AH/8A+H8DgHwIAHAAAAD//wP//A//8AABwAAHAAAcAABwAAHAAAAD//wP//A//8D8AAD4AAPgAB8AAP//A//8D//wAAAAAAAD//wP//Af/8Af4AAf4A//4D//wP//AAAAA//AH/+A//8DgBwOAHA4A8D//wH/+AP/wAAAAAAAA//8D//wP//A4cADhwAOPAA/8AB/gAD4AAP/wB//gP//A4AcDgBwPAPA//8B//4D//gAAEAAAAP//A//8D//wOOAA44ADjwAP//Af/8A+fwAAAAPjwB/PgP+/A48cDhxwPn/Afv4B+fgBw4AAAADgAAOAAA4AAD//wP//A//8DgAAOAAA4AAD//AP/+A//8AABwAAHAAA8D//wP/+A//wAAAAPAAA/4AD//gA//AAP8Af/wP/8A/4ADgAAAAAA4AAD/gAP//AH/8AB/wP//A//AD//gB//AAP8D//wP/4A/gACAAAOAHA/D8D//wB/4AH/gD//wPx/A4AcAAAAMAAA+AAD/AAD//AB/8B//wP8AA+AADAAAOAfA4H8Dh/wOf3A/8cD/BwPwHA8AcAAAAP//A//8D//wOAHA4AcDgBwAAAAAAAD8AAP/wAf/8AD/wAAPAAAAAAAAOAHA4AcDgBwP//A//8D//wA=="), 32, atob("AwQJCggPCwUHBwgKBAcEBwsFCgoKCgoKCgoEBAkKCQoMCQoKCgkJCgoFCgoJDAoKCgoLCgkKCg4JCQkHBwc="), 22+(scale<<8)+(1<<16)); + return this; +}; + + + +function getSteps() { + var steps = 0; + let health; + try { + health = require("health"); + } catch(ex) { + return steps; + } + + health.readDay(new Date(), h=>steps+=h.steps); + return steps; +} + + +function drawBackground() { + g.setFontAlign(0,0,0); + g.setNormalFont(); + + g.setColor(g.theme.fg); + for (let a=0;a<360;a+=6){ + if (a % 30 == 0 || (a > 345 || a < 15) || (a > 90-15 && a < 90+15) || (a > 180-15 && a < 180+15) || (a > 270-15 && a < 270+15)) { + continue; + } + + var theta=a*Math.PI/180; + g.drawLine(cx,cy,cx+125*Math.sin(theta),cy-125*Math.cos(theta)); + } + + g.clearRect(10,10,W-10,H-10); + for (let a=0;a<360;a+=30){ + if(a == 0 || a == 90 || a == 180 || a == 270){ + continue; + } + g.drawRotRect(6,R-80,125,a); + } + + g.clearRect(16,16,W-16,H-16); +} + + +function drawState(){ + g.setFontAlign(1,0,0); + + // Draw alarm + var highPrioImg = isAlarmEnabled() ? alarmImg : + Bangle.isCharging() ? chargeImg : + Bangle.isGPSOn() ? gpsImg : + undefined; + + var imgColor = isAlarmEnabled() ? state.color : + Bangle.isCharging() ? g.theme.fg : + Bangle.isGPSOn() ? g.theme.fg : + state.color; + + // As default, we draw weather if available, otherwise the steps symbol is shown. + if(!highPrioImg && state.has_weather){ + g.setColor(g.theme.fg); + g.drawString(state.temp, cx+cx/2+15, cy+cy/2+10); + } else { + g.setColor(imgColor); + var img = highPrioImg ? highPrioImg : stepsImg; + g.drawImage(img, cx+cx/2 - img.width/2 + 5, cy+cy/2 - img.height/2+5); + } +} + +function drawData() { + g.setFontAlign(0,0,0); + g.setNormalFont(); + + // Set hand functions + var drawBatteryHand = g.drawRotRect.bind(g,6,12,R-38); + var drawDataHand = g.drawRotRect.bind(g,5,12,R-24); + + // Draw battery hand + g.setColor(g.theme.fg); + g.setFontAlign(0,0,0); + drawBatteryHand(parseInt(state.bat*360/100)); + + // Draw data hand - depending on state + g.setColor(state.color); + if(isAlarmEnabled()){ + var alrm = getAlarmMinutes(); + drawDataHand(parseInt(alrm*360/60)); + return; + } + + // Default are the steps + drawDataHand(parseInt(state.steps*360/state.maxSteps)); +} + + +function drawTime(){ + g.setTimeFont(); + g.setFontAlign(0,0,0); + g.setColor(g.theme.fg); + + var currentDate = new Date(); + var posX = 14; + var posY = 14; + + // Hour + var h = currentDate.getHours(); + var h1 = parseInt(h / 10); + var h2 = h < 10 ? h : h - h1*10; + g.drawString(h1, cx, posY+8); + g.drawString(h2, W-posX, cy+5); + + // Minutes + var m = currentDate.getMinutes(); + var m1 = parseInt(m / 10); + var m2 = m < 10 ? m : m - m1*10; + g.drawString(m2, cx, H-posY); + g.drawString(m1, posX-1, cy+5); +} + + +function drawDate(){ + var currentDate = new Date(); + + // Date + g.setFontAlign(-1,0,0); + g.setNormalFont(); + g.setColor(g.theme.fg); + var dayStr = locale.dow(currentDate, true).toUpperCase(); + g.drawString(dayStr, cx/2-15, cy/2-5); + g.drawString(currentDate.getDate(), cx/2-15, cy/2+17); +} + + +function drawLock(){ + g.setColor(g.theme.fg); + g.fillCircle(cx, cy, 7); + + var c = Bangle.isLocked() ? state.color : g.theme.bg; + g.setColor(c); + g.fillCircle(cx, cy, 4); +} + + +function handleState(fastUpdate){ + // Set theme color + state.color = isAlarmEnabled() ? "#FF6A00" : + state.steps > state.maxSteps ? "#00ff00" : + "#ff0000"; + + if(fastUpdate){ + return; + } + + // Set battery + state.bat = E.getBattery(); + + // Set steps + state.steps = getSteps(); + state.maxSteps = 10000; + + // Set weather + state.has_weather = true; + try { + weather = require('weather').get(); + if (weather === undefined){ + state.has_weather = false; + state.temp = "-"; + } else { + state.temp = locale.temp(Math.round(weather.temp-273.15)); + } + } catch(ex) { + state.has_weather = false; + } + +} + + +function draw(fastUpdate){ + // Execute handlers + handleState(fastUpdate); + handleAlarm(); + + // Clear watch face + if(fastUpdate){ + var innerRect = 20; + g.clearRect(innerRect, innerRect, g.getWidth()-innerRect, g.getHeight()-innerRect); + } else { + g.reset(); + g.clearRect(0, 0, g.getWidth(), g.getHeight()); + } + + // Draw again + g.setColor(1,1,1); + + if(!fastUpdate){ + drawBackground(); + } + + drawDate(); + drawLock(); + drawState(); + drawData(); + drawTime(); + + // Queue draw in one minute + queueDraw(); +} + + +/* + * Listeners + */ +Bangle.on('lcdPower',on=>{ + if (on) { + draw(false); + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('charging',function(charging) { + draw(false); +}); + +Bangle.on('lock', function(isLocked) { + drawLock(); +}); + +Bangle.on('touch', function(btn, e){ + var upper = parseInt(g.getHeight() * 0.2); + var lower = g.getHeight() - upper; + + var is_upper = e.y < upper; + var is_lower = e.y > lower; + + if(is_upper){ + feedback(); + increaseAlarm(); + draw(true); + } + + if(is_lower){ + feedback(); + decreaseAlarm(); + draw(true); + } +}); + + +/* + * Some helpers + */ +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(false); + }, 60000 - (Date.now() % 60000)); +} + + +/* + * Handle alarm + */ +function getCurrentTimeInMinutes(){ + return Math.floor(Date.now() / (1000*60)); +} + +function isAlarmEnabled(){ + return settings.alarm >= 0; +} + +function getAlarmMinutes(){ + var currentTime = getCurrentTimeInMinutes(); + return settings.alarm - currentTime; +} + +function handleAlarm(){ + if(!isAlarmEnabled()){ + return; + } + + if(getAlarmMinutes() > 0){ + return; + } + + // Alarm + var t = 300; + Bangle.buzz(t, 1) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, 5E3))) + .then(() => { + // Update alarm state to disabled + settings.alarm = -1; + storage.writeJSON(SETTINGS_FILE, settings); + }); +} + + +function increaseAlarm(){ + if(isAlarmEnabled()){ + settings.alarm += 5; + } else { + settings.alarm = getCurrentTimeInMinutes() + 5; + } + + storage.writeJSON(SETTINGS_FILE, settings); +} + + +function decreaseAlarm(){ + if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){ + settings.alarm -= 5; + } else { + settings.alarm = -1; + } + + storage.writeJSON(SETTINGS_FILE, settings); +} + +function feedback(){ + Bangle.buzz(40, 0.6); +} + +/* + * Lets start widgets, listen for btn etc. + */ +// Show launcher when middle button pressed +Bangle.setUI("clock"); +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="";} + +// Clear the screen once, at startup and draw clock +// g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); +draw(false); + +// After drawing the watch face, we can draw the widgets +// Bangle.drawWidgets(); diff --git a/apps/notanalog/notanalog.icon.js b/apps/notanalog/notanalog.icon.js new file mode 100644 index 000000000..390a574ac --- /dev/null +++ b/apps/notanalog/notanalog.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkAxAA/ADWP/AWV/4XVxAXX5/5Iif/xHvF6QXDF6YADF5GHv4lBAAP3hBfOw4VDAAd4F5gWIAAIxFF4wREAxB6EGAZbC5+PAAYZD+woFIozoIJI4uFFogxEIQPwEZLdKaoYJIFxBiEIxAuKEwZIGFxeM/ADBGA6MJFgYPDSIouKBYovHLxOPBQZgHL4YNCFxAXB+4XFEYgpEHIpAGKYoMBFw6IHF4oYDNA3//yPLC4QAHF4olFTo4KJX46SEF6AHCz7FLF5OJz70HR5IADxOYBAy/LF8nwAwowGF46OBwBOLF5AXB5AvTB4UIC4wvNH48PC4JfMLwxPCGAovGHw4JD/AvKC5JIBBRAkDXooACwwLBMQyMDRowwGGIwIC4A7JPQZkDAoYVKAAOACIYAE/BFJA=")) \ No newline at end of file diff --git a/apps/notanalog/notanalog.png b/apps/notanalog/notanalog.png new file mode 100644 index 000000000..f27d79729 Binary files /dev/null and b/apps/notanalog/notanalog.png differ diff --git a/apps/notanalog/screenshot_1.png b/apps/notanalog/screenshot_1.png new file mode 100644 index 000000000..de7d9e61f Binary files /dev/null and b/apps/notanalog/screenshot_1.png differ diff --git a/apps/notanalog/screenshot_2.png b/apps/notanalog/screenshot_2.png new file mode 100644 index 000000000..8515542d4 Binary files /dev/null and b/apps/notanalog/screenshot_2.png differ diff --git a/apps/notanalog/screenshot_3.png b/apps/notanalog/screenshot_3.png new file mode 100644 index 000000000..b638862af Binary files /dev/null and b/apps/notanalog/screenshot_3.png differ diff --git a/apps/notify/metadata.json b/apps/notify/metadata.json new file mode 100644 index 000000000..e92d5e0e4 --- /dev/null +++ b/apps/notify/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "notify", + "name": "Notifications (default)", + "shortName": "Notifications", + "version": "0.11", + "description": "Provides the default `notify` module used by applications to display notifications in a bar at the top of the screen. This module is installed by default by client applications such as the Gadgetbridge app. Installing `Fullscreen Notifications` replaces this module with a version that displays the notifications using the full screen", + "icon": "notify.png", + "type": "notify", + "tags": "widget", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"notify","url":"notify.js"} + ] +} diff --git a/apps/notifyfs/metadata.json b/apps/notifyfs/metadata.json new file mode 100644 index 000000000..dea8cb022 --- /dev/null +++ b/apps/notifyfs/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "notifyfs", + "name": "Fullscreen Notifications", + "shortName": "Notifications", + "version": "0.12", + "description": "Provides a replacement for the `Notifications (default)` `notify` module. This version is used by applications to display notifications fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notify module.", + "icon": "notify.png", + "type": "notify", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"notify","url":"notify.js"} + ] +} 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/metadata.json b/apps/numerals/metadata.json new file mode 100644 index 000000000..dcb86da9a --- /dev/null +++ b/apps/numerals/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "numerals", + "name": "Numerals Clock", + "shortName": "Numerals Clock", + "version": "0.10", + "description": "A simple big numerals clock", + "icon": "numerals.png", + "type": "clock", + "tags": "numerals,clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-numerals-screenshot.png"}], + "storage": [ + {"name":"numerals.app.js","url":"numerals.app.js"}, + {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, + {"name":"numerals.settings.js","url":"numerals.settings.js"} + ], + "data": [{"name":"numerals.json"}] +} 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/oblique/metadata.json b/apps/oblique/metadata.json new file mode 100644 index 000000000..048c00a38 --- /dev/null +++ b/apps/oblique/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "oblique", + "name": "Oblique Strategies", + "version": "0.01", + "description": "Oblique Strategies for creativity. Copied from Brian Eno.", + "icon": "eno.png", + "tags": "tool", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"oblique.app.js","url":"app.js"}, + {"name":"oblique.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/omnitrix/metadata.json b/apps/omnitrix/metadata.json new file mode 100644 index 000000000..0c198e6f5 --- /dev/null +++ b/apps/omnitrix/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "omnitrix", + "name": "Omnitrix", + "version": "0.01", + "description": "An Omnitrix Showpiece", + "icon": "omnitrix.png", + "screenshots": [{"url":"screenshot.png"}], + "tags": "game", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"omnitrix.app.js","url":"omnitrix.app.js"}, + {"name":"omnitrix.img","url":"omnitrix.icon.js","evaluate":true} + ] +} diff --git a/apps/openloc/metadata.json b/apps/openloc/metadata.json new file mode 100644 index 000000000..e3043eb8d --- /dev/null +++ b/apps/openloc/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "openloc", + "name": "Open Location / Plus Codes", + "shortName": "Open Location", + "version": "0.01", + "description": "Convert your current GPS location to a series of characters", + "icon": "app.png", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"openloc.app.js","url":"app.js"}, + {"name":"openloc.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/openseizure/metadata.json b/apps/openseizure/metadata.json new file mode 100644 index 000000000..d884c48b3 --- /dev/null +++ b/apps/openseizure/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "openseizure", + "name": "OpenSeizureDetector Widget", + "shortName": "Short Name", + "version": "0.01", + "description": "[BETA!] A widget to work alongside [OpenSeizureDetector](https://www.openseizuredetector.org.uk/)", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"openseizure.wid.js","url":"widget.js"} + ] +} 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 95a4ff0a6..302f9a84a 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -2,6 +2,7 @@ + + + +
+

Presentor

+
+
+
+
+
+
#
+
Subject
+
Time
+
Notes
+
+
Loading...
+
+
+
+
+ + +
+ + + + + + diff --git a/apps/presentor/metadata.json b/apps/presentor/metadata.json new file mode 100644 index 000000000..e5b5e289f --- /dev/null +++ b/apps/presentor/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "presentor", + "name": "Presentor", + "version": "0.08", + "description": "Use your Bangle to present!", + "icon": "app.png", + "type": "app", + "tags": "tool,bluetooth", + "interface": "interface.html", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"presentor.app.js","url":"app.js"}, + {"name":"presentor.img","url":"app-icon.js","evaluate":true}, + {"name":"presentor.json","url":"settings.json"} + ] +} diff --git a/apps/presentor/settings.json b/apps/presentor/settings.json new file mode 100644 index 000000000..398bf1332 --- /dev/null +++ b/apps/presentor/settings.json @@ -0,0 +1 @@ +{"pparts":[{"subject":"#1","minutes":10,"seconds":0,"notes":"This is a note."},{"subject":"#2","minutes":2,"seconds":50,"notes":"Change in the app!"}],"sversion":2.2} \ No newline at end of file diff --git a/apps/promenu/ChangeLog b/apps/promenu/ChangeLog new file mode 100644 index 000000000..b7287cc80 --- /dev/null +++ b/apps/promenu/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Add Bangle.js 2 Support diff --git a/apps/promenu/README.md b/apps/promenu/README.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/promenu/README.md @@ -0,0 +1 @@ + diff --git a/apps/promenu/boot.js b/apps/promenu/boot.js new file mode 100644 index 000000000..bd813a812 --- /dev/null +++ b/apps/promenu/boot.js @@ -0,0 +1,176 @@ +E.showMenu = function(items) { + function RectRnd(x1,y1,x2,y2,r) { + pp = []; + pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r])); + pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2])); + pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r])); + pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1])); + return pp; + } + function fillRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.fillPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + function drawRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.drawPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + g.reset().clearRect(Bangle.appRect); // clear if no menu supplied + Bangle.setLCDPower(1); // ensure screen is on + if (!items) { + Bangle.setUI(); + return; + } + var menuItems = Object.keys(items); + var options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + options.fontHeight = options.fontHeight||35; + if (options.selected === undefined) + options.selected = 0; + if (!options.fontHeight) + options.fontHeight = 6; + var ar = Bangle.appRect; + var x = ar.x; + var x2 = ar.x2-11; // padding at side for up/down + var y = ar.y; + var y2 = ar.y2 - 20; // padding at end for arrow + if (options.title) + y += options.fontHeight+2; + var loc = require("locale"); + var l = { + lastIdx : 0, + draw : function(rowmin,rowmax) { + var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length); + var idx = E.clip(options.selected-(rows>>1),0,menuItems.length-rows); + if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled + l.lastIdx = idx; + var iy = y-5; + g.reset().setFont('6x8',2).setFontAlign(0,-1,0); + if (options.predraw) options.predraw(g); + if (rowmin===undefined && options.title) { + g.drawString(options.title,(x+x2)/2,y-options.fontHeight+5); + g.drawLine(x,y-7,x2,y-7); + } + if (rowmin!==undefined) { + if (idxrowmax) { + rows = 1+rowmax-rowmin; + } + } + while (rows--) { + var name = menuItems[idx]; + var item = items[name]; + var hl = (idx==options.selected && !l.selectEdit); + if(g.theme.dark){ + fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+20); + }else{ + fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); + } + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign(-1,-1); + if(loc.translate(name).length >= 11 && "object" == typeof item){ + var v = item.value; + if (item.format) v=item.format(v); + v = loc.translate(""+v); + g.drawString(loc.translate(name).substring(0, 15-v.length)+"...",x+8,iy+7); + }else{ + if(loc.translate(name).length >= 15){ + g.drawString(loc.translate(name).substring(0, 15)+"...",x+8,iy+7); + }else{ + g.drawString(loc.translate(name),x+8,iy+7); + } + } + if ("object" == typeof item) { + var xo = x2; + var v = item.value; + if (item.format) v=item.format(v); + v = loc.translate(""+v); + if (l.selectEdit && idx==options.selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2}); + } + g.setFontAlign(1,-1); + g.drawString(v,xo-4,iy+8.5); + } + g.setColor(g.theme.fg); + iy += options.fontHeight; + idx++; + } + g.setFontAlign(-1,-1); + var more = idxitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) item.onchange(item.value); + l.draw(options.selected,options.selected); + } else { + var lastSelected=options.selected; + options.selected = (dir+options.selected+menuItems.length)%menuItems.length; + l.draw(Math.min(lastSelected,options.selected), Math.max(lastSelected,options.selected)); + } + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + return l; +}; diff --git a/apps/promenu/bootb2.js b/apps/promenu/bootb2.js new file mode 100644 index 000000000..c84e0d894 --- /dev/null +++ b/apps/promenu/bootb2.js @@ -0,0 +1,142 @@ +E.showMenu = function(items) { + function RectRnd(x1,y1,x2,y2,r) { + pp = []; + pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r])); + pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2])); + pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r])); + pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1])); + return pp; + } + function fillRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.fillPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + function drawRectRnd(x1,y1,x2,y2,r,c) { + g.setColor(c); + g.drawPoly(RectRnd(x1,y1,x2,y2,r),1); + g.setColor(255,255,255); + } + g.reset().clearRect(Bangle.appRect); // clear if no menu supplied + Bangle.setLCDPower(1); // ensure screen is on + if (!items) { + Bangle.setUI(); + return; + } + var menuItems = Object.keys(items); + var options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + options.fontHeight = options.fontHeight||25; + if (options.selected === undefined) + options.selected = 0; + var ar = Bangle.appRect; + var x = ar.x; + var x2 = ar.x2; + var y = ar.y; + var y2 = ar.y2 - 12; // padding at end for arrow + if (options.title) + y += 22; + var loc = require("locale"); + var l = { + lastIdx : 0, + draw : function(rowmin,rowmax) { + var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length); + var idx = E.clip(options.selected-( rows>>1),0,menuItems.length-rows); + if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled + l.lastIdx = idx; + var iy = y; + g.reset().setFontAlign(0,-1,0).setFont('12x20'); + if (options.predraw) options.predraw(g); + if (rowmin===undefined && options.title) + g.drawString(options.title,(x+x2)/2,y-21).drawLine(x,y-2,x2,y-2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 4; + if (rowmin!==undefined) { + if (idxrowmax) { + rows = 1+rowmax-rowmin; + } + } + while (rows--) { + var name = menuItems[idx]; + var item = items[name]; + var hl = (idx==options.selected && !l.selectEdit); + if(g.theme.dark){ + fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+40); + }else{ + fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20); + } + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign(-1,-1); + var v = item.value; + v = loc.translate(""+v); + if(loc.translate(name).length >= 17-v.length && "object" == typeof item){ + if (item.format) v=item.format(v); + g.drawString(loc.translate(name).substring(0, 12-v.length)+"...",x+3.7,iy+2.7); + }else{ + if(loc.translate(name).length >= 15){ + g.drawString(loc.translate(name).substring(0, 15)+"...",x+3.7,iy+2.7); + }else{ + g.drawString(loc.translate(name),x+3.7,iy+2.7); + } + } + if ("object" == typeof item) { + var xo = x2; + var v = item.value; + if (item.format) v=item.format(v); + v = loc.translate(""+v); + if (l.selectEdit && idx==options.selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2}); + } + g.setFontAlign(1,-1); + g.drawString(v,xo-2,iy+1); + } + g.setColor(g.theme.fg); + iy += options.fontHeight; + idx++; + } + g.setFontAlign(-1,-1); + g.setColor((idxitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) item.onchange(item.value); + l.draw(options.selected,options.selected); + } else { + var lastSelected=options.selected; + options.selected = (dir+options.selected+menuItems.length)%menuItems.length; + l.draw(Math.min(lastSelected,options.selected), Math.max(lastSelected,options.selected)); + } + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + return l; +}; diff --git a/apps/promenu/icon.png b/apps/promenu/icon.png new file mode 100644 index 000000000..c6138b765 Binary files /dev/null and b/apps/promenu/icon.png differ diff --git a/apps/promenu/metadata.json b/apps/promenu/metadata.json new file mode 100644 index 000000000..d70f36b0a --- /dev/null +++ b/apps/promenu/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "promenu", + "name": "Pro Menu", + "version": "0.02", + "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", + "icon": "icon.png", + "type": "boot", + "tags": "system", + "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"pro-menu-screenshot.png"}], + "storage": [ + {"name":"promenu.boot.js","url":"boot.js","supports": ["BANGLEJS"]}, + {"name":"promenu.boot.js","url":"bootb2.js","supports": ["BANGLEJS2"]}, + {"name":"promenu.img","url":"promenuIcon.js","evaluate":true} + ] +} diff --git a/apps/promenu/pro-menu-screenshot.png b/apps/promenu/pro-menu-screenshot.png new file mode 100644 index 000000000..2ae318eb8 Binary files /dev/null and b/apps/promenu/pro-menu-screenshot.png differ diff --git a/apps/promenu/promenuIcon.js b/apps/promenu/promenuIcon.js new file mode 100644 index 000000000..5c2dc6adc --- /dev/null +++ b/apps/promenu/promenuIcon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwyBC/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFE+rwBSAAU2j02js1/oBC7oBBlu9AIWdluclt8AIMlrgBCngBCjgBBjn7AIXbAIW7AIMUzYBFLv5ddMJ5d/LqJfeLv5fdLv5fQMJhd/LqQA/AH4A/AH4A/AH4A/AH4A/AAM+rwBSAAU2j02js1/oBC7oBBlu9AIWdluclt8AIMlrgBCngBCjgBBjn7AIXbAIW7AIMUzYBFLv5ddMJ5d/LqJfeLv5fdLv5fQMJhd/LqQA/AH4A/AH4A/AH4A/AH4A/AAM+rwBSAAU2j02js1/oBC7oBBlu9AIWdluclt8AIMlrgBCngBCjgBBjn7AIXbAIW7AIMUzYBFLv5ddMJ5d/LqJfeLv5fdLv5fQMJhd/LqQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ADIA==")) diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog new file mode 100644 index 000000000..68b7d3e1c --- /dev/null +++ b/apps/ptlaunch/ChangeLog @@ -0,0 +1,7 @@ +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. +0.11: Respect theme colors. Fix: Do not pollute global space with internal variables ans functions in boot.js +0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/ +0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/ \ No newline at end of file diff --git a/apps/ptlaunch/README.md b/apps/ptlaunch/README.md new file mode 100644 index 000000000..12c205980 --- /dev/null +++ b/apps/ptlaunch/README.md @@ -0,0 +1,90 @@ +# Pattern Launcher + +Directly launch apps from the clock screen with custom patterns. + +## Installation and Usage + +Install Pattern Launcher alongside your main laucher app. +_Do not delete that launcher!_ +Pattern Launcher is designed as an additional app launching utility, not as a replacement for the main launcher. + +In the main launcher, start Pattern Launcher in the app menu to assign the pattern configuration (see below). +Note that this actually among the applications, _not_ in the application settings! + +Create patterns and link them to apps in the Pattern Launcher app. + +Then launch the linked apps directly from the clock screen by simply drawing the desired pattern. +Note that this does only work in the clock screen, not if other applications run. + +## Add Pattern Screenshots + +![](main_menu_add_light.png) +![](add_pattern_light.png) +![](select_app_light.png) + +![](main_menu_add_dark.png) +![](add_pattern_dark.png) +![](select_app_dark.png) + +## Manage Pattern Screenshots + +![](main_menu_manage_light.png) +![](manage_patterns_light.png) + +![](main_menu_manage_dark.png) +![](manage_patterns_dark.png) + +## Detailed Steps + +The main menu of Pattern Launcher is accessible from the _application_ starter of the main launcher. +From there you can: + +- Add a new pattern and link it to an app (first entry) + - To create a new pattern first select "Add Pattern" + - 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 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! + +4. Where can I configure the patterns? + +You have to start the "Pattern Launcher" app from the main app launcher's app selection. + +5. Do I have to delete my former app launcher so that Pattern Launcher is the only installed launcher? + +No! Pattern Launcher works alongside another "main" launcher. +If you have deleted that one, you do not have a general purpose app launcher any more and cannot access Pattern Launcher's configuration. +If you already have deleted your main launcher accidentially, just reinstall it from the app loader. + +## Authors + +Initial creation: [crazysaem](https://github.com/crazysaem) + +Improve pattern detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/) + +Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/) + +Doc additions: [dirkhillbrecht](http://forum.espruino.com/profiles/182498/) diff --git a/apps/ptlaunch/add_pattern_dark.png b/apps/ptlaunch/add_pattern_dark.png new file mode 100644 index 000000000..4d5770835 Binary files /dev/null and b/apps/ptlaunch/add_pattern_dark.png differ diff --git a/apps/ptlaunch/add_pattern_light.png b/apps/ptlaunch/add_pattern_light.png new file mode 100644 index 000000000..998ec21a0 Binary files /dev/null and b/apps/ptlaunch/add_pattern_light.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..5db3a335b --- /dev/null +++ b/apps/ptlaunch/app.js @@ -0,0 +1,488 @@ +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 recognizeAndDrawPattern = () => { + return new Promise((resolve) => { + E.showMenu(); + g.clear(); + drawCirclesWithPattern([]); + + 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); + + var positions = []; + var getPattern = (positions) => { + 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 }, + ]; + return positions.reduce((pattern, p, i, arr) => { + var idx = circles.findIndex((c) => { + var dx = p.x > c.x ? p.x - c.x : c.x - p.x; + if (dx > CIRCLE_RADIUS) { + return false; + } + var dy = p.y > c.y ? p.y - c.y : c.y - p.y; + if (dy > CIRCLE_RADIUS) { + return false; + } + if (dx + dy <= CIRCLE_RADIUS) { + return true; + } + return dx * dx + dy * dy <= CIRCLE_RADIUS_2; + }); + if (idx >= 0) { + pattern += circles[idx].i; + circles.splice(idx, 1); + } + if (circles.length === 0) { + arr.splice(1); + } + return pattern; + }, ""); + }; + var dragHandler = (position) => { + positions.push(position); + if (position.b === 0 || positions.length >= 200) { + pattern = getPattern(positions).split(""); + g.clear(); + drawCirclesWithPattern(pattern); + positions = []; + } + }; + 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 }, + }); + + 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.setColor(0); + 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); + drawBuffer.fillRect(0, 0, drawBuffer.getWidth(), drawBuffer.getHeight()); + + CIRCLES.forEach((circle) => drawCircle(circle, drawBuffer, scale)); + drawBuffer.setColor(1); + drawBuffer.setFontAlign(0, 0); + drawBuffer.setFont("Vector", 40 * scale); + pattern.forEach((circleIndex, patternIndex) => { + var circle = CIRCLES[circleIndex]; + drawBuffer.drawString( + patternIndex + 1, + (circle.x + (scale === 1 ? 1 : 5)) * scale, + circle.y * scale + ); + }); + image = { + width: drawBuffer.getWidth(), + height: drawBuffer.getHeight(), + bpp: 1, + buffer: drawBuffer.buffer, + palette: new Uint16Array([g.theme.fg, g.theme.bg], 0, 1), + }; + + if (enableCaching) { + cachedCirclesDrawings[pattern.join("")] = image; + } + } else { + log("using cached circle image"); + } + + g.drawImage(image, offset.x, offset.y); +}; + +////// +// misc lib functions +////// + +var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } +}; + +////// +// 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..19a8f16cb --- /dev/null +++ b/apps/ptlaunch/boot.js @@ -0,0 +1,101 @@ +(function () { + var DEBUG = false; + var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } + }; + + var storedPatterns; + var CIRCLE_RADIUS = 25; + var CIRCLE_RADIUS_2 = Math.pow(CIRCLE_RADIUS, 2); + var positions = []; + var getPattern = (positions) => { + 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 }, + ]; + return positions.reduce((pattern, p, i, arr) => { + var idx = circles.findIndex((c) => { + var dx = p.x > c.x ? p.x - c.x : c.x - p.x; + if (dx > CIRCLE_RADIUS) { + return false; + } + var dy = p.y > c.y ? p.y - c.y : c.y - p.y; + if (dy > CIRCLE_RADIUS) { + return false; + } + if (dx + dy <= CIRCLE_RADIUS) { + return true; + } + return dx * dx + dy * dy <= CIRCLE_RADIUS_2; + }); + if (idx >= 0) { + pattern += circles[idx].i; + circles.splice(idx, 1); + } + if (circles.length === 0) { + arr.splice(1); + } + return pattern; + }, ""); + }; + var dragHandler = (position) => { + positions.push(position); + if (position.b === 0 || positions.length >= 200) { + var pattern = getPattern(positions); + log(pattern); + + 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); + } + } + } + + positions = []; + } + }; + + 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_dark.png b/apps/ptlaunch/main_menu_add_dark.png new file mode 100644 index 000000000..4c1564eca Binary files /dev/null and b/apps/ptlaunch/main_menu_add_dark.png differ diff --git a/apps/ptlaunch/main_menu_add_light.png b/apps/ptlaunch/main_menu_add_light.png new file mode 100644 index 000000000..e9a5c52a9 Binary files /dev/null and b/apps/ptlaunch/main_menu_add_light.png differ diff --git a/apps/ptlaunch/main_menu_manage_dark.png b/apps/ptlaunch/main_menu_manage_dark.png new file mode 100644 index 000000000..e630db46d Binary files /dev/null and b/apps/ptlaunch/main_menu_manage_dark.png differ diff --git a/apps/ptlaunch/main_menu_manage_light.png b/apps/ptlaunch/main_menu_manage_light.png new file mode 100644 index 000000000..a6aee1427 Binary files /dev/null and b/apps/ptlaunch/main_menu_manage_light.png differ diff --git a/apps/ptlaunch/manage_patterns_dark.png b/apps/ptlaunch/manage_patterns_dark.png new file mode 100644 index 000000000..c502d23fe Binary files /dev/null and b/apps/ptlaunch/manage_patterns_dark.png differ diff --git a/apps/ptlaunch/manage_patterns_light.png b/apps/ptlaunch/manage_patterns_light.png new file mode 100644 index 000000000..13470f450 Binary files /dev/null and b/apps/ptlaunch/manage_patterns_light.png differ diff --git a/apps/ptlaunch/metadata.json b/apps/ptlaunch/metadata.json new file mode 100644 index 000000000..6c3870d24 --- /dev/null +++ b/apps/ptlaunch/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "ptlaunch", + "name": "Pattern Launcher", + "shortName": "Pattern Launcher", + "version": "0.13", + "description": "Directly launch apps from the clock screen with custom patterns.", + "icon": "app.png", + "screenshots": [{"url":"manage_patterns_light.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"}] +} diff --git a/apps/ptlaunch/select_app_dark.png b/apps/ptlaunch/select_app_dark.png new file mode 100644 index 000000000..422f2b5e2 Binary files /dev/null and b/apps/ptlaunch/select_app_dark.png differ diff --git a/apps/ptlaunch/select_app_light.png b/apps/ptlaunch/select_app_light.png new file mode 100644 index 000000000..56f0dfc83 Binary files /dev/null and b/apps/ptlaunch/select_app_light.png differ diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog new file mode 100644 index 000000000..0950b7ae0 --- /dev/null +++ b/apps/puzzle15/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version, UI mechanics ready, no real game play so far +0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... +0.03: Menu logic now generally functioning, splash screen added. The first really playable version! +0.04: Settings dialog, about screen +0.05: Central game end function diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md new file mode 100644 index 000000000..16c0c4593 --- /dev/null +++ b/apps/puzzle15/README.md @@ -0,0 +1,57 @@ +# Puzzle15 - A 15-puzzle for the Bangle.js 2 + +This is a Bangle.js 2 adoption of the famous 15 puzzle. + +## The game + +A board of _n_ by _n_ fields is filled with _n^2-1_ numbered stones. So, one field, the "gap", is left free. + +Bring them in the correct order so that the gap is finally at the bottom right of the playing field. +The less moves you need, the better you are. + +If _n_ is 4, the number of stones is _16-1=15_. Hence the name of the game. + +## How to play + +If you start the game, it shows a splash screen and then generates a shuffled 4x4 board with a 15 puzzle. +Move the stones with drag gestures on the screen. +If you want to move the stone below the gap upward, drag from the bottom of the screen upward. +The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved. + +If you managed to order the stones correctly, a success message appears. +You can continue with another game, go to the game's main menu, or quit the game entirely. + +There is a grey menu button right of the board containing the well-known three-bar menu symbol ("Hamburger menu"). +It opens the game's main menu directly from within the game. + +## The main menu + +Puzzle15 has a main menu which can be reached from the in-game menu button or the end-of-game message window. +It features the following options: + +* **Continue** - Continue the currently running game. _This option is only shown if the main menu is opened during an open game._ +* **Start 3x3**, **Start 4x4**, **Start 5x5** - Start a new game on a board with the respective dimension. Any currently open game is dropped. +* **About** Show a small "About" info box. +* **Exit** Exit Puzzle15 and return to the default watch face. + +## Game settings + +The game has some global settings which can be accessed on the usual way through the Bangle.js' app settings user interface. +Currently it has the following options: + +* **Splash** - Define whether the game should open with a splash screen. **long** shows the splash screen for five seconds, **short** shows it for two seconds. **off** starts the app _without_ a splash screen, it directly comes up with whatever the "Start with" option says. +* **Start with** - What should happen after the splash screen (or, if it is disabled, directly at app start): **3x3**, **4x4** and **5x5** start the game with a board of the respective dimension, **menu** shows the main menu which allows to select the board size. + +## Implementation notes + +The game engine always generates puzzles which can be solved. + +Solvability is detected by counting inversions, +i.e. pairs of stones where the stone at the earlier field (row-wise, left to right, top to bottom) has a number _greater than_ the stone on the later field, with all pairs of stones compared. +The algorithm is described at https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ . + +## The splash screen + +The Splash screen shows a part of the illustration "The 14-15-puzzle in puzzleland" from Sam Loyd. Other than Puzzle15, it depicts a 15 puzzle with the stones "14" and "15" swapped. This puzzle is indeed *not* solvable. + +Have fun! diff --git a/apps/puzzle15/introscreen.png b/apps/puzzle15/introscreen.png new file mode 100644 index 000000000..766cd61fa Binary files /dev/null and b/apps/puzzle15/introscreen.png differ diff --git a/apps/puzzle15/metadata.json b/apps/puzzle15/metadata.json new file mode 100644 index 000000000..b895e0e12 --- /dev/null +++ b/apps/puzzle15/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "puzzle15", + "name": "15 puzzle", + "version": "0.05", + "description": "A 15 puzzle game with drag gesture interface", + "readme":"README.md", + "icon": "puzzle15.app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"puzzle15.app.js","url":"puzzle15.app.js"}, + {"name":"puzzle15.settings.js","url":"puzzle15.settings.js"}, + {"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true} + ], + "data": [{"name":"puzzle15.json"}] +} diff --git a/apps/puzzle15/puzzle15.app-icon.js b/apps/puzzle15/puzzle15.app-icon.js new file mode 100644 index 000000000..04fb4a665 --- /dev/null +++ b/apps/puzzle15/puzzle15.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AC3+7oAD7e7AAW8BQndBQe79/9DomgHocH74KD/RJE34Xax4XDtvoC4fJ54XDluAC4f2z4XDzm/C4ett4XD34OBF4e/I4m+C4f8r4XChHuC5U98oXEF4cP7/AC5O9mYXC/2/F4cGtwvE/SsBC4Ws7gvD7YCBL4ULO4i/u1QAD7QED1e6AoetCAnf/YeE1wpD/lgBQcKIAgXG14LD/twC5kL3Z+BC4P+LgIXBg272wXD7wXEh7eCC4PWzIXChHtOoIXB/WX54XDh3KmAXC1oLBI4UD+AXC+/rdIIvD5wvD3O4C4cJ4AXC/dUI4kJhgMBC4Ov+AXDh9QC4X2/gvEhvvoAXC81dC4duR4f8wSncC6v8u4AD3ndAAXcy4KDtYKD7vf/oGE2wRDvPNBQfLFAnP/o2EVIIACg7yBAATZBAAe/C7P9g4XCx+wn/6C4Op//AC4MK+cI/+QC4X2/fPC4PM2HKh8H7vpewIXBhvThV5+AXC+/5C4UL2HHC4Pf/P/AIJHB6cAj2wC4X+3AXPhADBF4fX94XB1va1vOC4PXAIX6hfrxvb0CPD7p3C1e6hW2C4LOBAIIXB3eJ3YXEX78GM4IAC9QXG1QAD7QEDJYIFD14oE//7DwgME/twBQcPC70G6EG5dQ1/8VYPtC4ObgfM5IXHr/whvO4Gvy6LBtX9vfugnr3AXHkXggGOC4P97/43X9ukOgnv6BfIC4Oe2AXC6+nI4MOgfI9QXJhssF4f91AXCgnA9IXHr3u1HusGv3Ob//s/t693l3xHJX9v+3YAD7oAE5YKD34XFAC4=")) diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js new file mode 100644 index 000000000..eec21933c --- /dev/null +++ b/apps/puzzle15/puzzle15.app.js @@ -0,0 +1,771 @@ +// A 15-puzzle game for the Bangle.js 2 clock +// (C) Dirk Hillbrecht 2022 +// Released unter the terms of the MIT license + +// The intro screen as large base-64-encoded binary data +const introscreen = E.toArrayBuffer(atob("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")); + +// *** Global constants from which several other settings are derived + +// Minimum number of pixels to interpret it as drag gesture +const dragThreshold = 10; + +// Maximum number of pixels to interpret a click from a drag event series +const clickThreshold = 3; + +// Number of steps in stone move animation +const animationSteps = 6; + +// Milliseconds to wait between move animation steps +const animationWaitMillis = 30; + +// Total width of the playing field (full screen width) +const fieldw = g.getWidth(); + +// Total height of the playing field (screen height minus widget zones) +const fieldh = g.getHeight() - 48; + + +// *** Global game characteristics + +// Size of the playing field +var stonesPerLine; + +// Size of one field +var stonesize; + +// Actual left start of the playing field (so that it is centered) +var leftstart; + +// Actual top start of the playing field (so that it is centered) +var topstart; + +// Number of stones on the board (needed at several occasions) +var stonesPerBoard; + +// Set the stones per line globally and all derived values, too +function setStonesPreLine(bPL) { + stonesPerLine = bPL; + stonesize = Math.floor(Math.min(fieldw / (stonesPerLine + 1), fieldh / stonesPerLine)) - 2; + leftstart = (fieldw - ((stonesPerLine + 1) * stonesize + 8)) / 2; + topstart = 24 + ((fieldh - (stonesPerLine * stonesize + 6)) / 2); + stonesPerBoard = (stonesPerLine * stonesPerLine); +} + + +// *** Global app settings + +var SETTINGSFILE = "puzzle15.json"; + +// variables defined from settings +var splashMode; +var startWith; + +/* For development purposes +require('Storage').writeJSON(SETTINGSFILE, { + splashMode: "off", + startWith: "5x5", +}); +/* */ + +/* OR (also for development purposes) +require('Storage').erase(SETTINGSFILE); +/* */ + +// Helper method for loading the settings +function def(value, def) { + return (value !== undefined ? value : def); +} + +// Load settings +function loadSettings() { + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + splashMode = def(settings.splashMode, "long"); + startWith = def(settings.startWith, "4x4"); +} + + +// *** Low level helper classes + +// One node of a first-in-first-out storage +class FifoNode { + constructor(payload) { + this.payload = payload; + this.next = null; + } +} + +// Simple first-in-first-out (fifo) storage +// Needed to keep the stone movements in order +class Fifo { + + // Initialize an empty Fifo + constructor() { + this.first = null; + this.last = null; + } + + // Add an element to the end of the internal fifo queue + add(payload) { + if (this.last === null) { // queue is empty + this.first = new FifoNode(payload); + this.last = this.first; + } else { + let newlast = new FifoNode(payload); + this.last.next = newlast; + this.last = newlast; + } + } + + // Returns the first element in the queue, null if it is empty + remove() { + if (this.first === null) + return null; + let oldfirst = this.first; + this.first = this.first.next; + if (this.first === null) + this.last = null; + return oldfirst.payload; + } + + // Returns if the fifo is empty, i.e. it does not hold any elements + isEmpty() { + return (this.first === null); + } + +} + +// Helper class to keep track of tasks +// Executes tasks given by addTask. +// Tasks must call Worker.endTask() when they are finished, for this they get the worker passed as parameter. +// If a task is given with addTask() while another task is still running, +// it is queued and executed once the currently running task and all +// previously scheduled tasks have finished. +// Tasks must be functions with the Worker as first and only parameter. +class Worker { + + // Create an empty worker + constructor() { + this.tasks = new Fifo(); + this.busy = false; + } + + // Add a task to the worker + addTask(task) { + if (this.busy) // other task is running: Queue this task + this.tasks.add(task); + else { // No other task is running: Execute directly + this.busy = true; + task(this); + } + } + + // Called by the task once it finished + endTask() { + if (this.tasks.isEmpty()) // No more tasks queued: Become idle + this.busy = false; + else // Call the next task immediately + this.tasks.remove()(this); + } + +} + +// Evaluate "drag" events from the UI and call handlers for drags or clicks +// The UI sends a drag as a series of events indicating partial movements +// of the finger. +// This class combines such parts to a long drag from start to end +// If the drag is short, it is interpreted as click, +// otherwise as drag. +// The approprate method is called with the data of the drag. +class Dragger { + + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { + this.clickHandler = clickHandler; + this.dragHandler = dragHandler; + this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold); + this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold); + this.dx = 0; + this.dy = 0; + this.enabled = true; + } + + // Enable or disable the Dragger + setEnabled(b) { + this.enabled = b; + } + + // Handle a raw drag event from the UI + handleRawDrag(e) { + if (!this.enabled) + return; + this.dx += e.dx; // Always accumulate + this.dy += e.dy; + if (e.b === 0) { // Drag event ended: Evaluate full drag + if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold) + this.clickHandler({ + x: e.x - this.dx, + y: e.y - this.dy + }); // take x and y from the drag start + else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold) + this.dragHandler({ + x: e.x - this.dx, + y: e.y - this.dy, + dx: this.dx, + dy: this.dy + }); + this.dx = 0; // Clear the drag accumulator + this.dy = 0; + } + } + + // Attach the drag evaluator to the UI + attach() { + Bangle.on("drag", e => this.handleRawDrag(e)); + } +} + +// *** Mid-level game mechanics + +// Representation of a position where a stone is set. +// Stones can be moved from field to field. +// The playing field consists of a fixed set of fields forming a square. +// During an animation, a series of interim field instances is generated +// which represents the locations of a stone during the animation. +class Field { + + // Generate a field with a left and a top coordinate. + // Note that these coordinates are "cooked", i.e. they contain all offsets + // needed place the elements globally correct on the screen + constructor(left, top) { + this.left = left; + this.top = top; + this.centerx = (left + stonesize / 2) + 1; + this.centery = (top + stonesize / 2) + 2; + } + + // Returns whether this field contains the given coordinate + contains(x, y) { + return (this.left < x && this.left + stonesize > x && + this.top < y && this.top + stonesize > y); + } + + // Generate a field for the given playing field index. + // Playing field indexes start at top left with "0" + // and go from left to right line by line from top to bottom. + static forIndex(index) { + return new Field(leftstart + (index % stonesPerLine) * (stonesize + 2), + topstart + (Math.floor(index / stonesPerLine)) * (stonesize + 2)); + + } + // Special field for the result "stone" + static forResult() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart + ((stonesPerLine - 1) * (stonesize + 2))); + } + + // Special field for the menu + static forMenu() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart); + } + +} + +// Representation of a moveable stone of the game. +// Stones are moved from field to field to solve the puzzle +// Stones are numbered from 0 to the maximum number ot stones. +// Stone "0" represents the gap on the playing field. +// The main knowledge of a Stone instance is how to draw itself. +class Stone { + + // Create stone with the given number + // The constructor creates the "draw()" function which is used to draw the stone + constructor(number, targetindex) { + this.number = number; + this.targetindex = targetindex; + // gap: Does not draw anything + if (number === 0) + this.draw = function(field) {}; + else if ((number + (stonesPerLine % 2 == 0 ? (Math.floor((number - 1) / stonesPerLine)) : 0)) % 2 == 0) { + // Black stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(1, 1, 1).drawString(number, field.centerx, field.centery); + }; + } else { + // White stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.drawString(number, field.centerx, field.centery); + }; + } + } + + // Returns whether this stone is on its target index + isOnTarget(index) { + return index === this.targetindex; + } +} + +// Helper class which knows how to clear the rectangle opened up by the two given fields +class Clearer { + + // Create a clearer for the area between the two given fields + constructor(startfield, endfield) { + this.minleft = Math.min(startfield.left, endfield.left); + this.mintop = Math.min(startfield.top, endfield.top); + this.maxleft = Math.max(startfield.left, endfield.left); + this.maxtop = Math.max(startfield.top, endfield.top); + } + + // Clear the area defined by this clearer + clearArea() { + g.setColor(1, 1, 1); + g.fillRect(this.minleft, this.mintop, + this.maxleft + stonesize, this.maxtop + stonesize); + } + +} + +// Helper class which moves a stone between two fields +class Mover extends Clearer { + + // Create a mover which moves the given stone from startfield to endfield + // and animate the move in the given number of steps + constructor(stone, startfield, endfield, steps) { + super(startfield, endfield); + this.stone = stone; + this.startfield = startfield; + this.endfield = endfield; + this.steps = steps; + } + + // Create the coordinate between start and end for the given step + // Computation uses sinus for a smooth movement + stepCoo(start, end, step) { + return start + ((end - start) * ((1 + Math.sin((step / this.steps) * Math.PI - (Math.PI / 2))) / 2)); + } + + // Compute the interim field for the stone to place during the animation + stepField(step) { + return new Field( + (this.minleft === this.maxleft ? this.minleft : + this.stepCoo(this.startfield.left, this.endfield.left, step)), + (this.mintop === this.maxtop ? this.mintop : + this.stepCoo(this.startfield.top, this.endfield.top, step))); + } + + // Perform one animation step + animateStep(step, worker) { + this.clearArea(); + this.stone.draw(this.stepField(step)); + if (step < this.steps) // still steps left: Issue next step + setTimeout(function(t) { + t.animateStep(step + 1, worker); + }, animationWaitMillis, this); + else // all steps done: Inform the worker + worker.endTask(); + } + + // Start the animation, this method is called by the worker + animate(worker) { + this.animateStep(1, worker); + } + +} + +// Representation of the playing field +// Knows to draw the field and to move a stone into a gap +class Board { + + // Generates the actual playing field with all fields and stones + constructor() { + this.fields = []; + this.resultField = Field.forResult(); + this.menuField = Field.forMenu(); + for (i = 0; i < stonesPerBoard; i++) + this.fields[i] = Field.forIndex(i); + this.setShuffled(); + //this.setAlmostSolved(); // to test the game end + } + + /* Set the board into the "solved" position. Useful for showcasing and development + setSolved() { + this.stones = []; + for (i = 0; i < stonesPerBoard; i++) + this.stones[i] = new Stone((i + 1) % stonesPerBoard, i); + this.moveCount = 0; + } + /* */ + + /* Initialize an almost solved playing field. Useful for tests and development + setAlmostSolved() { + this.setSolved(); + b = this.stones[this.stones.length - 1]; + this.stones[this.stones.length - 1] = this.stones[this.stones.length - 2]; + this.stones[this.stones.length - 2] = b; + } + /* */ + + // Initialize a shuffled field. The fields are always solvable. + setShuffled() { + let nrs = []; // numbers of the stones + for (i = 0; i < stonesPerBoard; i++) + nrs[i] = i; + this.stones = []; + let count = stonesPerBoard; + for (i = 0; i < stonesPerBoard; i++) { + // Take a random number of the (remaining) numbers + let curridx = Math.floor(Math.random() * count); + let currnr = nrs[curridx]; + // Initialize the next stone with that random number + this.stones[i] = new Stone(currnr, (currnr + (stonesPerBoard - 1)) % stonesPerBoard); + // Remove the number just taken from the list of numbers + for (j = curridx + 1; j < count; j++) + nrs[j - 1] = nrs[j]; + count -= 1; + } + // not solvable: Swap the first and second stone which are not the gap. + // This will always result in a solvable board. + if (!this.isSolvable()) { + let a = (this.stones[0].number === 0 ? 2 : 0); + let b = (this.stones[1].number === 0 ? 2 : 1); + let bx = this.stones[a]; + this.stones[a] = this.stones[b]; + this.stones[b] = bx; + } + this.moveCount = 0; + } + + // Draws the complete playing field + draw() { + new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); + for (i = 0; i < this.fields.length; i++) + this.stones[i].draw(this.fields[i]); + this.drawResult(null); + this.drawMenu(); + } + + // returns the index of the field left of the field with the given index, + // -1 if there is none (index indicates already a leftmost field on the board) + leftOf(index) { + return (index % stonesPerLine === 0 ? -1 : index - 1); + } + + // returns the index of the field right of the field with the given index, + // -1 if there is none (index indicates already a rightmost field on the board) + rightOf(index) { + return (index % stonesPerLine === (stonesPerLine - 1) ? -1 : index + 1); + } + + // returns the index of the field top of the field with the given index, + // -1 if there is none (index indicates already a topmost field on the board) + topOf(index) { + return (index >= stonesPerLine ? index - stonesPerLine : -1); + } + + // returns the index of the field bottom of the field with the given index, + // -1 if there is none (index indicates already a bottommost field on the board) + bottomOf(index) { + return (index < (stonesPerLine - 1) * stonesPerLine ? index + stonesPerLine : -1); + } + + // Return the index of the gap in the field, -1 if there is none (should never happel) + indexOf0() { + for (i = 0; i < this.stones.length; i++) + if (this.stones[i].number === 0) + return i; + return -1; + } + + // Returns the row in which the gap is, 0 is upmost + rowOf0() { + let idx = this.indexOf0(); + if (idx < 0) + return -1; + return Math.floor(idx / stonesPerLine); + } + + // Searches the gap on the field and then moves one of the adjacent stones into it. + // The stone is selected by the given startfunc which returns the index + // of the selected adjacent field. + // Startfunc is one of (left|right|top|bottom)Of. + moveTo0(startfunc, worker) { + let endidx = this.indexOf0(); // Target field (the gap) + if (endidx === -1) { + worker.endTask(); + return; + } + let startidx = startfunc(endidx); // Start field (relative to the gap) + if (startidx === -1) { + worker.endTask(); + return; + } + // Replace in the internal representation + let moved = this.stones[startidx]; + this.stones[startidx] = this.stones[endidx]; + this.stones[endidx] = moved; + this.moveCount += 1; + // Move on screen using an animation effect. + new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(worker); + } + + // Move the stone right from the gap into the gap + moveRight(worker) { + this.moveTo0(this.leftOf, worker); + } + + // Move the stone left from the gap into the gap + moveLeft(worker) { + this.moveTo0(this.rightOf, worker); + } + + // Move the stone above the gap into the gap + moveUp(worker) { + this.moveTo0(this.bottomOf, worker); + } + + // Move the stone below the gap into the gap + moveDown(worker) { + this.moveTo0(this.topOf, worker); + } + + // Check if the board is solved (all stones at the right position) + isSolved() { + for (i = 0; i < this.stones.length; i++) + if (!this.stones[i].isOnTarget(i)) + return false; + return true; + } + + // counts the inversions on the board + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + getInversionCount() { + let inversions = 0; + for (outer = 0; outer < stonesPerBoard - 1; outer++) { + let outernr = this.stones[outer].number; + if (outernr === 0) + continue; + for (inner = outer + 1; inner < stonesPerBoard; inner++) { + let innernr = this.stones[inner].number; + if (innernr > 0 && outernr > innernr) + inversions++; + } + } + return inversions; + } + + // return whether the puzzle is solvable + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + isSolvable() { + let invs = this.getInversionCount(); + if (stonesPerLine % 2 !== 0) // odd number of rows/columns + return (invs % 2 === 0); + else { + return ((invs + this.rowOf0()) % 2 !== 0); + } + } + + // draw the result field, pass null as argument if not called from worker + drawResult(worker) { + let field = this.resultField; + let solved = this.isSolved(); + if (solved) + g.setColor(0, 1, 0); + else + g.setColor(1, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery); + if (worker !== null) + worker.endTask(); + if (solved) + setTimeout(() => { + gameEnd(this.moveCount); + }, 500); + } + + // draws the menu button + drawMenu() { + let field = this.menuField; + g.setColor(0.5, 0.5, 0.5); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + let l = field.left + 8; + let r = field.left + stonesize - 8; + let t = field.top + 5; + for (i = 0; i < 3; i++) + g.fillRect(l, t + (i * 6), r, t + (i * 6) + 2); + } + +} + + +// *** Global helper methods + +// draw some text with some surrounding to increase contrast +// text is drawn at given (x,y) position with textcol. +// frame is drawn 2 pixels around (x,y) in each direction in framecol. +function framedText(text, x, y, textcol, framecol) { + g.setColor(framecol); + for (i = -2; i < 3; i++) + for (j = -2; j < 3; j++) { + if (i === 0 && j === 0) + continue; + g.drawString(text, x + i, y + j); + } + g.setColor(textcol).drawString(text, x, y); +} + +// Show the splash screen at program start, call afterSplash afterwards. +// If spash mode is "off", call afterSplash directly. +function showSplash(afterSplash) { + if (splashMode === "off") + afterSplash(); + else { + g.reset(); + g.drawImage(introscreen, 0, 0); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(afterSplash, (splashMode === "long" ? 2000 : 1000)); + }, (splashMode === "long" ? 1000 : 1)); + }, (splashMode === "long" ? 2000 : 1000)); + } +} + + +// *** Global flow control + +// Initialize the game with an explicit number of stones per line +function initGame(bpl) { + setStonesPreLine(bpl); + newGame(); +} + +// Start a new game with the same number of stones per line as before +function newGame() { + board = new Board(); + continueGame(); +} + +// Continue the currently running game +function continueGame() { + E.showMenu(); + board.draw(); + dragger.setEnabled(true); +} + +// Show message on game end, allows to restart new game +function gameEnd(moveCount) { + dragger.setEnabled(false); + E.showPrompt("You solved the\n" + stonesPerLine + "x" + stonesPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { + title: "Puzzle solved", + buttons: { + "Again": newGame, + "Menu": () => showMenu(false), + "Exit": exitGame + } + }).then(v => { + E.showPrompt(); + setTimeout(v, 10); + }); +} + +// A tiny about screen +function showAbout(doContinue) { + E.showAlert("Author: Dirk Hillbrecht\nLicense: MIT", "Puzzle15").then(() => { + if (doContinue) + continueGame(); + else + showMenu(false); + }); +} + +// Show the in-game menu allowing to start a new game +function showMenu(withContinue) { + var mainmenu = { + "": { + "title": "15 Puzzle" + } + }; + if (withContinue) + mainmenu.Continue = continueGame; + mainmenu["Start 3x3"] = () => initGame(3); + mainmenu["Start 4x4"] = () => initGame(4); + mainmenu["Start 5x5"] = () => initGame(5); + mainmenu.About = () => showAbout(withContinue); + mainmenu.Exit = exitGame; + dragger.setEnabled(false); + g.clear(true); + E.showMenu(mainmenu); +} + +// Handle a "click" event (only needed for menu button) +function handleclick(e) { + if (board.menuField.contains(e.x, e.y)) + setTimeout(() => showMenu(true), 10); +} + +// Handle a drag event (moving the stones around) +function handledrag(e) { + worker.addTask(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => board.moveRight(e) : e => board.moveLeft(e)) : + (e.dy > 0 ? e => board.moveDown(e) : e => board.moveUp(e))); + worker.addTask(e => board.drawResult(e)); +} + +// exit the game, clear screen first to prevent ghost images +function exitGame() { + g.clear(true); + setTimeout(load, 300); +} + + +// *** Main program + +g.clear(true); + +// Load global app settings +loadSettings(); + +// We need a worker... +var worker = new Worker(); + +// Board will be initialized after the splash screen has been shown +var board; + +// Dragger is needed for interaction during the game +var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +// Disable dragger as board is not yet initialized +dragger.setEnabled(false); + +// Nevertheless attach it so that it is ready once the game starts +dragger.attach(); + +// Start the game by handling the splash screen sequence +showSplash(() => { + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); + if (startWith === "3x3") + initGame(3); + else if (startWith === "4x4") + initGame(4); + else if (startWith === "5x5") + initGame(5); + else + showMenu(false); +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/puzzle15.app.png b/apps/puzzle15/puzzle15.app.png new file mode 100644 index 000000000..f95366a46 Binary files /dev/null and b/apps/puzzle15/puzzle15.app.png differ diff --git a/apps/puzzle15/puzzle15.settings.js b/apps/puzzle15/puzzle15.settings.js new file mode 100644 index 000000000..352ec4315 --- /dev/null +++ b/apps/puzzle15/puzzle15.settings.js @@ -0,0 +1,50 @@ +// Settings menu for the Puzzle15 app + +(function(back) { + var FILE = "puzzle15.json"; + // Load settings + var settings = Object.assign({ + splashMode: "long", + startWith: "4x4" + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "15 Puzzle" + }, + "< Back": () => back(), + "Splash": stringInSettings("splashMode", ["long", "short", "off"]), + "Start with": stringInSettings("startWith", ["3x3", "4x4", "5x5", "menu"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/screenshot.png b/apps/puzzle15/screenshot.png new file mode 100644 index 000000000..3a79a3ae6 Binary files /dev/null and b/apps/puzzle15/screenshot.png differ diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog index 135e69d23..b9be6039d 100644 --- a/apps/qalarm/ChangeLog +++ b/apps/qalarm/ChangeLog @@ -1,2 +1,6 @@ 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 +0.04: Fix alarm not activating sometimes. 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/metadata.json b/apps/qalarm/metadata.json new file mode 100644 index 000000000..326ba33a7 --- /dev/null +++ b/apps/qalarm/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "qalarm", + "name": "Q Alarm and Timer", + "shortName": "Q Alarm", + "icon": "app.png", + "version": "0.04", + "description": "Alarm and timer app with days of week and 'hard' option.", + "tags": "tool,alarm,widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + { "name": "qalarm.app.js", "url": "app.js" }, + { "name": "qalarm.boot.js", "url": "boot.js" }, + { "name": "qalarm.js", "url": "qalarm.js" }, + { "name": "qalarmcheck.js", "url": "qalarmcheck.js" }, + { "name": "qalarm.img", "url": "app-icon.js", "evaluate": true }, + { "name": "qalarm.wid.js", "url": "widget.js" } + ], + "data": [{ "name": "qalarm.json" }] +} diff --git a/apps/qalarm/qalarm.js b/apps/qalarm/qalarm.js index 6b31ba645..8e82be186 100644 --- a/apps/qalarm/qalarm.js +++ b/apps/qalarm/qalarm.js @@ -143,7 +143,7 @@ let alarms = require("Storage").readJSON("qalarm.json", 1) || []; let active = alarms.filter( (alarm) => alarm.on && - alarm.t < t && + alarm.t <= t && alarm.last != time.getDate() && (alarm.timer || alarm.daysOfWeek[time.getDay()]) ); diff --git a/apps/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..c868b6668 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -1,3 +1,8 @@ 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 +0.06: Fix: don't try to redraw widget when widgets not loaded +0.07: Option to switch theme + Changed time selection to 5-minute intervals \ No newline at end of file diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md index 033014789..660bda787 100644 --- a/apps/qmsched/README.md +++ b/apps/qmsched/README.md @@ -1,9 +1,19 @@ # 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) +### Switch Theme: + +Switch to dark theme during Quiet Mode. + * **NOTE**: This switches between the default "Dark BW" and "Light BW" themes, so custom theme settings will be lost. + +### LCD Settings: + +If set, these override the default LCD settings while Quiet Mode is active. \ No newline at end of file diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js index c6377d4ba..e05eff6a2 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -2,27 +2,75 @@ 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 settings +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); + eval(require('Storage').read('qmsched.boot.js')); // apply new schedules right away } -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) { @@ -30,31 +78,66 @@ function formatTime(t) { const mins = Math.round((t-hrs)*60); return (" "+hrs).substr(-2)+":"+("0"+mins).substr(-2); } +/** + * Apply theme + */ +function applyTheme() { + const theme = (require("Storage").readJSON("setting.json", 1) || {}).theme; + if (theme && theme.dark===g.theme.dark) return; // already correct + g.theme = theme; + delete g.reset; + g._reset = g.reset; + g.reset = function(n) { return g._reset().setColor(g.theme.fg).setBgColor(g.theme.bg); }; + g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); }; + g.clear(1); + Bangle.drawWidgets(); + delete m.lastIdx; // force redraw + m.draw(); +} +/** + * Library uses this to make the app update itself + * @param {int} mode New Quite Mode + */ +function setAppQuietMode(mode) { + if (mode === current) return; + current = mode; + delete m.lastIdx; // force redraw + applyTheme(); + if (m.lastIdx===undefined) m.draw(); // applyTheme didn't redraw menu, but we need to show updated mode +} + +let m; function showMainMenu() { - let menu = {"": {"title": "Quiet Mode"}}; + let menu = { + "": {"title": "Quiet Mode"}, + "< Exit": () => load() + }; // "Current Mode""Silent" won't fit on Bangle.js 2 - menu["Current" + ((process.env.HWVERSION===2)?"":" Mode")]= { - value: (require("Storage").readJSON("setting.json", 1) || {}).quiet|0, - format: v => modeNames[v], - onchange: function(v) { - if (v<0) {v = 2;} - if (v>2) {v = 0;} - require("qmsched").setMode(v); - this.value = v; - }, + menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = { + value: current, + min:0, max:2, wrap: true, + format: () => modeNames[current], + onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current` }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { - 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: () => { + m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange) + showEditMenu(idx); + } }; }); menu["Add Schedule"] = () => showEditMenu(-1); - menu["< Back"] = () => {load();}; - return E.showMenu(menu); + menu["Switch Theme"] = { + value: !!get("switchTheme"), + format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", + onchange: v => v ? set("switchTheme", v) : unset("switchTheme"), + }; + menu["LCD Settings"] = () => showOptionsMenu(); + m = E.showMenu(menu); } function showEditMenu(index) { @@ -69,33 +152,22 @@ function showEditMenu(index) { } const menu = { "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, + "< Cancel": () => showMainMenu(), "Hours": { value: hrs, - onchange: function(v) { - if (v<0) {v = 23;} - if (v>23) {v = 0;} - hrs = v; - this.value = v; - }, // no arrow fn -> preserve 'this' + min:0, max:23, wrap:true, + onchange: v => {hrs = v;}, }, "Minutes": { value: mins, - onchange: function(v) { - if (v<0) {v = 59;} - if (v>59) {v = 0;} - mins = v; - this.value = v; - }, // no arrow fn -> preserve 'this' + min:0, max:55, step:5, wrap:true, + onchange: v => {mins = v;}, }, "Switch to": { value: mode, + min:0, max:2, wrap:true, format: v => modeNames[v], - onchange: function(v) { - if (v<0) {v = 2;} - if (v>2) {v = 0;} - mode = v; - this.value = v; - }, // no arrow fn -> preserve 'this' + onchange: v => {mode = v;}, }, }; function getSched() { @@ -110,18 +182,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); + m = 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");}, + }, + }; + m = E.showMenu(oMenu); +} + +loadSettings(); showMainMenu(); diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js index 2712cab30..c4610ce3e 100644 --- a/apps/qmsched/boot.js +++ b/apps/qmsched/boot.js @@ -1,7 +1,15 @@ // apply Quiet Mode schedules (function qm() { - let scheds = require("Storage").readJSON("qmsched.json", 1) || []; - if (!scheds.length) { return;} + if (Bangle.qmTimeout) clearTimeout(Bangle.qmTimeout); // so the app can eval() this file to apply changes right away + delete Bangle.qmTimeout; + let bSettings = require('Storage').readJSON('setting.json',true)||{}; + const curr = 0|bSettings.quiet; + delete bSettings; + 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); @@ -12,7 +20,7 @@ let t = 3600000*(next.hr-hr); // timeout in milliseconds if (t<0) {t += 86400000;} // scheduled for tomorrow: add a day /* update quiet mode at the correct time. */ - setTimeout(() => { + Bangle.qmTimeout=setTimeout(() => { require("qmsched").setMode(mode); qm(); // schedule next update }, t); diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js index a3d36ed34..9696657cc 100644 --- a/apps/qmsched/lib.js +++ b/apps/qmsched/lib.js @@ -1,18 +1,51 @@ +/** + * Apply appropriate theme for given mode + * @param {int} mode Quiet Mode + */ +function switchTheme(mode) { + if (!!mode === g.theme.dark) return; // nothing to do + let s = require("Storage").readJSON("setting.json", 1) || {}; + // default themes, copied from settings.js:showThemeMenu() + function cl(x) { return g.setColor(x).getColor(); } + s.theme = mode ? { + // 'Dark BW' + fg: cl("#fff"), bg: cl("#000"), + fg2: cl("#0ff"), bg2: cl("#000"), + fgH: cl("#fff"), bgH: cl("#00f"), + dark: true + } : { + // 'Light BW' + fg: cl("#000"), bg: cl("#fff"), + fg2: cl("#000"), bg2: cl("#cff"), + fgH: cl("#000"), bgH: cl("#0ff"), + dark: false + }; + require("Storage").writeJSON("setting.json", s); + // reload clocks with new theme, otherwise just wait for user to switch apps + if (Bangle.CLOCK) load(global.__FILE__); +} +/** + * Apply LCD options and theme for given mode + * @param {int} mode Quiet Mode + */ +exports.applyOptions = function(mode) { + 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)); + if ((require("Storage").readJSON("qmsched.json", 1) || {}).switchTheme) switchTheme(mode); +}; /** * 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 (typeof WIDGETS === "object" && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw(); + if (global.setAppQuietMode) setAppQuietMode(mode); // current app knows how to update itself +}; diff --git a/apps/qmsched/metadata.json b/apps/qmsched/metadata.json new file mode 100644 index 000000000..daeaad624 --- /dev/null +++ b/apps/qmsched/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "qmsched", + "name": "Quiet Mode Schedule and Widget", + "shortName": "Quiet Mode", + "version": "0.07", + "description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.", + "icon": "app.png", + "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, + {"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}], + "tags": "tool,widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"qmsched","url":"lib.js"}, + {"name":"qmsched.app.js","url":"app.js"}, + {"name":"qmsched.boot.js","url":"boot.js"}, + {"name":"qmsched.img","url":"icon.js","evaluate":true}, + {"name":"qmsched.wid.js","url":"widget.js"} + ], + "data": [{"name":"qmsched.json"}] +} 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..daa11ac71 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.reset().clearRect(x, y, x+23, y+23); + // quiet mode: draw red one-way-street sign (dim red on Bangle.js 1) + x = this.x+11;y = this.y+11; // center of widget + g.setColor(process.env.HWVERSION===2 ? 1 : 0.8, 0, 0).fillCircle(x, y, 8); + 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..6d9cc0569 100644 --- a/apps/qrcode/ChangeLog +++ b/apps/qrcode/ChangeLog @@ -1,2 +1,5 @@ 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 +0.05: Change brightness on touch diff --git a/apps/qrcode/custom.html b/apps/qrcode/custom.html index 618840da9..7ae3eb3af 100644 --- a/apps/qrcode/custom.html +++ b/apps/qrcode/custom.html @@ -3,56 +3,170 @@ - - - -
+ 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/metadata.json b/apps/qrcode/metadata.json new file mode 100644 index 000000000..22f8f7b53 --- /dev/null +++ b/apps/qrcode/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "qrcode", + "name": "Custom QR Code", + "version": "0.05", + "description": "Use this to upload a customised QR code to Bangle.js", + "icon": "app.png", + "tags": "qrcode", + "supports": ["BANGLEJS","BANGLEJS2"], + "custom": "custom.html", + "customConnect": true, + "storage": [ + {"name":"qrcode.app.js"}, + {"name":"qrcode.img","url":"app-icon.js","evaluate":true} + ] +} 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/rclock/metadata.json b/apps/rclock/metadata.json new file mode 100644 index 000000000..77a036481 --- /dev/null +++ b/apps/rclock/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "rclock", + "name": "Round clock with seconds, minutes and date", + "shortName": "Round Clock", + "version": "0.06", + "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"rclock.app.js","url":"rclock.app.js"}, + {"name":"rclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog new file mode 100644 index 000000000..b9c26b4e3 --- /dev/null +++ b/apps/rebble/ChangeLog @@ -0,0 +1,4 @@ +0.01: First release +0.02: Fix typo to Purple +0.03: Added dependancy on Pedometer Widget +0.04: Fixed icon and png to 48x48 pixels diff --git a/apps/rebble/README.md b/apps/rebble/README.md new file mode 100644 index 000000000..0ecb51d7a --- /dev/null +++ b/apps/rebble/README.md @@ -0,0 +1,27 @@ +# 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 +* The icon is James Dean - 'Rebel Without a Cause' + +![](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/metadata.json b/apps/rebble/metadata.json new file mode 100644 index 000000000..212a7b5b3 --- /dev/null +++ b/apps/rebble/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "rebble", + "name": "Rebble Clock", + "shortName": "Rebble", + "version": "0.04", + "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion", + "readme": "README.md", + "icon": "rebble.png", + "dependencies": {"mylocation":"app", "widpedom":"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} + ] +} 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..3fc45b820 --- /dev/null +++ b/apps/rebble/rebble.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoOG4EV+/I+dVAAVUCgcFBIYABpIJBgcFoIKEqkQgEH6EH0ILEqAhCgkBqEVBYdAhUBBoU9GAlAlw5CgERgILDIocEgEGoALDlEHwEAlkUg8EBYfAFwVA+BgEqmQjWrBgMQhgvDqmA9Wq1WsNoMALweDBQIAB4E8BYdTpwLD/kA4AXDjwKC1f/IAILDnQLC1//4ALEHQQLCKgILDFwYLB6EATgVABYe///MNgdA3kQEoILGqCNBlfQh//4NAPAVQ+YLBQYM/ocABYfAiEqgE0g6DBF4eAlFrYQZHDoOu1Xo8lgBYtCKIOo9aOBAAJrCBYWv9X/+gXEqSZC/f//4LHz/6DQIjEBYOhgG6BY1a1WggDCB3ojErYTBoEOa4QLF1X9jWrXwILGKYOvBYtfKYX+17iBHYdX1WQgf/34LBUwQLB1cLWIJqCBYdV9W+1+//oLBWQVVqnuD4M/KQoAB/+kBYJGBCwYLCI4P/DQILFnwLCEQ1Vp+q/46CBYtDXgJ1FAAVwfI4ABqAUCBY8A9gLIqEA9ALEKYYLB9YLERwQ=")) diff --git a/apps/rebble/rebble.png b/apps/rebble/rebble.png new file mode 100644 index 000000000..acfd37400 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/recorder/ChangeLog b/apps/recorder/ChangeLog index 40240de64..e2ae0111b 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -3,3 +3,11 @@ Fix interface.html 0.03: Fix theme and maps/graphing if no GPS 0.04: Multiple bugfixes +0.05: Add recording for coresensor +0.06: Add recording for battery stats + Fix execution of other recorders (*.recorder.js) + Modified icons and colors for better visibility + Only show plotting speed if Latitude is available +0.07: Add recording for Barometer + Record all HRM events + Move recording for CoreTemp to its own app diff --git a/apps/recorder/README.md b/apps/recorder/README.md index ba53a99f2..87be34424 100644 --- a/apps/recorder/README.md +++ b/apps/recorder/README.md @@ -16,7 +16,9 @@ You can record * **Time** The current time * **GPS** GPS Latitude, Longitude and Altitude * **Steps** Steps counted by the step counter -* **HR** Heart rate +* **HR** Heart rate and confidence +* **BAT** Battery percentage and voltage +* **Core** CoreTemp body temperature **Note:** It is possible for other apps to record information using this app as well. They need to define a `foobar.recorder.js` file - see the `getRecorders` @@ -24,4 +26,4 @@ function in `widget.js` for more information. ## Tips -When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. +When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a red satellite symbol, which you will see turn green when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. diff --git a/apps/recorder/app.js b/apps/recorder/app.js index fcd8d6031..5b1c63aef 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -199,9 +199,10 @@ function viewTrack(filename, info) { menu['Plot Alt.'] = function() { plotGraph(info, "Altitude"); }; - menu['Plot Speed'] = function() { - plotGraph(info, "Speed"); - }; + if (info.fields.includes("Latitude")) + menu['Plot Speed'] = function() { + plotGraph(info, "Speed"); + }; // TODO: steps, heart rate? menu['Erase'] = function() { E.showPrompt("Delete Track?").then(function(v) { diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index ad0de4887..0535b2d51 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -18,8 +18,15 @@ ${track[0].Heartrate!==undefined ? ` - Step Count`:``} - + Step Count + `:``} +${track[0].Core!==undefined ? ` + Core Temp + `:``} +${track[0].Skin!==undefined ? ` + Skin Temp + `:``} + Tracks @@ -37,6 +44,12 @@ ${track.map(pt=>` ${0|pt.Heartrate}\n`).join("") ${track[0].Steps!==undefined ? ` ${track.map(pt=>` ${0|pt.Steps}\n`).join("")} `:``} +${track[0].Core!==undefined ? ` +${track.map(pt=>` ${0|pt.Core}\n`).join("")} + `:``} +${track[0].Skin!==undefined ? ` +${track.map(pt=>` ${0|pt.Skin}\n`).join("")} + `:``} @@ -59,8 +72,7 @@ ${track.map(pt=>` ${0|pt.Steps}\n`).join("")} function saveGPX(track, title) { var gpx = ` - - + diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json new file mode 100644 index 000000000..815a7db40 --- /dev/null +++ b/apps/recorder/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "recorder", + "name": "Recorder (BETA)", + "shortName": "Recorder", + "version": "0.07", + "description": "Record GPS position, heart rate and more in the background, then download to your PC.", + "icon": "app.png", + "tags": "tool,outdoors,gps,widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"recorder.app.js","url":"app.js"}, + {"name":"recorder.img","url":"app-icon.js","evaluate":true}, + {"name":"recorder.wid.js","url":"widget.js"}, + {"name":"recorder.settings.js","url":"settings.js"} + ], + "data": [{"name":"recorder.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] +} diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 09893bbb7..de465b7c1 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -48,38 +48,46 @@ Bangle.removeListener('GPS', onGPS); Bangle.setGPSPower(0,"recorder"); }, - draw : (x,y) => g.setColor(hasFix?"#f00":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y) + draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y) }; }, hrm:function() { - var bpm = 0, bpmConfidence = 0; - var hasBPM = false; + var bpm = "", bpmConfidence = ""; function onHRM(h) { - if (h.confidence >= bpmConfidence) { - bpmConfidence = h.confidence; - bpm = h.bpm; - if (bpmConfidence) hasBPM = true; - } + bpmConfidence = h.confidence; + bpm = h.bpm; } return { name : "HR", - fields : ["Heartrate"], + fields : ["Heartrate", "Confidence"], getValues : () => { - var r = [bpmConfidence?bpm:""]; - bpm = 0; bpmConfidence = 0; + var r = [bpm,bpmConfidence]; + bpm = ""; bpmConfidence = ""; return r; }, start : () => { - hasBPM = false; Bangle.on('HRM', onHRM); Bangle.setHRMPower(1,"recorder"); }, stop : () => { - hasBPM = false; Bangle.removeListener('HRM', onHRM); Bangle.setHRMPower(0,"recorder"); }, - draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y) + draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) + }; + }, + bat:function() { + return { + name : "BAT", + fields : ["Battery Percentage", "Battery Voltage", "Charging"], + getValues : () => { + return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()]; + }, + start : () => { + }, + stop : () => { + }, + draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y) }; }, steps:function() { @@ -94,11 +102,41 @@ }, start : () => { lastSteps = Bangle.getStepCount(); }, stop : () => {}, - draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y) + draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y) }; } - // TODO: recAltitude from pressure sensor }; + if (Bangle.getPressure){ + recorders['baro'] = function() { + var temp="",press="",alt=""; + function onPress(c) { + temp=c.temperature; + press=c.pressure; + alt=c.altitude; + } + return { + name : "Baro", + fields : ["Barometer Temperature", "Barometer Pressure", "Barometer Altitude"], + getValues : () => { + var r = [temp,press,alt]; + temp=""; + press=""; + alt=""; + return r; + }, + start : () => { + Bangle.setBarometerPower(1,"recorder"); + Bangle.on('pressure', onPress); + }, + stop : () => { + Bangle.setBarometerPower(0,"recorder"); + Bangle.removeListener('pressure', onPress); + }, + draw : (x,y) => g.setColor("#0f0").drawImage(atob("DAwBAAH4EIHIEIHIEIHIEIEIH4AA"),x,y) + }; + } + } + /* eg. foobar.recorder.js (function(recorders) { recorders.foobar = { @@ -111,7 +149,7 @@ } }) */ - require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders)); + require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders)); return recorders; } diff --git a/apps/rndmclk/metadata.json b/apps/rndmclk/metadata.json new file mode 100644 index 000000000..e837c4bce --- /dev/null +++ b/apps/rndmclk/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "rndmclk", + "name": "Random Clock Loader", + "version": "0.03", + "description": "Load a different clock whenever the LCD is switched on.", + "icon": "rndmclk.png", + "type": "widget", + "tags": "widget,clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"rndmclk.wid.js","url":"widget.js"} + ] +} diff --git a/apps/route/metadata.json b/apps/route/metadata.json new file mode 100644 index 000000000..bc1962868 --- /dev/null +++ b/apps/route/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "route", + "name": "Route Viewer", + "version": "0.02", + "description": "Upload a KML file of a route, and have your watch display a map with how far around it you are", + "icon": "app.png", + "tags": "", + "supports": ["BANGLEJS"], + "custom": "custom.html", + "storage": [ + {"name":"route.app.js"}, + {"name":"route.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/rpgdice/metadata.json b/apps/rpgdice/metadata.json new file mode 100644 index 000000000..5a37a1f43 --- /dev/null +++ b/apps/rpgdice/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "rpgdice", + "name": "RPG dice", + "version": "0.02", + "description": "Simple RPG dice rolling app.", + "icon": "rpgdice.png", + "type": "app", + "tags": "game,fun", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-rpg-dice-screenshot.png"}], + "storage": [ + {"name":"rpgdice.app.js","url":"app.js"}, + {"name":"rpgdice.img","url":"app-icon.js","evaluate":true} + ] +} 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/rtorch/metadata.json b/apps/rtorch/metadata.json new file mode 100644 index 000000000..ee056ac57 --- /dev/null +++ b/apps/rtorch/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "rtorch", + "name": "Red Torch", + "shortName": "RedTorch", + "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","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"rtorch.app.js","url":"app.js"}, + {"name":"rtorch.wid.js","url":"widget.js", "supports": ["BANGLEJS"]}, + {"name":"rtorch.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog new file mode 100644 index 000000000..0df910367 --- /dev/null +++ b/apps/run/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Set pace format to mm:ss, time format to h:mm:ss, + added settings to opt out of GPS and HRM +0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2 diff --git a/apps/run/README.md b/apps/run/README.md new file mode 100644 index 000000000..c094d4873 --- /dev/null +++ b/apps/run/README.md @@ -0,0 +1,34 @@ +# Run App + +This app allows you to display the status of your run, it +shows distance, time, steps, cadence, pace and more. + +To use it, start the app and press the middle button so that +the red `STOP` in the bottom right turns to a green `RUN`. + +## Display + +* `DIST` - the distance travelled based on the GPS (if you have a GPS lock). + * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As + the GPS updates your position as it gets more satellites your position changes and the distance + shown will increase, even if you are standing still. +* `TIME` - the elapsed time for your run +* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far** +* `HEART` - Your heart rate +* `STEPS` - Steps since you started exercising +* `CADENCE` - Steps per second based on your step rate *over the last minute* +* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically +so if you have no GPS lock you just need to wait. +* The current time is displayed right at the bottom of the screen +* `RUN/STOP` - whether the distance for your run is being displayed or not + +## Recording Tracks + +`Run` doesn't directly allow you to record your tracks at the moment. +However you can just install the `Recorder` app, turn recording on in +that, and then start the `Run` app. + +## TODO + +* Allow this app to trigger the `Recorder` app on and off directly. +* Keep a log of each run's stats (distance/steps/etc) diff --git a/apps/run/app-icon.js b/apps/run/app-icon.js new file mode 100644 index 000000000..a97d1b8ce --- /dev/null +++ b/apps/run/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/run/app.js b/apps/run/app.js new file mode 100644 index 000000000..a92bbe387 --- /dev/null +++ b/apps/run/app.js @@ -0,0 +1,178 @@ +var B2 = process.env.HWVERSION==2; +var Layout = require("Layout"); +var locale = require("locale"); +var fontHeading = "6x8:2"; +var fontValue = B2 ? "6x15:2" : "6x8:3"; +var headingCol = "#888"; +var running = false; +var fixCount = 0; +var startTime; +var startSteps; +// This & previous GPS readings +var lastGPS, thisGPS; +var distance = 0; ///< distance in meters +var startSteps = Bangle.getStepCount(); ///< number of steps when we started +var lastStepCount = startSteps; // last time 'step' was called +var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute) + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// --------------------------- + +function formatTime(ms) { + let hrs = Math.floor(ms/3600000).toString(); + let mins = (Math.floor(ms/60000)%60).toString(); + let secs = (Math.floor(ms/1000)%60).toString(); + + if (hrs === '0') + return mins.padStart(2,0)+":"+secs.padStart(2,0); + else + return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours +} + +// Format speed in meters/second +function formatPace(speed) { + if (speed < 0.1667) { + return `__:__`; + } + const pace = Math.round(1000 / speed); // seconds for 1km + const min = Math.floor(pace / 60); // minutes for 1km + const sec = pace % 60; + return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2); +} + +// --------------------------- + +function clearState() { + distance = 0; + startSteps = Bangle.getStepCount(); + stepHistory.fill(0); + layout.dist.label=locale.distance(distance); + layout.time.label="00:00"; + layout.pace.label=formatPace(0); + layout.hrm.label="--"; + layout.steps.label=0; + layout.cadence.label= "0"; + layout.status.bgCol = "#f00"; +} + +function onStartStop() { + running = !running; + if (running) { + clearState(); + startTime = Date.now(); + } + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + // if stopping running, don't clear state + // so we can at least refer to what we've done + layout.render(); +} + +var layout = new Layout( { + type:"v", c: [ + { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol }, + {type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 }, + {type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"PACE", fillx:1, col:headingCol }, + {type:"txt", font:fontHeading, label:"HEART", fillx:1, col:headingCol } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontValue, label:`__'__"`, id:"pace", fillx:1 }, + {type:"txt", font:fontValue, label:"--", id:"hrm", fillx:1 } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"STEPS", fillx:1, col:headingCol }, + {type:"txt", font:fontHeading, label:"CADENCE", fillx:1, col:headingCol } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontValue, label:"0", id:"steps", fillx:1 }, + {type:"txt", font:fontValue, label:"0", id:"cadence", fillx:1 } + ]}, { type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, + {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, + {type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 } + ]}, + + ] +},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]}); +clearState(); +layout.render(); + +function onTimer() { + layout.clock.label = locale.time(new Date(),1); + if (!running) { + layout.render(); + return; + } + // called once a second + var duration = Date.now() - startTime; // in ms + // set cadence based on steps over last minute + var stepsInMinute = E.sum(stepHistory); + var cadence = 60000 * stepsInMinute / Math.min(duration,60000); + // update layout + layout.time.label = formatTime(duration); + layout.steps.label = Bangle.getStepCount()-startSteps; + layout.cadence.label = Math.round(cadence); + layout.render(); + // move step history onwards + stepHistory.set(stepHistory,1); + stepHistory[0]=0; +} + +function radians(a) { + return a*Math.PI/180; +} + +// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km +// https://www.movable-type.co.uk/scripts/latlong.html +function calcDistance(a,b) { + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + +Bangle.on("GPS", function(fix) { + layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; + if (!fix.fix) { return; } // only process actual fixes + if (fixCount++ == 0) { + Bangle.buzz(); // first fix, does not need to respect quiet mode + lastGPS = fix; // initialise on first fix + } + + thisGPS = fix; + + if (running) { + var d = calcDistance(lastGPS, thisGPS); + distance += d; + layout.dist.label=locale.distance(distance); + var duration = Date.now() - startTime; // in ms + var speed = distance * 1000 / duration; // meters/sec + layout.pace.label = formatPace(speed); + lastGPS = fix; + } +}); +Bangle.on("HRM", function(h) { + layout.hrm.label = h.bpm; +}); +Bangle.on("step", function(steps) { + if (running) { + layout.steps.label = steps-Bangle.getStepCount(); + stepHistory[0] += steps-lastStepCount; + } + lastStepCount = steps; +}); + +let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true}; + +// We always call ourselves once a second, if only to update the time +setInterval(onTimer, 1000); + +/* Turn GPS and HRM on right at the start to ensure +we get the highest chance of a lock. */ +if (settings.use_hrm) Bangle.setHRMPower(true,"app"); +if (settings.use_gps) Bangle.setGPSPower(true,"app"); diff --git a/apps/run/app.png b/apps/run/app.png new file mode 100644 index 000000000..7059b8b01 Binary files /dev/null and b/apps/run/app.png differ diff --git a/apps/run/metadata.json b/apps/run/metadata.json new file mode 100644 index 000000000..4d5e85778 --- /dev/null +++ b/apps/run/metadata.json @@ -0,0 +1,16 @@ +{ "id": "run", + "name": "Run", + "version":"0.03", + "description": "Displays distance, time, steps, cadence, pace and more for runners.", + "icon": "app.png", + "tags": "run,running,fitness,outdoors,gps", + "supports" : ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"run.app.js","url":"app.js"}, + {"name":"run.img","url":"app-icon.js","evaluate":true}, + {"name":"run.settings.js","url":"settings.js"} + ], + "data": [{"name":"run.json"}] +} diff --git a/apps/run/screenshot.png b/apps/run/screenshot.png new file mode 100644 index 000000000..1a813f19d Binary files /dev/null and b/apps/run/screenshot.png differ diff --git a/apps/run/settings.js b/apps/run/settings.js new file mode 100644 index 000000000..882b15c71 --- /dev/null +++ b/apps/run/settings.js @@ -0,0 +1,44 @@ +(function(back) { + const SETTINGS_FILE = "run.json"; + + // initialize with default settings... + let s = { + 'use_gps': true, + 'use_hrm': true + } + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = storage.readJSON(SETTINGS_FILE, 1) || {} + const saved = settings || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + E.showMenu({ + '': { 'title': 'Run' }, + '< Back': back, + 'Use GPS': { + value: s.use_gps, + format: () => (s.use_gps ? 'Yes' : 'No'), + onchange: () => { + s.use_gps = !s.use_gps; + save(); + }, + }, + 'Use HRM': { + value: s.use_hrm, + format: () => (s.use_hrm ? 'Yes' : 'No'), + onchange: () => { + s.use_hrm = !s.use_hrm; + save(); + }, + } + }) +}) diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog new file mode 100644 index 000000000..7953548cb --- /dev/null +++ b/apps/ruuviwatch/ChangeLog @@ -0,0 +1,2 @@ +0.01: Hello Ruuvi Watch! +0.02: Clear gfx on startup. diff --git a/apps/ruuviwatch/README.md b/apps/ruuviwatch/README.md new file mode 100644 index 000000000..bf4358267 --- /dev/null +++ b/apps/ruuviwatch/README.md @@ -0,0 +1,25 @@ +# Ruuvi Watch + +Watch the status of [RuuviTags](https://ruuvi.com) in range. + + - Id + - Temperature (°C) + - Humidity (%) + - Pressure (hPa) + - Battery voltage + + Also shows how "fresh" the data is (age of reading). + + ## Usage + + - Scans for devices when launched and every N seconds. + - Page trough devices with BTN1/BTN3. + - Trigger scan with BTN2. + +## Todo / ideas + + - Allow to "name" known devices + - Prevent flicker when updating + - Include more data + - Support older Ruuvi protocols + diff --git a/apps/ruuviwatch/metadata.json b/apps/ruuviwatch/metadata.json new file mode 100644 index 000000000..12f9ff4a0 --- /dev/null +++ b/apps/ruuviwatch/metadata.json @@ -0,0 +1,14 @@ +{ "id": "ruuviwatch", + "name": "Ruuvi Watch", + "shortName":"Ruuvi Watch", + "icon": "ruuviwatch.png", + "version":"0.02", + "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", + "readme":"README.md", + "tags": "bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"}, + {"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true} + ] +} diff --git a/apps/ruuviwatch/ruuviwatch.app-icon.js b/apps/ruuviwatch/ruuviwatch.app-icon.js new file mode 100644 index 000000000..7ed27ef6c --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA")) \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js new file mode 100644 index 000000000..46218a323 --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -0,0 +1,151 @@ +require("Storage").write("ruuviwatch.info",{ + "id":"ruuviwatch", + "name":"Ruuvi Watch", + "src":"ruuviwatch.app.js", + "icon":"ruuviwatch.img" + }); + + const lookup = {}; + const ruuvis = []; + let current = 0; + + function int2Hex (str) { + return ('0' + str.toString(16).toUpperCase()).slice(-2); + } + + function p(data) { + const OFFSET = 7; // 0-4 header, 5-6 Ruuvi id + const robject = {}; + robject.version = data[OFFSET]; + + let temperature = (data[OFFSET+1] << 8) | (data[OFFSET+2] & 0xff); + if (temperature > 32767) { + temperature -= 65534; + } + robject.temperature = temperature / 200.0; + + robject.humidity = (((data[OFFSET+3] & 0xff) << 8) | (data[OFFSET+4] & 0xff)) / 400.0; + robject.pressure = ((((data[OFFSET+5] & 0xff) << 8) | (data[OFFSET+6] & 0xff)) + 50000) / 100.0; + + let accelerationX = (data[OFFSET+7] << 8) | (data[OFFSET+8] & 0xff); + if (accelerationX > 32767) accelerationX -= 65536; // two's complement + robject.accelerationX = accelerationX / 1000.0; + + let accelerationY = (data[OFFSET+9] << 8) | (data[OFFSET+10] & 0xff); + if (accelerationY > 32767) accelerationY -= 65536; // two's complement + robject.accelerationY = accelerationY / 1000.0; + + let accelerationZ = (data[OFFSET+11] << 8) | (data[OFFSET+12] & 0xff); + if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement + robject.accelerationZ = accelerationZ / 1000.0; + + const powerInfo = ((data[OFFSET+13] & 0xff) << 8) | (data[OFFSET+14] & 0xff); + robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0; + robject.txPower = (powerInfo & 0b11111) * 2 - 40; + robject.movementCounter = data[OFFSET+15] & 0xff; + robject.measurementSequenceNumber = ((data[OFFSET+16] & 0xff) << 8) | (data[OFFSET+17] & 0xff); + + robject.mac = [ + int2Hex(data[OFFSET+18]), + int2Hex(data[OFFSET+19]), + int2Hex(data[OFFSET+20]), + int2Hex(data[OFFSET+21]), + int2Hex(data[OFFSET+22]), + int2Hex(data[OFFSET+23]) + ].join(':'); + + robject.name = "Ruuvi " + int2Hex(data[OFFSET+22]) + int2Hex(data[OFFSET+23]); + return robject; + } + + function getAge(created) { + const now = new Date().getTime(); + const ago = ((now - created) / 1000).toFixed(0); + return ago > 0 ? ago + "s ago" : "now"; + } + + function redraw() { + if (ruuvis.length > 0 && ruuvis[current]) { + const ruuvi = ruuvis[current]; + g.clear(); + g.setFontAlign(0,0); + g.setFont("Vector",12); + g.drawString(" (" + (current+1) + "/" + ruuvis.length + ")", g.getWidth()/2, 10); + g.setFont("Vector",20); + g.drawString(ruuvi.name, g.getWidth()/2, 30); + g.setFont("Vector",12); + const age = getAge(ruuvi.time); + if(age > (5*60)) { + g.setColor("#ff0000"); + } else if (age > 60) { + g.setColor("#f39c12"); + } else { + g.setColor("#2ecc71"); + } + g.drawString(age, g.getWidth()/2, 50); + g.setColor("#ffffff"); + g.setFont("Vector",60); + g.drawString(ruuvi.temperature.toFixed(2) + "°c", g.getWidth()/2, g.getHeight()/2); + g.setFontAlign(0,1); + g.setFont("Vector",20); + g.drawString(ruuvi.humidity + "% " + ruuvi.pressure + "hPa ", g.getWidth()/2, g.getHeight()-30); + g.setFont("Vector",12); + g.drawString(ruuvi.battery + "v", g.getWidth()/2, g.getHeight()-10); + } else { + g.clear(); + g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); + g.setFontAlign(0,0); + g.setFont("Vector",16); + g.drawString("Looking for Ruuvi...", g.getWidth()/2, g.getHeight()/2 + 50); + } + } + + function scan() { + NRF.findDevices(function(devices) { + let foundNew = false; + devices.forEach(device => { + const data = p(device.data); + data.time = new Date().getTime(); + const idx = lookup[data.name]; + if (idx !== undefined) { + ruuvis[idx] = data; + } else { + lookup[data.name] = ruuvis.push(data)-1; + foundNew = true; + } + }); + redraw(); + if (foundNew) { + Bangle.buzz(); + g.flip(); + } + + }, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] }); + } + + g.clear(); + g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); + + var drawInterval = setInterval(redraw, 1000); + var scanInterval = setInterval(scan, 10000); + setWatch(() => { + current--; + if (current < 0) { + current = ruuvis.length-1; + } + redraw(); + }, BTN1, {repeat:true}); + + setWatch(() => { + scan(); + }, BTN2, {repeat:true}); + + setWatch(() => { + current++; + if (current >= ruuvis.length) { + current = 0; + } + redraw(); + }, BTN3, {repeat:true}); + + scan(); \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.png b/apps/ruuviwatch/ruuviwatch.png new file mode 100644 index 000000000..3737a7e8c Binary files /dev/null and b/apps/ruuviwatch/ruuviwatch.png differ diff --git a/apps/s7clk/metadata.json b/apps/s7clk/metadata.json new file mode 100644 index 000000000..19aa0479d --- /dev/null +++ b/apps/s7clk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "s7clk", + "name": "Simple 7 segment Clock", + "version": "0.03", + "description": "A simple 7 segment Clock with date", + "icon": "icon.png", + "screenshots": [{"url":"screenshot_s7segment.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"s7clk.app.js","url":"app.js"}, + {"name":"s7clk.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/schoolCalendar/metadata.json b/apps/schoolCalendar/metadata.json new file mode 100644 index 000000000..e77956bb2 --- /dev/null +++ b/apps/schoolCalendar/metadata.json @@ -0,0 +1,19 @@ +{ "id": "schoolCalendar", + "name": "School Calendar", + "shortName":"SCalendar", + "icon": "CalenderLogo.png", + "version": "0.01", + "description": "A simple calendar that you can see your upcoming events that you create in the customizer. Keep in note that your events reapeat weekly.(Beta)", + "tags": "tool", + "readme":"README.md", + "custom":"custom.html", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"screenshot_basic.png"},{"url":"screenshot_info.png"}], + "storage": [ + {"name":"schoolCalendar.app.js"}, + {"name":"schoolCalendar.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"calendarItems.csv"} + ] +} diff --git a/apps/sclock/metadata.json b/apps/sclock/metadata.json new file mode 100644 index 000000000..97f5baf3d --- /dev/null +++ b/apps/sclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "sclock", + "name": "Simple Clock", + "version": "0.07", + "description": "A Simple Digital Clock", + "icon": "clock-simple.png", + "screenshots": [{"url":"screenshot_simplec.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"sclock.app.js","url":"clock-simple.js"}, + {"name":"sclock.img","url":"clock-simple-icon.js","evaluate":true} + ] +} diff --git a/apps/scolor/metadata.json b/apps/scolor/metadata.json new file mode 100644 index 000000000..c171aead5 --- /dev/null +++ b/apps/scolor/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "scolor", + "name": "Show Color", + "version": "0.01", + "description": "Display all available Colors and Names", + "icon": "show-color.png", + "type": "app", + "tags": "tool", + "screenshots": [{"url":"bangle1-view-color-screenshot.png"}], + "supports": ["BANGLEJS"], + "allow_emulator": true, + "storage": [ + {"name":"scolor.app.js","url":"show-color.js"}, + {"name":"scolor.img","url":"show-color-icon.js","evaluate":true} + ] +} diff --git a/apps/score/metadata.json b/apps/score/metadata.json new file mode 100644 index 000000000..fd72e197d --- /dev/null +++ b/apps/score/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "score", + "name": "Score Tracker", + "version": "0.01", + "description": "Score Tracker for sports that use plain numbers (e.g. Badminton, Volleyball, Soccer, Table Tennis, ...). Also supports tennis scoring.", + "icon": "score.app.png", + "screenshots": [{"url":"screenshot_score.png"}], + "type": "app", + "tags": "", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"score.app.js","url":"score.app.js"}, + {"name":"score.settings.js","url":"score.settings.js"}, + {"name":"score.presets.json","url":"score.presets.json"}, + {"name":"score.img","url":"score.app-icon.js","evaluate":true} + ], + "data": [{"name":"score.json"}] +} 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/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/metadata.json b/apps/scribble/metadata.json new file mode 100644 index 000000000..eaec185ee --- /dev/null +++ b/apps/scribble/metadata.json @@ -0,0 +1,19 @@ +{ "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" } + ] +} 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 new file mode 100644 index 000000000..33e44b70c --- /dev/null +++ b/apps/sensible/ChangeLog @@ -0,0 +1,5 @@ +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 +0.05: Refactored for efficiency, corrected sensor value inaccuracies \ No newline at end of file diff --git a/apps/sensible/README.md b/apps/sensible/README.md new file mode 100644 index 000000000..fcff3b0f9 --- /dev/null +++ b/apps/sensible/README.md @@ -0,0 +1,35 @@ +# Sensible + +Collect all the sensor data from the Bangle.js 2, display the live readings in menu pages, and broadcast in Bluetooth Low Energy (BLE) advertising packets to any listening devices in range. + + +## Usage + +The advertising packets will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the standard packet types. Also convenient for testing individual sensors of the Bangle.js 2 via the menu interface. + + +## Features + +Currently implements: +- Accelerometer +- Barometer +- GPS +- Heart Rate Monitor +- Magnetometer + +in the menu display, and broadcasts all sensor data readings _except_ acceleration in Bluetooth Low Energy advertising packets as GATT characteristic services. + + +## Controls + +Browse and control sensors using the standard Espruino menu interface. + + +## Requests + +[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates. + + +## Creator + +Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com) diff --git a/apps/sensible/metadata.json b/apps/sensible/metadata.json new file mode 100644 index 000000000..df0421441 --- /dev/null +++ b/apps/sensible/metadata.json @@ -0,0 +1,25 @@ +{ +"id": "sensible", +"name": "SensiBLE", +"shortName": "SensiBLE", +"version": "0.05", +"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,bluetooth", +"supports" : [ "BANGLEJS2" ], +"allow_emulator": true, +"readme": "README.md", +"storage": [ + { "name": "sensible.app.js", "url": "sensible.js" }, + { "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true } +] +} 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-icon.js b/apps/sensible/sensible-icon.js new file mode 100644 index 000000000..f904fc7f3 --- /dev/null +++ b/apps/sensible/sensible-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkG/4AG+cilGIxGCkU/B44AGmQUBAAsjCyoYN+QWJAAMvCxsjLQXzG4gYIOIZwG+YLDCw34BRIkFx4JFHQRDElGCJYgOCFw5RCPQwJFGAg4BIoSRIDAQQEG4YLBHgYAGJQIjCJ4RGBDoU4SIqNDwYwDJAQEDFwSRGDAQfBFQgIDFwQtDRoowBAgQDEDYQzC7oACTogrEA4IfF/4WDDAY/Fx4CCEYQbB/oXF74TDCAYGBUoIDDCwowCUoIkBAYSABGwIDCLogADBIKMCAYRODLwRGGJAaMFPwghBnoXJHoJ8DF4Q5DC5HTKogVBgAAFpoXH6oQGAA1dC7/UC5sNC4/dCA0QAwsEC50BC40AC5FQC4sgMB4XFgUwC40FC4/QBwkD+B5HDA6oFh/xSREFqtVbogMEj/yVxkFMwRgEl//Y5sAqhgF///SA4AHghgDgQXBPBAAHrpICh4XBMBoADC4ReBAALxHABUBCwX/bI4AKgYXD+YXRn4XDSKCNDAAZ5QOoZhSLohhESRkBLopJQIo4YOCxYYCJQ0BCxoACmURCoMRkYOI")) \ No newline at end of file diff --git a/apps/sensible/sensible.js b/apps/sensible/sensible.js new file mode 100644 index 000000000..73c348556 --- /dev/null +++ b/apps/sensible/sensible.js @@ -0,0 +1,268 @@ +/** + * Copyright reelyActive 2021 + * We believe in an open Internet of Things + */ + + +// 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 +let acc, bar, hrm, mag; +let isAccMenu = false; +let isBarMenu = false; +let isGpsMenu = false; +let isHrmMenu = false; +let isMagMenu = false; +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 +let mainMenu = { + "": { "title": "-- SensiBLE --" }, + "Acceleration": function() { E.showMenu(accMenu); isAccMenu = true; }, + "Barometer": function() { E.showMenu(barMenu); isBarMenu = true; }, + "GPS": function() { E.showMenu(gpsMenu); isGpsMenu = true; }, + "Heart Rate": function() { E.showMenu(hrmMenu); isHrmMenu = true; }, + "Magnetometer": function() { E.showMenu(magMenu); isMagMenu = true; } +}; +let accMenu = { + "": { "title" : "- Acceleration -" }, + "State": { value: "On" }, + "x": { value: null }, + "y": { value: null }, + "z": { value: null }, + "<-": function() { E.showMenu(mainMenu); isAccMenu = false; }, +}; +let barMenu = { + "": { "title" : "- Barometer -" }, + "State": { + value: isBarEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isBarEnabled = v; Bangle.setBarometerPower(v, APP_ID); } + }, + "Altitude": { value: null }, + "Press": { value: null }, + "Temp": { value: null }, + "<-": function() { E.showMenu(mainMenu); isBarMenu = false; }, +}; +let gpsMenu = { + "": { "title" : "- GPS -" }, + "State": { + value: isGpsEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isGpsEnabled = v; Bangle.setGPSPower(v, APP_ID); } + }, + "Lat": { value: null }, + "Lon": { value: null }, + "Altitude": { value: null }, + "Satellites": { value: null }, + "HDOP": { value: null }, + "<-": function() { E.showMenu(mainMenu); isGpsMenu = false; }, +}; +let hrmMenu = { + "": { "title" : "- Heart Rate -" }, + "State": { + value: isHrmEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isHrmEnabled = v; Bangle.setHRMPower(v, APP_ID); } + }, + "BPM": { value: null }, + "Confidence": { value: null }, + "<-": function() { E.showMenu(mainMenu); isHrmMenu = false; }, +}; +let magMenu = { + "": { "title" : "- Magnetometer -" }, + "State": { + value: isMagEnabled, + format: v => v ? "On" : "Off", + onchange: v => { isMagEnabled = v; Bangle.setCompassPower(v, APP_ID); } + }, + "x": { value: null }, + "y": { value: null }, + "z": { value: null }, + "Heading": { value: null }, + "<-": function() { E.showMenu(mainMenu); isMagMenu = false; }, +}; + + +// 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; + } + + let interval = 1000 / data.length; + NRF.setAdvertising(data, { showName: false, interval: interval }); +} + + +// Encode the bar service data to fit in a Bluetooth PDU +function encodeBarServiceData() { + let t = toByteArray(Math.round(bar.temperature * 100), 2, true); + let p = toByteArray(Math.round(bar.pressure * 1000), 4, false); + let e = toByteArray(Math.round(bar.altitude * 100), 3, true); + + 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 s = toByteArray(Math.round(1000 * gps.speed / 36), 2, false); + let lat = toByteArray(Math.round(gps.lat * 10000000), 4, true); + let lon = toByteArray(Math.round(gps.lon * 10000000), 4, true); + let e = toByteArray(Math.round(gps.alt * 100), 3, true); + let h = toByteArray(Math.round(gps.course * 100), 2, false); + + return [ + 0x02, 0x01, 0x06, // Flags + 0x14, 0x16, 0x67, 0x2a, 0x9d, 0x02, s[0], s[1], lat[0], lat[1], lat[2], + lat[3], lon[0], lon[1], lon[2], lon[3], e[0], e[1], e[2], h[0], h[1] + // Location and Speed + ]; +} + + +// Encode the mag service data using the magnetic flux density 3D characteristic +function encodeMagServiceData() { + let x = toByteArray(mag.x, 2, true); + let y = toByteArray(mag.y, 2, true); + let z = toByteArray(mag.z, 2, true); + + return [ + 0x02, 0x01, 0x06, // Flags + 0x09, 0x16, 0xa1, 0x2a, x[0], x[1], y[0], y[1], z[0], z[1] // Mag 3D + ]; +} + + +// Convert the given value to a little endian byte array +function toByteArray(value, numberOfBytes, isSigned) { + let byteArray = new Array(numberOfBytes); + + if(isSigned && (value < 0)) { + value += 1 << (numberOfBytes * 8); + } + + for(let index = 0; index < numberOfBytes; index++) { + byteArray[index] = (value >> (index * 8)) & 0xff; + } + + return byteArray; +} + + +// Update acceleration +Bangle.on('accel', function(newAcc) { + acc = newAcc; + isNewAccData = true; + + if(isAccMenu) { + accMenu.x.value = acc.x.toFixed(2); + accMenu.y.value = acc.y.toFixed(2); + accMenu.z.value = acc.z.toFixed(2); + E.showMenu(accMenu); + } +}); + +// Update barometer +Bangle.on('pressure', function(newBar) { + bar = newBar; + isNewBarData = true; + + if(isBarMenu) { + barMenu.Altitude.value = bar.altitude.toFixed(1) + 'm'; + barMenu.Press.value = bar.pressure.toFixed(1) + 'mbar'; + barMenu.Temp.value = bar.temperature.toFixed(1) + 'C'; + E.showMenu(barMenu); + } +}); + +// Update GPS +Bangle.on('GPS', function(newGps) { + gps = newGps; + isNewGpsData = true; + + if(isGpsMenu) { + gpsMenu.Lat.value = gps.lat.toFixed(4); + gpsMenu.Lon.value = gps.lon.toFixed(4); + gpsMenu.Altitude.value = gps.alt + 'm'; + gpsMenu.Satellites.value = gps.satellites; + gpsMenu.HDOP.value = (gps.hdop * 5).toFixed(1) + 'm'; + E.showMenu(gpsMenu); + } +}); + +// Update heart rate monitor +Bangle.on('HRM', function(newHrm) { + hrm = newHrm; + isNewHrmData = true; + + if(isHrmMenu) { + hrmMenu.BPM.value = hrm.bpm; + hrmMenu.Confidence.value = hrm.confidence + '%'; + E.showMenu(hrmMenu); + } +}); + +// Update magnetometer +Bangle.on('mag', function(newMag) { + mag = newMag; + isNewMagData = true; + + if(isMagMenu) { + magMenu.x.value = mag.x; + magMenu.y.value = mag.y; + magMenu.z.value = mag.z; + magMenu.Heading.value = mag.heading.toFixed(1); + E.showMenu(magMenu); + } +}); + + +// On start: enable sensors and display main menu +g.clear(); +Bangle.setBarometerPower(isBarEnabled, APP_ID); +Bangle.setGPSPower(isGpsEnabled, APP_ID); +Bangle.setHRMPower(isHrmEnabled, APP_ID); +Bangle.setCompassPower(isMagEnabled, APP_ID); +E.showMenu(mainMenu); +setInterval(transmitUpdatedSensorData, 1000); \ No newline at end of file diff --git a/apps/sensible/sensible.png b/apps/sensible/sensible.png new file mode 100644 index 000000000..d3e3dfbef Binary files /dev/null and b/apps/sensible/sensible.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index faa50405f..77c7b2040 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -36,3 +36,11 @@ 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 +0.38: Restructed menus as per forum discussion +0.39: Fix misbehaving debug info option +0.40: Moved off into Utils, put System after Apps +0.41: Stop users disabling all wake-up methods and locking themselves out (fix #1272) diff --git a/apps/setting/README.md b/apps/setting/README.md index 1875fc3b0..42e3939fb 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. @@ -32,9 +31,12 @@ This is Bangle.js's settings menu * **LCD Brightness** set how bright the LCD is. Due to hardware limitations in the LCD backlight, you may notice flicker if the LCD is not at 100% brightness. * **LCD Timeout** how long should the LCD stay on for if no activity is detected. 0=stay on forever * **Wake on X** should the given activity wake up the Bangle.js LCD? + * On Bangle.js 2 when locked the touchscreen is turned off to save power. Because of this, + `Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js. * **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement. + ## Quiet Mode Quiet Mode is a hint to apps and widgets that you do not want to be disturbed. @@ -44,6 +46,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/metadata.json b/apps/setting/metadata.json new file mode 100644 index 000000000..1e82f97b4 --- /dev/null +++ b/apps/setting/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "setting", + "name": "Settings", + "version": "0.41", + "description": "A menu for setting up Bangle.js", + "icon": "settings.png", + "tags": "tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"setting.app.js","url":"settings.js"}, + {"name":"setting.img","url":"settings-icon.js","evaluate":true} + ], + "data": [{"name":"setting.json","url":"settings.min.json","evaluate":true}], + "sortorder": -5 +} diff --git a/apps/setting/settings.js b/apps/setting/settings.js index fcf651b6f..65e076753 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -7,17 +7,22 @@ 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) + var o = settings.options; + // Check to make sure nobody disabled all wakeups and locked themselves out! + if (BANGLEJS2) { + if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) { + o.wakeOnBTN1 = true; + } + } else { + if (!(o.wakeOnBTN1||o.wakeOnBTN2||o.wakeOnBTN3||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) + o.wakeOnBTN2 = true; } + updateSettings(); + Bangle.setOptions(o) } function gToInternal(g) { @@ -56,22 +61,46 @@ 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() { + + const mainmenu = { + '': { 'title': /*LANG*/'Settings' }, + '< Back': ()=>load(), + /*LANG*/'Apps': ()=>showAppSettingsMenu(), + /*LANG*/'System': ()=>showSystemMenu(), + /*LANG*/'Bluetooth': ()=>showBLEMenu(), + /*LANG*/'Alerts': ()=>showAlertsMenu(), + /*LANG*/'Utils': ()=>showUtilMenu() + }; + + return E.showMenu(mainmenu); +} + +function showSystemMenu() { + + const mainmenu = { + '': { 'title': /*LANG*/'System' }, + '< Back': ()=>showMainMenu(), + /*LANG*/'Theme': ()=>showThemeMenu(), + /*LANG*/'LCD': ()=>showLCDMenu(), + /*LANG*/'Locale': ()=>showLocaleMenu(), + /*LANG*/'Select Clock': ()=>showClockMenu(), + /*LANG*/'Set Time': ()=>showSetTimeMenu() + }; + + return E.showMenu(mainmenu); +} + +function showAlertsMenu() { var beepMenuItem; if (BANGLEJS2) { beepMenuItem = { @@ -88,7 +117,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, @@ -102,23 +131,11 @@ 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': { + '': { 'title': /*LANG*/'Alerts' }, + '< Back': ()=>showMainMenu(), + /*LANG*/'Beep': beepMenuItem, + /*LANG*/'Vibration': { value: settings.vibrate, format: boolFormat, onchange: () => { @@ -130,25 +147,30 @@ 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(); + }, + } }; return E.showMenu(mainmenu); } + function showBLEMenu() { - var hidV = [false, "kbmedia", "kb", "joy"]; - var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"]; + var hidV = [false, "kbmedia", "kb", "com", "joy"]; + var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"]; E.showMenu({ + '': { 'title': 'Bluetooth' }, '< Back': ()=>showMainMenu(), - 'BLE': { + /*LANG*/'Make Connectable': ()=>makeConnectable(), + /*LANG*/'BLE': { value: settings.ble, format: boolFormat, onchange: () => { @@ -156,7 +178,7 @@ function showBLEMenu() { updateSettings(); } }, - 'Programmable': { + /*LANG*/'Programmable': { value: settings.blerepl, format: boolFormat, onchange: () => { @@ -164,7 +186,7 @@ function showBLEMenu() { updateSettings(); } }, - 'HID': { + /*LANG*/'HID': { value: Math.max(0,0 | hidV.indexOf(settings.HID)), min: 0, max: 3, format: v => hidN[v], @@ -173,11 +195,11 @@ function showBLEMenu() { updateSettings(); } }, - 'Passkey BETA': { + /*LANG*/'Passkey BETA': { value: settings.passkey?settings.passkey:"none", onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, - 'Whitelist': { + /*LANG*/'Whitelist': { value: settings.whitelist?(settings.whitelist.length+" devs"):"off", onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call } @@ -199,9 +221,9 @@ function showThemeMenu() { m.draw(); } var m = E.showMenu({ - '':{title:'Theme'}, - '< Back': ()=>showMainMenu(), - 'Dark BW': ()=>{ + '':{title:/*LANG*/'Theme'}, + '< Back': ()=>showSystemMenu(), + /*LANG*/'Dark BW': ()=>{ upd({ fg:cl("#fff"), bg:cl("#000"), fg2:cl("#0ff"), bg2:cl("#000"), @@ -209,7 +231,7 @@ function showThemeMenu() { dark:true }); }, - 'Light BW': ()=>{ + /*LANG*/'Light BW': ()=>{ upd({ fg:cl("#000"), bg:cl("#fff"), fg2:cl("#000"), bg2:cl("#cff"), @@ -217,7 +239,7 @@ function showThemeMenu() { dark:false }); }, - 'Customize': ()=>showCustomThemeMenu(), + /*LANG*/'Customize': ()=>showCustomThemeMenu(), }); function showCustomThemeMenu() { @@ -249,9 +271,9 @@ function showThemeMenu() { "< Back": () => showThemeMenu() }; const labels = { - fg: 'Foreground', bg: 'Background', - fg2: 'Foreground 2', bg2: 'Background 2', - fgH: 'Highlight FG', bgH: 'Highlight BG', + fg: /*LANG*/'Foreground', bg: /*LANG*/'Background', + fg2: /*LANG*/'Foreground 2', bg2: /*LANG*/'Background 2', + fgH: /*LANG*/'Highlight FG', bgH: /*LANG*/'Highlight BG', }; ["fg", "bg", "fg2", "bg2", "fgH", "bgH"].forEach(t => { menu[labels[t]] = { @@ -280,14 +302,16 @@ function showThemeMenu() { function showPasskeyMenu() { var menu = { "< Back" : ()=>showBLEMenu(), - "Disable" : () => { + /*LANG*/"Disable" : () => { settings.passkey = undefined; updateSettings(); 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], @@ -306,7 +330,7 @@ function showPasskeyMenu() { function showWhitelistMenu() { var menu = { "< Back" : ()=>showBLEMenu(), - "Disable" : () => { + /*LANG*/"Disable" : () => { settings.whitelist = undefined; updateSettings(); showBLEMenu(); @@ -314,7 +338,7 @@ function showWhitelistMenu() { }; if (settings.whitelist) settings.whitelist.forEach(function(d){ menu[d.substr(0,17)] = function() { - E.showPrompt('Remove\n'+d).then((v) => { + E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { if (v) { settings.whitelist.splice(settings.whitelist.indexOf(d),1); updateSettings(); @@ -323,8 +347,8 @@ function showWhitelistMenu() { }); } }); - menu['Add Device']=function() { - E.showAlert("Connect device\nto add to\nwhitelist","Whitelist").then(function() { + menu[/*LANG*/'Add Device']=function() { + E.showAlert(/*LANG*/"Connect device\nto add to\nwhitelist",/*LANG*/"Whitelist").then(function() { NRF.removeAllListeners('connect'); showWhitelistMenu(); }); @@ -343,8 +367,8 @@ function showWhitelistMenu() { function showLCDMenu() { const lcdMenu = { '': { 'title': 'LCD' }, - '< Back': ()=>showMainMenu(), - 'LCD Brightness': { + '< Back': ()=>showSystemMenu(), + /*LANG*/'LCD Brightness': { value: settings.brightness, min: 0.1, max: 1, @@ -352,12 +376,10 @@ 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': { + /*LANG*/'LCD Timeout': { value: settings.timeout, min: 0, max: 60, @@ -365,12 +387,10 @@ 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': { + /*LANG*/'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { @@ -381,7 +401,7 @@ function showLCDMenu() { }; if (!BANGLEJS2) Object.assign(lcdMenu, { - 'Wake on BTN2': { + /*LANG*/'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { @@ -389,7 +409,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Wake on BTN3': { + /*LANG*/'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { @@ -398,7 +418,7 @@ function showLCDMenu() { } }}); Object.assign(lcdMenu, { - 'Wake on FaceUp': { + /*LANG*/'Wake on FaceUp': { value: settings.options.wakeOnFaceUp, format: boolFormat, onchange: () => { @@ -406,7 +426,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Wake on Touch': { + /*LANG*/'Wake on Touch': { value: settings.options.wakeOnTouch, format: boolFormat, onchange: () => { @@ -414,7 +434,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Wake on Twist': { + /*LANG*/'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { @@ -422,7 +442,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Twist Threshold': { + /*LANG*/'Twist Threshold': { value: internalToG(settings.options.twistThreshold), min: -0.5, max: 0.5, @@ -432,7 +452,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Twist Max Y': { + /*LANG*/'Twist Max Y': { value: settings.options.twistMaxY, min: -1500, max: 1500, @@ -442,7 +462,7 @@ function showLCDMenu() { updateOptions(); } }, - 'Twist Timeout': { + /*LANG*/'Twist Timeout': { value: settings.options.twistTimeout, min: 0, max: 2000, @@ -455,111 +475,12 @@ 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 = { - '': { 'title': 'Locale' }, - '< Back': ()=>showMainMenu(), - 'Time Zone': { + '': { 'title': /*LANG*/'Locale' }, + '< Back': ()=>showSystemMenu(), + /*LANG*/'Time Zone': { value: settings.timezone, min: -11, max: 13, @@ -569,7 +490,7 @@ function showLocaleMenu() { updateSettings(); } }, - 'Clock Style': { + /*LANG*/'Clock Style': { value: !!settings["12hour"], format: v => v ? "12hr" : "24hr", onchange: v => { @@ -581,28 +502,73 @@ function showLocaleMenu() { return E.showMenu(localemenu); } -function showResetMenu() { - const resetmenu = { - '': { 'title': 'Reset' }, +function showUtilMenu() { + var menu = { + '': { 'title': /*LANG*/'Utilities' }, '< Back': ()=>showMainMenu(), - 'Reset Settings': () => { - E.showPrompt('Reset Settings?').then((v) => { + /*LANG*/'Debug Info': { + value: E.clip(0|settings.log,0,2), + min: 0, + max: 2, + format: v => [/*LANG*/"Hide",/*LANG*/"Show",/*LANG*/"Log"][E.clip(0|v,0,2)], + onchange: v => { + settings.log = v; + updateSettings(); + } + }, + /*LANG*/'Compact Storage': () => { + E.showMessage(/*LANG*/"Compacting...\nTakes approx\n1 minute",{title:/*LANG*/"Storage"}); + require("Storage").compact(); + showUtilMenu(); + }, + /*LANG*/'Rewrite Settings': () => { + require("Storage").write(".boot0","eval(require('Storage').read('bootupdate.js'));"); + load("setting.app.js"); + }, + /*LANG*/'Flatten Battery': () => { + E.showMessage(/*LANG*/'Flattening battery - this can take hours.\nLong-press button to cancel.'); + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat"); + 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); + }, + /*LANG*/'Reset Settings': () => { + E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => { if (v) { E.showMessage('Resetting'); resetSettings(); - } - setTimeout(showMainMenu, 50); + setTimeout(showMainMenu, 50); + } else showUtilMenu(); + }); + }, + /*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() } + }; + if (Bangle.factoryReset) { + menu[/*LANG*/'Factory Reset'] = ()=>{ + E.showPrompt(/*LANG*/'This will remove everything!',{title:/*LANG*/"Factory Reset"}).then((v) => { + if (v) { + E.showMessage(); + Terminal.setConsole(); + Bangle.factoryReset(); + } else showUtilMenu(); }); } - }; - return E.showMenu(resetmenu); + } + + return E.showMenu(menu); } function makeConnectable() { try { NRF.wake(); } catch (e) { } Bluetooth.setConsole(1); var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", ""); - E.showPrompt(name + "\nStay Connectable?", { title: "Connectable" }).then(r => { + E.showPrompt(name + /*LANG*/"\nStay Connectable?", { title: /*LANG*/"Connectable" }).then(r => { if (settings.ble != r) { settings.ble = r; updateSettings(); @@ -618,9 +584,9 @@ function showClockMenu() { .sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { - 'title': 'Select Clock', + 'title': /*LANG*/'Select Clock', }, - '< Back': ()=>showMainMenu(), + '< Back': ()=>showSystemMenu(), }; clockApps.forEach((app, index) => { var label = app.name; @@ -636,7 +602,7 @@ function showClockMenu() { }; }); if (clockApps.length === 0) { - clockMenu["No Clocks Found"] = () => { }; + clockMenu[/*LANG*/"No Clocks Found"] = () => { }; } return E.showMenu(clockMenu); } @@ -644,47 +610,47 @@ function showClockMenu() { function showSetTimeMenu() { d = new Date(); const timemenu = { - '': { 'title': 'Set Time' }, + '': { 'title': /*LANG*/'Set Time' }, '< Back': function () { setTime(d.getTime() / 1000); - showMainMenu(); + showSystemMenu(); }, - 'Hour': { + /*LANG*/'Hour': { value: d.getHours(), onchange: function (v) { this.value = (v+24)%24; d.setHours(this.value); } }, - 'Minute': { + /*LANG*/'Minute': { value: d.getMinutes(), onchange: function (v) { this.value = (v+60)%60; d.setMinutes(this.value); } }, - 'Second': { + /*LANG*/'Second': { value: d.getSeconds(), onchange: function (v) { this.value = (v+60)%60; d.setSeconds(this.value); } }, - 'Date': { + /*LANG*/'Date': { value: d.getDate(), onchange: function (v) { this.value = ((v+30)%31)+1; d.setDate(this.value); } }, - 'Month': { + /*LANG*/'Month': { value: d.getMonth() + 1, onchange: function (v) { this.value = ((v+11)%12)+1; d.setMonth(this.value - 1); } }, - 'Year': { + /*LANG*/'Year': { value: d.getFullYear(), min: 2019, max: 2100, @@ -698,7 +664,7 @@ function showSetTimeMenu() { function showAppSettingsMenu() { let appmenu = { - '': { 'title': 'App Settings' }, + '': { 'title': /*LANG*/'App Settings' }, '< Back': ()=>showMainMenu(), } const apps = storage.list(/\.settings\.js$/) @@ -715,7 +681,7 @@ function showAppSettingsMenu() { return 0; }) if (apps.length === 0) { - appmenu['No app has settings'] = () => { }; + appmenu[/*LANG*/'No app has settings'] = () => { }; } apps.forEach(function (app) { appmenu[app.name] = () => { showAppSettings(app) }; @@ -732,17 +698,17 @@ function showAppSettings(app) { appSettings = eval(appSettings); } catch (e) { console.log(`${app.name} settings error:`, e) - return showError('Error in settings'); + return showError(/*LANG*/'Error in settings'); } if (typeof appSettings !== "function") { - return showError('Invalid settings'); + return showError(/*LANG*/'Invalid settings'); } try { // pass showAppSettingsMenu as "back" argument appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) - return showError('Error in settings'); + return showError(/*LANG*/'Error in settings'); } } diff --git a/apps/shortcuts/metadata.json b/apps/shortcuts/metadata.json new file mode 100644 index 000000000..2351a102f --- /dev/null +++ b/apps/shortcuts/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "shortcuts", + "name": "Shortcuts", + "shortName": "Shortcuts", + "version": "0.01", + "description": "Quickly load your favourite apps from (almost) any watch face.", + "icon": "app.png", + "type": "bootloader", + "tags": "tool", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"shortcuts.boot.js","url":"boot.js"}, + {"name":"shortcuts.settings.js","url":"settings.js"} + ], + "data": [{"name":"shortcuts.json"}] +} diff --git a/apps/showimg/ChangeLog b/apps/showimg/ChangeLog new file mode 100644 index 000000000..e8d890f83 --- /dev/null +++ b/apps/showimg/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial release +0.02: Fixed launcher image diff --git a/apps/showimg/README.md b/apps/showimg/README.md new file mode 100644 index 000000000..0624fd962 --- /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 "showimg.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..abb1eb434 --- /dev/null +++ b/apps/showimg/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4Aah//BRQAMDowUNC5AARC4YKKL5gTC+B3TCpAyIC5oNBEA4XNJwS4GC55pHC8TEHC57QHC4wSEC5YpEC6YwEC5oEEC5x3DC6ZHbC7PwcYxfNAYYXPJA4XQDAwKEBYQXJIoReHC5gMFAAojBC5QUIC5Y5JMgYXIUQYJFPggXMAwICCBAYXMCAQJDDwQUCC5QOCUwQdEC5QqFDghNFC5wrEC5gQDPgoTCDYYXFMAgXaCQoXJEwZ4FLQbhFC4imDAAglFC5QAGBgYXKIoYWIC5YYFG4ZkDC4YjCYYwAJC4gASC6THFH5pqGAAY")) 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/showimg/metadata.json b/apps/showimg/metadata.json new file mode 100644 index 000000000..d5e44c0ee --- /dev/null +++ b/apps/showimg/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "showimg", + "name": "simple image viewer", + "shortName":"showImage", + "version":"0.02", + "description": "Displays the image in \"showimg.user.img\". The file has to be uploaded via the espruino IDE. 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} + ] +} diff --git a/apps/simple_clock/LICENSE b/apps/simple_clock/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/simple_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/simple_clock/README.md b/apps/simple_clock/README.md new file mode 100644 index 000000000..d44c495c6 --- /dev/null +++ b/apps/simple_clock/README.md @@ -0,0 +1,14 @@ +# Simple Analog Clock # + +This app displays a simple, yet stylish, analog clock. It considers the +currently configured "theme" (and may therefore look different than shown in +the screenshot on your watch depending on which theme you prefer). + +![](app-screenshot.png) + +This clock also acts as an example for the building blocks found in the author's +[GitHub repository](https://github.com/rozek/banglejs-2-activities) + +## License ## + +[MIT License](LICENSE) diff --git a/apps/simple_clock/app-icon.js b/apps/simple_clock/app-icon.js new file mode 100644 index 000000000..9bc0ebfe1 --- /dev/null +++ b/apps/simple_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBIf4AOgPAjgROh/A/+AEZ8DCKH8Gp/4Gp0QCKAARgQRigFACMUICMT7SEcUAkAvK/EAv//BpH8eoYOBAQP//0ECIrvDCIQABj4TB8AREj4RCgIyFn4RJh5HBCJQ1DAA0/UKBuJQZIRgL4wRL4ARhAH4AIg4RQdIwRcnAjiLKIA/ACI=")) \ No newline at end of file diff --git a/apps/simple_clock/app-icon.png b/apps/simple_clock/app-icon.png new file mode 100644 index 000000000..b1dcc461e Binary files /dev/null and b/apps/simple_clock/app-icon.png differ diff --git a/apps/simple_clock/app-screenshot.png b/apps/simple_clock/app-screenshot.png new file mode 100644 index 000000000..ec99c9fc6 Binary files /dev/null and b/apps/simple_clock/app-screenshot.png differ diff --git a/apps/simple_clock/app.js b/apps/simple_clock/app.js new file mode 100644 index 000000000..3c1843cb0 --- /dev/null +++ b/apps/simple_clock/app.js @@ -0,0 +1,230 @@ + let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; + let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; + + let outerRadius = Math.min(CenterX,CenterY) * 0.9; + + Bangle.loadWidgets(); + +/**** updateClockFaceSize ****/ + + function updateClockFaceSize () { + CenterX = ScreenWidth/2; + CenterY = ScreenHeight/2; + + outerRadius = Math.min(CenterX,CenterY) * 0.9; + + if (global.WIDGETS == null) { return; } + + let WidgetLayouts = { + tl:{ x:0, y:0, Direction:0 }, + tr:{ x:ScreenWidth-1, y:0, Direction:1 }, + bl:{ x:0, y:ScreenHeight-24, Direction:0 }, + br:{ x:ScreenWidth-1, y:ScreenHeight-24, Direction:1 } + }; + + for (let Widget of WIDGETS) { + let WidgetLayout = WidgetLayouts[Widget.area]; // reference, not copy! + if (WidgetLayout == null) { continue; } + + Widget.x = WidgetLayout.x - WidgetLayout.Direction * Widget.width; + Widget.y = WidgetLayout.y; + + WidgetLayout.x += Widget.width * (1-2*WidgetLayout.Direction); + } + + let x,y, dx,dy; + let cx = CenterX, cy = CenterY, r = outerRadius, r2 = r*r; + + x = WidgetLayouts.tl.x; y = WidgetLayouts.tl.y+24; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + x = WidgetLayouts.tr.x; y = WidgetLayouts.tr.y+24; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + x = WidgetLayouts.bl.x; y = WidgetLayouts.bl.y; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + x = WidgetLayouts.br.x; y = WidgetLayouts.br.y; dx = x - cx; dy = y - cy; + if (dx*dx + dy*dy < r2) { + cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.sqrt(r2); + } + + CenterX = cx; CenterY = cy; outerRadius = r * 0.9; + } + + updateClockFaceSize(); + +/**** custom version of Bangle.drawWidgets (does not clear the widget areas) ****/ + + Bangle.drawWidgets = function () { + var w = g.getWidth(), h = g.getHeight(); + + var pos = { + tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left + tr:{x:w-1, y:0, r:1, c:0}, + bl:{x:0, y:h-24, r:0, c:0}, + br:{x:w-1, y:h-24, r:1, c:0} + }; + + if (global.WIDGETS) { + for (var wd of WIDGETS) { + var p = pos[wd.area]; + if (!p) continue; + + wd.x = p.x - p.r*wd.width; + wd.y = p.y; + + p.x += wd.width*(1-2*p.r); + p.c++; + } + + g.reset(); // also loads the current theme + + if (pos.tl.c || pos.tr.c) { + g.setClipRect(0,h-24,w-1,h-1); + g.reset(); // also (re)loads the current theme + } + + if (pos.bl.c || pos.br.c) { + g.setClipRect(0,h-24,w-1,h-1); + g.reset(); // also (re)loads the current theme + } + + try { + for (wd of WIDGETS) { + g.clearRect(wd.x,wd.y, wd.x+wd.width-1,23); + wd.draw(wd); + } + } catch (e) { print(e); } + } + }; + + let HourHandLength = outerRadius * 0.5; + let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2; + + let MinuteHandLength = outerRadius * 0.7; + let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2; + + let SecondHandLength = outerRadius * 0.9; + let SecondHandOffset = 6; + + let twoPi = 2*Math.PI; + let Pi = Math.PI; + let halfPi = Math.PI/2; + + let sin = Math.sin, cos = Math.cos; + + let HourHandPolygon = [ + -halfHourHandWidth,halfHourHandWidth, + -halfHourHandWidth,halfHourHandWidth-HourHandLength, + halfHourHandWidth,halfHourHandWidth-HourHandLength, + halfHourHandWidth,halfHourHandWidth, + ]; + + let MinuteHandPolygon = [ + -halfMinuteHandWidth,halfMinuteHandWidth, + -halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength, + halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength, + halfMinuteHandWidth,halfMinuteHandWidth, + ]; + +/**** drawClockFace ****/ + + function drawClockFace () { + g.setColor(g.theme.fg); + g.setFont('Vector', 22); + + g.setFontAlign(0,-1); + g.drawString('12', CenterX,CenterY-outerRadius); + + g.setFontAlign(1,0); + g.drawString('3', CenterX+outerRadius,CenterY); + + g.setFontAlign(0,1); + g.drawString('6', CenterX,CenterY+outerRadius); + + g.setFontAlign(-1,0); + g.drawString('9', CenterX-outerRadius,CenterY); + } + +/**** transforme polygon ****/ + + let transformedPolygon = new Array(HourHandPolygon.length); + + function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } + } + +/**** draw clock hands ****/ + + function drawClockHands () { + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + let Seconds = now.getSeconds(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + g.setColor(g.theme.fg); + + transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + g.setColor(g.theme.fg2); + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + } + +/**** refreshDisplay ****/ + + let Timer; + function refreshDisplay () { + g.clear(true); // also loads current theme + + Bangle.drawWidgets(); + + drawClockFace(); + drawClockHands(); + + let Pause = 1000 - (Date.now() % 1000); + Timer = setTimeout(refreshDisplay,Pause); + } + + setTimeout(refreshDisplay, 500); // enqueue first draw request + + Bangle.on('lcdPower', (on) => { + if (on) { + if (Timer != null) { clearTimeout(Timer); Timer = undefined; } + refreshDisplay(); + } + }); + + Bangle.loadWidgets(); + + Bangle.setUI('clock'); diff --git a/apps/simple_clock/metadata.json b/apps/simple_clock/metadata.json new file mode 100644 index 000000000..ccec0bfbc --- /dev/null +++ b/apps/simple_clock/metadata.json @@ -0,0 +1,17 @@ +{ "id": "simple_clock", + "name": "Simple Analog Clock", + "shortName":"Simple Clock", + "version":"0.02", + "description": "a simple, yet stylish, analog clock", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"app-screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"simple_clock.app.js","url":"app.js"}, + {"name":"simple_clock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/simplest/ChangeLog b/apps/simplest/ChangeLog index f37015d6a..e7ab5f2c3 100644 --- a/apps/simplest/ChangeLog +++ b/apps/simplest/ChangeLog @@ -1,3 +1,6 @@ 0.01: Modified for use with new bootloader and firmware 0.02: Use Bangle.setUI for button/launcher handling 0.03: Fix display for Bangle 2 +0.04: Use queueDraw(), update every minute, respect theme, use Lato font +0.05: Decided against custom font as it inceases the code size + minimalism is useful when narrowing down issues diff --git a/apps/simplest/app.js b/apps/simplest/app.js index 68564ff33..582c4c2d5 100644 --- a/apps/simplest/app.js +++ b/apps/simplest/app.js @@ -1,28 +1,55 @@ - const h = g.getHeight(); const w = g.getWidth(); function draw() { - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4].substr(0,5); - + var date = new Date(); + var timeStr = require("locale").time(date,1); + g.reset(); - g.clearRect(0, 30, w, 99); - g.setFontAlign(0, -1); - g.setFont("Vector", w/3); - g.drawString(time, w/2, 40); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + g.setFont('Vector', w/3); + g.setFontAlign(0, 0); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, h/2); + + queueDraw(); } -// handle switch display on by pressing BTN1 -Bangle.on('lcdPower', function(on) { - if (on) draw(); +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } }); g.clear(); + +// Show launcher when middle button pressed +//Bangle.setUI("clock"); +// use clockupdown as it tests for issue #1249 +Bangle.setUI("clockupdown", btn=> { + draw(); +}); + + +// Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); -setInterval(draw, 15000); // refresh every 15s draw(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/simplest/metadata.json b/apps/simplest/metadata.json new file mode 100644 index 000000000..d3118fd22 --- /dev/null +++ b/apps/simplest/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "simplest", + "name": "Simplest Clock", + "version": "0.05", + "description": "The simplest working clock, acts as a tutorial piece", + "icon": "simplest.png", + "screenshots": [{"url":"screenshot_simplest.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"simplest.app.js","url":"app.js"}, + {"name":"simplest.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/simpletimer/metadata.json b/apps/simpletimer/metadata.json new file mode 100644 index 000000000..d319da39e --- /dev/null +++ b/apps/simpletimer/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "simpletimer", + "name": "Timer", + "version": "0.07", + "description": "Simple timer, useful when playing board games or cooking", + "icon": "app.png", + "tags": "timer", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-timer-screenshot.png"}], + "storage": [ + {"name":"simpletimer.app.js","url":"app.js"}, + {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, + {"name":".tfmodel","url":"gesture-tfmodel.js","evaluate":true}, + {"name":"simpletimer.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"simpletimer.json"}] +} diff --git a/apps/slash/ChangeLog b/apps/slash/ChangeLog new file mode 100644 index 000000000..f3fae1785 --- /dev/null +++ b/apps/slash/ChangeLog @@ -0,0 +1 @@ +0.01: First version for upload diff --git a/apps/slash/README.md b/apps/slash/README.md new file mode 100644 index 000000000..9bef104cc --- /dev/null +++ b/apps/slash/README.md @@ -0,0 +1,11 @@ +# Slash Watch + +![](screenshot.png) + +Slash Watch, a recreation of the Slash watch face for Pebble watches by Nikki. +Simple watchface with a slash through the hours and minutes. Date shown at the bottom. Theme will change (light/dark) based on watch theme. + +This watch face was made using Espruino documentation, Espruino forum threads, the 93 Dub watch face, the barclock watch face, and the waveclk app. + +Contributors: +* Ray Holder (93 Dub watchface helped create this one) diff --git a/apps/slash/app-icon.js b/apps/slash/app-icon.js new file mode 100644 index 000000000..a0737974a --- /dev/null +++ b/apps/slash/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgEBgUiACUgC4IWTAAIuVGAQWVAH4A/AH4AmmQWVl//C6s//4wPkfzAof/F4U/DRgpFC4Uj/4hEFw4RBC4QTDBIouJEoYrEBQouLRwXyBZAuKAwQXCGBQiHPgowJEI6mDGBYXXHIy6GPBQhFRwJ9GVBAiFUwjYNEYiOFa5YkFC4guMFYqOEPgwwIBoSmFn4uLJYopMABKOEACUjCyoA/AH4A/AE0CCysggAXVgEAGCguB")) diff --git a/apps/slash/app.js b/apps/slash/app.js new file mode 100644 index 000000000..f548bcaf7 --- /dev/null +++ b/apps/slash/app.js @@ -0,0 +1,109 @@ +// Get 12 hour status, from barclock +const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + +// Used from waveclk to schedule updates 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)); +} + +// From forum conversation 348275 +function fillLine(x1, y1, x2, y2, lineWidth) { + var dx, dy, d; + if (!lineWidth) { + g.drawLine(x1, y1, x2, y2); + } else { + lineWidth = (lineWidth - 1) / 2; + dx = x2 - x1; + dy = y2 - y1; + d = Math.sqrt(dx * dx + dy * dy); + dx = Math.round(dx * lineWidth / d, 0); + dy = Math.round(dy * lineWidth / d, 0); + g.fillPoly([x1 + dx, y1 - dy, x1 - dx, y1 + dy, x2 - dx, y2 + dy, x2 + dx, y2 - dy], true); + } +} + +// Mainly to convert day number to day of the week +function convertDate(date) { + var dayNum = date.getDay(); + var month = date.getMonth(); + var dayOfMonth = date.getDate(); + var dayChar; + + month += 1; + + switch (dayNum) { + case 0 : dayChar = "Sun"; break; + case 1 : dayChar = "Mon"; break; + case 2 : dayChar = "Tue"; break; + case 3 : dayChar = "Wed"; break; + case 4 : dayChar = "Thur"; break; + case 5 : dayChar = "Fri"; break; + case 6 : dayChar = "Sat"; break; + } + + return dayChar + " " + month + "/" + dayOfMonth; +} + +function draw() { + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(); + var minutes = ("0"+m).substr(-2); + g.reset(); + + // Convert to 12hr time mode + if (is12Hour && h > 12) { + h = h - 12; + if (h < 10) { + h = "0" + h; + } + } else if (h < 12) { + h = "0" + h; + } else if (h == 0) { + h = 12; + } + + var hour = (" "+h).substr(-2); + + // Draw the time, vector font + g.setFont("Vector", 50); + g.setFontAlign(1,1); // Align right bottom + g.drawString(hour, 85, 80, true); + g.drawString(minutes, 155, 140, true); + + // Draw slash, width 6 + fillLine(57, 120, 112, 40, 6); + + // Convert date then draw + g.setFont("Vector", 20); + g.setFontAlign(0,1); // Align center bottom + var convertedDate = convertDate(d); + g.drawString(convertedDate, g.getWidth()/2, 170, true); + + Bangle.drawWidgets(); + queueDraw(); +} + +// Clear screen and draw +g.clear(); +draw(); + +// From waveclk +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // Draw immediately, queue redraw + } else { // Stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/slash/metadata.json b/apps/slash/metadata.json new file mode 100644 index 000000000..6bdb4cd53 --- /dev/null +++ b/apps/slash/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "slash", + "name": "Slash Watch", + "shortName":"Slash", + "icon": "slash.png", + "screenshots": [{"url":"screenshot.png"}], + "version":"0.01", + "description": "Slash Watch based on Pebble watch face by Nikki.", + "tags": "clock", + "type": "clock", + "supports":["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"slash.app.js","url":"app.js"}, + {"name":"slash.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/slash/screenshot.png b/apps/slash/screenshot.png new file mode 100644 index 000000000..41008d76c Binary files /dev/null and b/apps/slash/screenshot.png differ diff --git a/apps/slash/slash.png b/apps/slash/slash.png new file mode 100644 index 000000000..cbfc8693d Binary files /dev/null and b/apps/slash/slash.png differ diff --git a/apps/sleepphasealarm/metadata.json b/apps/sleepphasealarm/metadata.json new file mode 100644 index 000000000..f74c97b54 --- /dev/null +++ b/apps/sleepphasealarm/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "sleepphasealarm", + "name": "SleepPhaseAlarm", + "shortName": "SleepPhaseAlarm", + "version": "0.02", + "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", + "icon": "app.png", + "tags": "alarm", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"sleepphasealarm.app.js","url":"app.js"}, + {"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/slevel/ChangeLog b/apps/slevel/ChangeLog index 3a6431e50..e77ca4e8b 100644 --- a/apps/slevel/ChangeLog +++ b/apps/slevel/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Updated to work with both Bangle.js 1 and 2. +0.03: Now also visible on Bangle.js 2 +0.04: Now work with different themes diff --git a/apps/slevel/metadata.json b/apps/slevel/metadata.json new file mode 100644 index 000000000..2a8223498 --- /dev/null +++ b/apps/slevel/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "slevel", + "name": "Spirit Level", + "version": "0.04", + "description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat", + "icon": "spiritlevel.png", + "screenshots": [{"url":"screenshot.png"}], + "tags": "tool", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"slevel.app.js","url":"spiritlevel.js"}, + {"name":"slevel.img","url":"spiritlevel-icon.js","evaluate":true} + ] +} diff --git a/apps/slevel/screenshot.png b/apps/slevel/screenshot.png new file mode 100644 index 000000000..e945414ac Binary files /dev/null and b/apps/slevel/screenshot.png differ diff --git a/apps/slevel/spiritlevel.js b/apps/slevel/spiritlevel.js index 9db54b825..dd6d56bdc 100644 --- a/apps/slevel/spiritlevel.js +++ b/apps/slevel/spiritlevel.js @@ -2,6 +2,7 @@ g.clear(); var old = {x:0,y:0}; var W = g.getWidth(); var H = g.getHeight(); + Bangle.on('accel',function(v) { var max = Math.max(Math.abs(v.x),Math.abs(v.y),Math.abs(v.z)); if (Math.abs(v.y)==max) { @@ -12,21 +13,26 @@ Bangle.on('accel',function(v) { var d = Math.sqrt(v.x*v.x+v.y*v.y); var ang = Math.atan2(d,Math.abs(v.z))*180/Math.PI; - - g.setColor(1,1,1); - g.setFont("6x8",2); - g.setFontAlign(0,-1); - g.clearRect(W*(1/4),0,W*(3/4),H*(1/16)); - g.drawString(ang.toFixed(1),W/2,0); + + g.reset(); + g.clearRect(W*(1/4),0,W*(3/4),16);// clear behind text + g.setFont("6x8",2).setFontAlign(0,-1).drawString(ang.toFixed(1),W/2,0); var n = { x:E.clip(W/2+v.x*256,4,W-4), y:E.clip(H/2+v.y*256,4,H-4)}; - g.clearRect(old.x-3,old.y-3,old.x+6,old.y+6); - g.setColor(1,1,1); - g.fillRect(n.x-3,n.y-3,n.x+6,n.y+6); - g.setColor(1,0,0); + g.clearRect(old.x-3,old.y-3,old.x+6,old.y+6); // clear old marker + g.setColor("#0f0"); + g.fillRect(n.x-3,n.y-3,n.x+6,n.y+6); // draw new marker + // draw rings + g.setColor("#f00"); g.drawCircle(W/2,H/2,W*(1/12)); g.drawCircle(W/2,H/2,W*(1/4)); g.drawCircle(W/2,H/2,W*(5/12)); old = n; }); + +setWatch(_=>load(), BTN1); +if (global.BTN2) { + setWatch(_=>load(), BTN2); + setWatch(_=>load(), BTN3); +} diff --git a/apps/slidingtext/metadata.json b/apps/slidingtext/metadata.json new file mode 100644 index 000000000..0f380ec8f --- /dev/null +++ b/apps/slidingtext/metadata.json @@ -0,0 +1,25 @@ +{ + "id": "slidingtext", + "name": "Sliding Clock", + "version": "0.07", + "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported", + "icon": "slidingtext.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "allow_emulator": false, + "storage": [ + {"name":"slidingtext.app.js","url":"slidingtext.js"}, + {"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true}, + {"name":"slidingtext.locale.en.js","url":"slidingtext.locale.en.js"}, + {"name":"slidingtext.locale.en2.js","url":"slidingtext.locale.en2.js"}, + {"name":"slidingtext.utils.en.js","url":"slidingtext.utils.en.js"}, + {"name":"slidingtext.locale.es.js","url":"slidingtext.locale.es.js"}, + {"name":"slidingtext.locale.fr.js","url":"slidingtext.locale.fr.js"}, + {"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"}, + {"name":"slidingtext.locale.de.js","url":"slidingtext.locale.de.js"}, + {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} + ] +} diff --git a/apps/slimehunt/ChangeLog b/apps/slimehunt/ChangeLog new file mode 100644 index 000000000..eb0cc5918 --- /dev/null +++ b/apps/slimehunt/ChangeLog @@ -0,0 +1,2 @@ +0.01: Public version is a go! +0.02: Fixed bug where Critial Up wasn't letting player attack. diff --git a/apps/slimehunt/README.md b/apps/slimehunt/README.md new file mode 100644 index 000000000..84d8f2b16 --- /dev/null +++ b/apps/slimehunt/README.md @@ -0,0 +1,77 @@ +<><><><>-SLIME HUNT-<><><><> + +Slime Hunt is a RPG turn-based style combat game +where you fight slimes until your HP runs out. + +The main goal is to beat your personal highscore! + +============================ + +During each fight the player has 3 options, + +BTN1) FIGHT +- Attacks the slime, dealing 1 hp worth of damage. + +BTN2) DEFEND +- Defends against the slime, blocking 3 damage from the next slime attack. + +BTN3) RUN +- Find a new slime to fight against. (This could change in the future!) + +============================ + +There are currently 5 types of slime each with unique behavior. + +<><>-BEHAVIORS-<><> + +1. NEUTRAL +- Slime deals 0-1 damage on it's next attack. + +2. ANGRY +- Slime deals 3-5 damage on it's next attack. + +3. ERACTIC +- Slime deals 0-5 damage on it's next attack. + +<><>-ITEMS-<><> + +1. Attack Up +- +1 damage next battle. + +2. Defence Up +- +1 defence next battle, stacks with block. +Setting defence to 4 when using DEFEND, and 1 otherwise. + +3. HP Up +- +3 HP. + +4. Block Up +- +2 block on DEFEND next battle, +setting Defence to 5 when using DEFEND command. + +5. Critical Up +- 20% chance to crit next battle on each attack, +instantly defeating the Slime. + +*****Using the RUN command causes you to lose your item!***** + +<><>-SLIMES-<><> + +1. GREEN SLIME +- Is always neutral. | 0% chance of item. + +2. RED SLIME +- Can be either neutral or angry. | 10% chance of item. + +3. GRAY SLIME +- Can be neutral, angry or eratic. | 20% chance of item. + +4. YELLOW SLIME +- Is always eratic. | 50% chance of item. + +5. PURPLE SLIME +- Is always angry. | 100% chance of item. + +============================ + +Created by Colton LaChance! diff --git a/apps/slimehunt/app-icon.js b/apps/slimehunt/app-icon.js new file mode 100644 index 000000000..36486cb8a --- /dev/null +++ b/apps/slimehunt/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH9bxgAM1gtsGTwtTGDVhFSPX64wZLqgwFF6iMVGAhgUF6owBMCzsWAAthL1AAGF/4vxrdhADVbeCQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AFA")) diff --git a/apps/slimehunt/app.js b/apps/slimehunt/app.js new file mode 100644 index 000000000..547ea7356 --- /dev/null +++ b/apps/slimehunt/app.js @@ -0,0 +1,477 @@ +//Create constants------------------------------------------------------------------ + +//Slimes +const GREEN_SLIME = 1; //Normal slime, is always neutral. | 0% Item chance +const PINK_SLIME = 2; //Can get angry. | 10% Item chance +const GRAY_SLIME = 3; //Can be neutral, angry or erratic. | 20% Item chance +const YELLOW_SLIME = 4; //Is always erratic. | 50% Item chance +const PURPLE_SLIME = 5; //Is always angry. | 100% Item chance + +//Items +const ITEM_ATK_UP = 1; //Raises damage dealt by +1 for next battle +const ITEM_DEF_UP = 2; //Reduces all damage by +1 for next battle +const ITEM_HP_UP = 3; //Increases HP by 3 +const ITEM_BLOCK_UP = 4; //Raises defence when defending by from 3 to 5 for next battle +const ITEM_CRIT_UP = 5; //Gives attack a 20% chance to instantly KO slime for next battle + +//Base stats +const BASE_ATK = 1; +const BASE_DEF = 0; +const BASE_BLOCK = 3; +const BASE_CRIT = 0; + +//Initialize variables------------------------------------------------------------------ +var playerHP = 20; +var slimeHP = 3; +var slimeType = GREEN_SLIME; +var turn = 0; +var screenWidth = g.getWidth(); +var screenHeight = g.getHeight(); +var slimeState = 0; +var showBattleResult = false; +var dmgDealt = 0; +var playerDefence = 0; +var playerItem = 0; +var critChance = 0; + +//Stats (Modifiers) +var statAtk = 1; +var statDef = 0; +var statBlock = 3; +var statCrit = 0; + +//Item vars +var itemName = ""; +var itemDesc = ""; +var itemChance = 0; + +var refreshInterval; +var waitTime = 0; + +var highscore = 0; +var score = 0; + +var themeNote = 0; + +//Load files------------------------------------------------------------------ +var file = require("Storage").open("highscore.txt", "r"); +highscore = file.readLine(); +if (highscore == undefined) highscore = 0; + +var greenSlime = require("Storage").read("slime.img"); +var pinkSlime = require("Storage").read("slimered.img"); +var graySlime = require("Storage").read("slimegray.img"); +var yellowSlime = require("Storage").read("slimeyellow.img"); +var purpleSlime = require("Storage").read("slimepurple.img"); + +//UI Stuff------------------------------------------------------------------ +function drawOpeningUI() { + g.clear(); + g.setFont("Vector", screenWidth / 15); + g.setFontAlign(0, 0); // center font + g.drawString("SLIME HUNT", screenWidth / 2, screenHeight * 0.1); + g.drawString("-SCORE TO BEAT-", screenWidth / 2, screenHeight * 0.3); + g.drawString("<><><> " + highscore + " <><><>", screenWidth / 2, screenHeight * 0.45); + g.setFont("Vector", screenWidth / 20); + g.drawString("A Slime approches...", screenWidth / 2, screenHeight * 0.6); + wait(8, waitForBattle); +} + +function drawSlime() { + switch (slimeType) { + case GREEN_SLIME: + g.drawImage(greenSlime, screenWidth / 2, screenHeight / 2, { + scale: 4, + rotate: 0 + }); + break; + case PINK_SLIME: + g.drawImage(pinkSlime, screenWidth / 2, screenHeight / 2, { + scale: 4, + rotate: 0 + }); + break; + case GRAY_SLIME: + g.drawImage(graySlime, screenWidth / 2, screenHeight / 2, { + scale: 4, + rotate: 0 + }); + break; + case YELLOW_SLIME: + g.drawImage(yellowSlime, screenWidth / 2, screenHeight / 2, { + scale: 4, + rotate: 0 + }); + break; + case PURPLE_SLIME: + g.drawImage(purpleSlime, screenWidth / 2, screenHeight / 2, { + scale: 4, + rotate: 0 + }); + break; + } +} + +function drawBattleUI() { + g.clear(); + g.setFont("Vector", screenWidth / 8); + g.setFontAlign(0, 0); // center font + g.drawString("SLIME HP: " + slimeHP, screenWidth / 2, screenHeight * 0.1); + g.setFont("Vector", screenWidth / 20); + if (!showBattleResult) { + switch (slimeState) { + case 0: + g.drawString("The slime seems neutral...", screenWidth / 2, screenHeight * 0.25); + break; + case 1: + g.drawString("The slime seems angry...", screenWidth / 2, screenHeight * 0.25); + break; + case 2: + g.drawString("The slime seems eratic...", screenWidth / 2, screenHeight * 0.25); + break; + } + } else { + var brString = (turn == 0 ? "The Slime loses " : "You lose "); + g.drawString(brString + dmgDealt + "HP!", screenWidth / 2, screenHeight * 0.25); + } + drawSlime(); + g.drawLine(0, screenHeight * 0.72, screenWidth, screenHeight * 0.72); + if (turn == 0) { + g.setFont("Vector", screenWidth / 15); + g.drawString("Your HP is " + playerHP + ".", screenWidth / 2, screenHeight * 0.8); + g.setFont("Vector", screenWidth / 20); + g.drawString("(B1) FIGHT\t|\t(B2) DEFEND\t|\t(B3) RUN", screenWidth / 2, screenHeight * 0.9); + } +} + +//Win / lose functions------------------------------------------------------------------ +function win() { + wait(5, winTheme); + calcScore(slimeType); + showBattleResult = false; + g.clear(); + g.setFont("Vector", screenWidth / 8); + g.setFontAlign(0, 0); // center font + g.drawString("YOU WON!", screenWidth / 2, screenHeight * 0.1); + g.drawLine(0, screenHeight * 0.2, screenWidth, screenHeight * 0.2); + g.setFont("Vector", screenWidth / 12); + g.drawString((playerItem == 0 ? "No Item." : "GOT ITEM!"), screenWidth / 2, screenHeight * 0.27); + g.setFont("Vector", screenWidth / 15); + g.drawString((playerItem == 0 ? "" : "<><> " + itemName + " <><>"), screenWidth / 2, screenHeight * 0.40); + g.setFont("Vector", screenWidth / 20); + g.drawString((playerItem == 0 ? "" : itemDesc), screenWidth / 2, screenHeight * 0.52); + g.drawLine(0, screenHeight * 0.6, screenWidth, screenHeight * 0.6); + g.drawString("Your score is << " + score + " >>", screenWidth / 2, screenHeight * 0.75); + g.drawString("Press (B3) to find another slime!", screenWidth / 2, screenHeight * 0.9); + turn = 0; + setWatch(run, BTN3); +} + +function lose() { + wait(5, loseTheme); + playerHP = 20; + showBattleResult = false; + g.clear(); + g.setFont("Vector", screenWidth / 8); + g.setFontAlign(0, 0); // center font + g.drawString("You lose...", screenWidth / 2, screenHeight * 0.1); + g.drawLine(0, screenHeight * 0.2, screenWidth, screenHeight * 0.2); + g.setFont("Vector", screenWidth / 12); + g.drawString((score > highscore ? "-NEW HIGHSCORE-" : "-SCORE TO BEAT-"), screenWidth / 2, screenHeight * 0.27); + g.setFont("Vector", screenWidth / 15); + g.drawString((score > highscore ? "<><> " + score + " <><>" : "<><> " + highscore + " <><>"), screenWidth / 2, screenHeight * 0.43); + g.drawLine(0, screenHeight * 0.6, screenWidth, screenHeight * 0.6); + g.setFont("Vector", screenWidth / 20); + g.drawString("Your score is << " + score + " >>", screenWidth / 2, screenHeight * 0.75); + g.drawString("Press (B3) to try again...", screenWidth / 2, screenHeight * 0.9); + score = 0; + turn = 0; + setWatch(run, BTN3); +} + +//Battle Stuff------------------------------------------------------------------ +function nextTurn() { + turn = (turn == 0 ? 1 : 0); +} + +function slimeFight() { + Bangle.beep(100, 500); + switch (slimeState) { + case 0: + dmgDealt = Math.floor(Math.random() * 2); + break; + case 1: + dmgDealt = Math.floor(Math.random() * 3) + 3; + break; + case 2: + dmgDealt = Math.floor(Math.random() * 6); + break; + } + dmgDealt = Math.max(0, dmgDealt - playerDefence); + playerHP -= dmgDealt; + slimeAI(); +} + +function fight() { + if (turn == 0 && waitTime <= 0) { + Bangle.beep(100, 1000); + dmgDealt = statAtk; + playerDefence = statDef; + if (statCrit == 0) { + slimeHP -= dmgDealt; + }else{ + critChance = Math.floor(Math.random() * 100); + if (critChance >= 100-statCrit) { + slimeHP = 0; + dmgDealt = 99; + }else{ + slimeHP -= dmgDealt; + } + critChance = 0; + } + showBattleResult = true; + drawBattleUI(); + wait(5, waitForTurn); + } +} + +function defend() { + if (turn == 0 && waitTime <= 0) { + dmgDealt = 0; + playerDefence = statBlock + statDef; + showBattleResult = true; + drawBattleUI(); + wait(5, waitForTurn); + } +} + +function run() { + if (turn == 0 && waitTime <= 0) { + showBattleResult = false; + Bangle.beep(200, 4000); + wait(3, waitForBattle); + } +} + + +function newBattle() { + showBattleResult = false; + slimeType = Math.floor(Math.random() * 5) + 1; + useItem(); //Use item at start of new battle + switch (slimeType) { + case GREEN_SLIME: + slimeHP = 3; + break; + case PINK_SLIME: + slimeHP = 3; + break; + case GRAY_SLIME: + slimeHP = 5; + break; + case YELLOW_SLIME: + slimeHP = 5; + break; + case PURPLE_SLIME: + slimeHP = 5; + break; + } + turn = 0; + battle(); + slimeAI(); + drawBattleUI(); +} + +function battle() { + setWatch(fight, BTN1); + setWatch(defend, BTN2); + setWatch(run, BTN3); +} + +function slimeAI() { + switch (slimeType) { + case GREEN_SLIME: + slimeState = 0; + break; + case PINK_SLIME: + slimeState = Math.floor(Math.random() * 2); + break; + case GRAY_SLIME: + slimeState = Math.floor(Math.random() * 3); + break; + case YELLOW_SLIME: + slimeState = 2; + break; + case PURPLE_SLIME: + slimeState = 1; + break; + } +} +//Items------------------------------------------------------------------ +function getItem() { + playerItem = Math.floor(Math.random() * 5) + 1; + switch (playerItem) { + case ITEM_ATK_UP: + itemName = "Attack Up"; + itemDesc = "+1 damage next battle."; + break; + case ITEM_DEF_UP: + itemName = "Defence Up"; + itemDesc = "+1 defence next battle."; + break; + case ITEM_HP_UP: + itemName = "HP Up"; + itemDesc = "+3 HP."; + break; + case ITEM_BLOCK_UP: + itemName = "Block Up"; + itemDesc = "+2 block on DEFEND next battle."; + break; + case ITEM_CRIT_UP: + itemName = "Critical Up"; + itemDesc = "20% chance to crit next battle."; + break; + } +} + +function useItem() { + statAtk = BASE_ATK; + statDef = BASE_DEF; + statBlock = BASE_BLOCK; + statCrit = BASE_CRIT; + switch (playerItem) { + case ITEM_ATK_UP: + statAtk = 2; + break; + case ITEM_DEF_UP: + statDef = 1; + break; + case ITEM_HP_UP: + playerHP += 3; + break; + case ITEM_BLOCK_UP: + statBlock = 5; + break; + case ITEM_CRIT_UP: + statCrit = 20; + break; + } + playerItem = 0; +} + +//Timed transitions------------------------------------------------------------------ +function wait(duration, waitFunc) { + waitTime = duration; + if (!refreshInterval) + refreshInterval = setInterval(waitFunc, 500); +} + +function waitForTurn() { + waitTime--; + if (waitTime <= 0) { + clearInterval(refreshInterval); + refreshInterval = undefined; + nextTurn(); + if (playerHP > 0 && slimeHP > 0) { + if (turn == 1) { + slimeFight(); + wait(5, waitForTurn); + } else { + showBattleResult = false; + battle(); + } + drawBattleUI(); + } else { + if (playerHP <= 0) { + lose(); + } + if (slimeHP <= 0) { + win(); + } + } + } + Bangle.setLCDPower(1); +} + +function waitForBattle() { + waitTime--; + Bangle.beep(100, 1000); + if (waitTime <= 0) { + clearInterval(refreshInterval); + refreshInterval = undefined; + showBattleResult = false; + newBattle(); + } + Bangle.setLCDPower(1); +} + +function winTheme() { + waitTime--; + Bangle.beep(200, 100 * themeNote); + themeNote++; + if (waitTime <= 0) { + themeNote = 0; + clearInterval(refreshInterval); + refreshInterval = undefined; + setWatch(run, BTN3); + } + Bangle.setLCDPower(1); +} + +function loseTheme() { + waitTime--; + Bangle.beep(200, 600 - (100 * themeNote)); + themeNote++; + if (waitTime <= 0) { + themeNote = 0; + clearInterval(refreshInterval); + refreshInterval = undefined; + setWatch(run, BTN3); + } + Bangle.setLCDPower(1); +} + +//Calculations------------------------------------------------------------------ +function calcScore(slimeType) { + switch (slimeType) { + case GREEN_SLIME: + score += 1; + //No items + break; + case PINK_SLIME: + score += 2; + itemChance = Math.floor(Math.random() * 100); + if (itemChance >= 100 - 10) { //100 - ITEM CHANCE % + getItem(); + } + break; + case GRAY_SLIME: + score += 3; + itemChance = Math.floor(Math.random() * 100); + if (itemChance >= 100 - 25) { //100 - ITEM CHANCE % + getItem(); + } + break; + case YELLOW_SLIME: + score += 5; + itemChance = Math.floor(Math.random() * 100); + if (itemChance >= 100 - 50) { //100 - ITEM CHANCE % + getItem(); + } + break; + case PURPLE_SLIME: + score += 10; + getItem(); + break; + } + if (score > highscore) { + file.erase(); + file = require("Storage").open("highscore.txt", "w"); + file.write(score); + } +} + + +//------------------------------------GAME STARTS HERE ----------------------------------------------- + +//Load opening UI +drawOpeningUI(); diff --git a/apps/slimehunt/app.png b/apps/slimehunt/app.png new file mode 100644 index 000000000..208e68f46 Binary files /dev/null and b/apps/slimehunt/app.png differ diff --git a/apps/slimehunt/metadata.json b/apps/slimehunt/metadata.json new file mode 100644 index 000000000..2a3509a01 --- /dev/null +++ b/apps/slimehunt/metadata.json @@ -0,0 +1,14 @@ +{ "id": "slimehunt", + "name": "Slime Hunt", + "shortName":"SlimeHunt", + "icon": "app.png", + "version":"0.02", + "description": "Fight against slimes in turn based combat, try to get the highscore!", + "tags": "rpg,slime", + "supports" : ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"slimehunt.app.js","url":"app.js"}, + {"name":"slimehunt.img","url":"app-icon.js","evaluate":true} +] +} diff --git a/apps/slomoclock/metadata.json b/apps/slomoclock/metadata.json new file mode 100644 index 000000000..5afcbf87c --- /dev/null +++ b/apps/slomoclock/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "slomoclock", + "name": "SloMo Clock", + "shortName": "SloMo Clock", + "version": "0.10", + "description": "Simple 24h clock face with large digits, hour above minute. Uses Layout library.", + "icon": "watch.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-slow-mo-clock-screenshot.png"}], + "storage": [ + {"name":"slomoclock.app.js","url":"app.js"}, + {"name":"slomoclock.img","url":"app-icon.js","evaluate":true}, + {"name":"slomoclock.settings.js","url":"settings.js"} + ], + "data": [{"name":"slomoclock.json"}] +} diff --git a/apps/smartibot/metadata.json b/apps/smartibot/metadata.json new file mode 100644 index 000000000..612a6f904 --- /dev/null +++ b/apps/smartibot/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "smartibot", + "name": "Smartibot controller", + "shortName": "Smartibot", + "version": "0.01", + "description": "Control a [Smartibot Robot](https://thecraftyrobot.net/) straight from your Bangle.js", + "icon": "app.png", + "tags": "", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"smartibot.app.js","url":"app.js"}, + {"name":"smartibot.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/smtswch/metadata.json b/apps/smtswch/metadata.json new file mode 100644 index 000000000..14c768a5d --- /dev/null +++ b/apps/smtswch/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "smtswch", + "name": "Smart Switch", + "shortName": "Smart Switch", + "version": "0.01", + "description": "Using EspruinoHub, control your smart devices on and off via Bluetooth Low Energy!", + "icon": "app.png", + "type": "app", + "tags": "bluetooth,btle,smart,switch", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"smtswch.app.js","url":"app.js"}, + {"name":"smtswch.img","url":"app-icon.js","evaluate":true}, + {"name":"light-on.img","url":"light-on.js","evaluate":true}, + {"name":"light-off.img","url":"light-off.js","evaluate":true}, + {"name":"switch-on.img","url":"switch-on.js","evaluate":true}, + {"name":"switch-off.img","url":"switch-off.js","evaluate":true} + ] +} diff --git a/apps/snake/metadata.json b/apps/snake/metadata.json new file mode 100644 index 000000000..c8a92c39b --- /dev/null +++ b/apps/snake/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "snake", + "name": "Snake", + "shortName": "Snake", + "version": "0.02", + "description": "The classic snake game. Eat apples and don't bite your tail.", + "icon": "snake.png", + "tags": "game,fun", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"snake.app.js","url":"snake.js"}, + {"name":"snake.img","url":"snake-icon.js","evaluate":true} + ] +} 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/metadata.json b/apps/snaky/metadata.json new file mode 100644 index 000000000..62baa8608 --- /dev/null +++ b/apps/snaky/metadata.json @@ -0,0 +1,14 @@ +{ "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} + ] +} 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/metadata.json b/apps/snek/metadata.json new file mode 100644 index 000000000..d8d3568f2 --- /dev/null +++ b/apps/snek/metadata.json @@ -0,0 +1,14 @@ +{ "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} + ] +} 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/solarclock/metadata.json b/apps/solarclock/metadata.json new file mode 100644 index 000000000..428b4c486 --- /dev/null +++ b/apps/solarclock/metadata.json @@ -0,0 +1,29 @@ +{ + "id": "solarclock", + "name": "Solar Clock", + "version": "0.02", + "description": "Using your current or chosen location the solar watch face shows the Sun's sky position, time and date. Also allows you to wind backwards and forwards in time to see the sun's position", + "icon": "solar_clock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "custom": "custom.html", + "allow_emulator": false, + "storage": [ + {"name":"solarclock.app.js","url":"solar_clock.js"}, + {"name":"solarclock.img","url":"solar_clock-icon.js","evaluate":true}, + {"name":"solar_colors.js","url":"solar_colors.js"}, + {"name":"solar_controller.js","url":"solar_controller.js"}, + {"name":"solar_date_utils.js","url":"solar_date_utils.js"}, + {"name":"solar_graphic_utils.js","url":"solar_graphic_utils.js"}, + {"name":"solar_location.js","url":"solar_location.js"}, + {"name":"solar_math_utils.js","url":"solar_math_utils.js"}, + {"name":"solar_loc.Reykjavik.json","url":"solar_loc.Reykjavik.json"}, + {"name":"solar_loc.Hong_Kong.json","url":"solar_loc.Hong_Kong.json"}, + {"name":"solar_loc.Honolulu.json","url":"solar_loc.Honolulu.json"}, + {"name":"solar_loc.Rio.json","url":"solar_loc.Rio.json"}, + {"name":"solar_loc.Tokyo.json","url":"solar_loc.Tokyo.json"}, + {"name":"solar_loc.Seoul.json","url":"solar_loc.Seoul.json"} + ] +} diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog new file mode 100644 index 000000000..1f1fc7386 --- /dev/null +++ b/apps/sonicclk/Changelog @@ -0,0 +1,2 @@ +0.01 Added sonic clock app +0.02 Fixed text alignment issue; Increased acceleration required to activate twist; diff --git a/apps/sonicclk/README.md b/apps/sonicclk/README.md new file mode 100644 index 000000000..a381e0a07 --- /dev/null +++ b/apps/sonicclk/README.md @@ -0,0 +1,13 @@ +# Sonic Clock + +A classic sonic clock featuring run, stop and wait animations. + +![Sonic Clock screenshot](screenshot.png) + +## Usage + +- Sonic will run when the screen is unlocked +- Sonic will stop when the screen is locked +- Sonic will wait when looking at your watch face (when `Bangle.on("twist", fn)` is fired). + +### Made with love by [Joseph](https://github.com/Johoseph) 🤗 diff --git a/apps/sonicclk/app-icon.js b/apps/sonicclk/app-icon.js new file mode 100644 index 000000000..33e22971b --- /dev/null +++ b/apps/sonicclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AE0ZzIACBIgFFC7oTCylEzOIDYeZogX6LwWd7oYCAAOJC82UpoXFAAKnMC6x2CpIVFC/gSCxOYAYP///4C4X/DBIXWIIwWBDAWPAYIXedQJwG/8AgEP//wgAX2CwIADRoQXmSIoXtJAeEoi+C+lEoAcBogX7zOUondolEpvdAAQXgYIgXCAAwXlAAIYC6ENLx4XtAYMZDAvd6gWJC7IKJABgX/C74A/ADY")) diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js new file mode 100644 index 000000000..296677281 --- /dev/null +++ b/apps/sonicclk/app.js @@ -0,0 +1,284 @@ +const vw = g.getWidth(); +const bgWidth = 384; + +const sonic = { + bpp: 4, + transparent: 1, +}; + +const defaultSonic = { + width: 36, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECKYOComZxAVDogRXBQfdCQQABCLITCogRFCQIRYLAgAFCQwRQBoQCC///AYYRWGAoMBAAgRYCYf/gEAB4ICBCKrDDCL4UECMA3DolEB4IDBAAQRb7oMB7vdHIIRWCAYRCAAoRWCoozDCBQRRBQI3FERIRPPIbGBPIQiICKAQCdQRZDAYIRXCQIJB/4RDEhQRQGYP//7FEGpARQCAYREgEzERQRNAAIRCLIKkBCLg4CYoIABB5IRWAQQRcBoIODCRYRQB4INECLkRgH/ZAMACRgRSmbGBCIIFBmYWCCLAAC7oCBdgNEAgIRWBQY2B7oTBAwMzCYoRUBIIQBCQQGBHIYRUABwRRA=")), +}; + +const startingBuffers = [ + { + width: 49, + height: 63, + buffer: require("heatshrink").decompress(atob("iIAFzIAEyMRjICBABkZDAogCCxgRBxAACzOUpIzDFxmYC4WJzNEGYgyQzOd6lIDoJiNGQYwB7tEDoQxQI4NN7BPDGJ54BondQAgvPGCjDDGCQVCCAVESAOIx/4GJYuCSIQwBFwX/GJbyEAAmDn///AwJjLyDAAsz/4xBGBAvJGAfwhIwHF5QwDgAwHXwYADGB6+DAAY2EGBYzHGA8P+EAgLyKGBIBBgD3ObwVPGAMEoAwIe4+ZolEGAVEGBRfDAYOUpvd6gaBpvQF5IuBFoOJDANE7vdC4IEBF5h8DGAQABGQNBF5YuCGAgyDYZwxE6lDmcxR5gAFGINEmczF6IxCob1B+YxIF5AxCeoQxJzLNBAAQFBhOUoEDGAIxJjOQC4cJAoIvBgAwCGJIwWiEAgcwglEpOZGwgvCGBANBmcAaIJ2BC4cPC4QwIga3BmEIeoIwBDoJhDF4j5DBgIABgGIGAROBMIYvEilBGAgvDAARIBGQL0FA4IwFF4YABJQIyBoczF4lEGA0JCwWUEoPd7qBCzIwDgAwGF4YXBpvd6iyBzORGAVAgLEEF4YwCF4IwEMYMzGAjECC4YwBF4IwEycxic0GAsN7vRiMZDIQvDBwItBS4IqBGAgQBAgIXCzovCLwItBS4MwgAwCF4XTn/xGIiODF4QwCF40z/4xERwYvCGAQvF7vf+CUEEobvCAAJgEF4Xd/6UEXgTvDAANAMAwwFCwgwESBAwFC4owCF43f/vUoaSCABAvCMAv/aQKSDAA8UF4JgG7/0oIWJGAhgG/9EC5YwCSA5eKFQiQIC5sQF4QwD6hfMGpAvBL5g1IhovVGAQvUGAMEF6owBF6owB")), + }, + { + width: 42, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIAEA4QNEAAwUSBwOIAAWZogXECjgWFzPdogECCj5pDondIggUeSAYUjCISRDCQX/CpIUTXQgAEmf/CoIUaCZIUDgAUZRoYADCkIXHCg4BBgAUZX4y+BogUCogUccwgTB7vdCwIEBFA4UTHQYACCgQACogoJCiYAFCoYTHCigTHCoQnICioaJCRwUUHYbhBHpwUTXoQTCmczCpgUTcwYUB/8zAIIUfgDeBCoQUC/4UeRYQPBCoITCHxQUXCYMAgAVBmboKCjCUCAAQPFToIUaMgOIAAIUGBoIUYCoS9DNYIVECYwUWmYVBcIYVBokRBQIUbBoLhB7pjFOYQTGCixpCAwidCCY4UVCoYGFCYIUeBwLaGCZYUVDQ4TRChA")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5CbobeBAAPdCCgPECAYADoggHCCIRFB4YQQXYwSDOgwQPAAoQDBwMzmYRICB5SCGIYQZB4h0CEBoQKmf/oj/CCQQgHCB4PBAAJ1CxAWBEBAQRmYPBAgJCKCCIyCfoYQXCIKQBB4ndGoIQWiL4BgDaBS4IPBGw4QPBwQSCAgMzCoScECCg4FAAQQVB4IREABAQSAAQtGCCoRDOQYiMCBpxGCCoA==")), + }, + { + width: 44, + height: 62, + buffer: require("heatshrink").decompress(atob("iIAEzIADBAgPFCrIQBxAABAYIZDxAVfC4QSBogaFCsQAB7oWFCsAUCogVmSAQVCCQP/CxgVUYggACmf/CwQVdCYwVEgGIgAVbSoQADCsgYGCowACCroWFolECoVECr7wCAALyB7oXBAYJCJCqgUECoYADokzmYWFCrYWFNoIVceQwXDS4QUBCrYACRwQVDAoIXBCrznCMwRAFAAYpFCqf/mf/CgKZFohQGCrIACS4hFCCQ3dIgIVXIQIUDxAVIBAIVXCYJAFxGITIIVfCwMziOZAIIXEAAIVeAAQFBgBFEDITADCrUAAAoeDCwP/IAX/CrAPBog/CJQoWBHwJsFCrAWBCYpqCCgQNECqwUBCpJrCBooVUIBgABEQwVWokzChRPDCrA/NKIgxJCp4FEAB4VVA")), + }, + { + width: 38, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECSgNBxAABAYIWDxASZCgQPBogXFCTgAB7oTFCTQRCogSgPYQSCB4P/CZISRVIgACmf/CYQSXCAwSEgGIgASVdgbvDCToVGCQwACCS4TFolECQVECTLPCe4b4BAAPdCTARECQYADogkJCSYTFCIoSSaIwUDEYwSTAATKCCQa4GCSwRC/8zog3CehASYaIkAmYTGCSYTBCIISEBIITGCSStEZ4YSceo7PBQpASSBAQVFaBISRgChBAIIRCxAkKCSAPBeQI5CK4ITG7oJBCSgRCPoYSFCIISVTwaiEzIREEAISTABH/AAIDBG4YSce4IAB/43MCSQRD7q1CEhwSQRwS1CCTqzDWwgSTA==")), + }, + { + width: 40, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIAEA4QNEAAoTRBoOIAAWZogWECbQVFzPdogECCbpkDondIIgTcRIYThCASKDCIX/ChATSWIgAEmf/CgITYCRITDgATXQwYADCb4WHCY4BBgATXWwy1BogTCogTabYgSB7vdCoIDBEwwTSG4YACB4QACogmICaQAFCgYSGCaYSHCgQlHCagZIWIISNCaQ5DmYTB/8zCbq0DogThbYgTDExwTQWIszmazKCagWFbQNEgATcHwpoBomZgAULCaYUFgA8SCZlE7q5DHIITdBQPda4ITDRpQTXMQoUJCbLhCCgUzCgwTWCgIJFMxATVCgQABBYoACCa4IEBQwAICZwA=")), + }, + { + width: 36, + height: 60, + buffer: require("heatshrink").decompress(atob("iIANzIPOCI+ZABWICKwTDxGZogDBAAIRaHAfdCQoRXKQdECLhWCBIQRBAgP/CQwRQB4YAFmf/CQQRUK4YRIgGIgARUdIoReCQgRGAAQRVCQtEogRCogRXBoLqEdYIAB7oRWCAczmYRCAAdEEQ4RNOIR4CCQoQECKJxBogQCCIJcCPYPdCKo1CCIwIBG4QRUSYZ8BGgszUoQRSFIQSBdQbGDQQIVBCKIQCGILJCCAYRBRIKuCCKf/xARIiIRYK4ZYExBYDCKjDBxGICQdEAYQQCCKjcBBQKjFGgayDCKYAGT4gRSCQYRICAgRVCRYRYcIIRdCQSZCG44YFCKArCEiARQAAYTCChwRKA==")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5C7oADCQLABCCgPBBwkACgogECBwJFCAoPDCCAPFJgJ5BKYJjECCBqBBwf/mYQBAoMzmYRCCCIpCBAIPCA4gQSB4RrBboY6CoggGCCC+DB4YQBOogQRXwYQGU4YQUSYRTEdQgQTWYQzDB4gQVKooABBwYQSRAIQECYgQCHoQQWHQIQDXYLdDCB4RELYQPDCCyOGXYIPKCCAPGCAIgOCBpBLCCKyDBxYQMA==")), + }, + { + width: 36, + height: 60, + buffer: require("heatshrink").decompress(atob("iIANzIPOCI+ZABWICKwTDxGZogDBAAIRaHAfdCQoRXKQdECLhWCBIQRBAgP/CQwRQB4YAFmf/CQQRUK4YRIgGIgARUdIoReCQgRGAAQRVCQtEogRCogRXBoLqEdYIAB7oRWCAczmYRCAAdEEQ4RNOIR4CCQoQECKJxBogQCCIJcCPYwRRGoQRGBAITCCKoACPgI0FUYQSGCJpNDdQbGDQQQRTJooQECIIMBCMMRCKhwDPIMALAoZBCLQADxAABCAQRXogOBUYgUBCLTIDAAQSB/4RYB4oAB/4QDWQYROYowADgDHCCLASGCIYJDCKI2MDQwRQCQgcGQwwRRBAIAGB44RPA==")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5CbobeBAAPdCCgPDmczCAQADoggGCBkAA4RPBCIoPDCCAPDCAZXCOgoQQJYgQEBwJbBCIQQUBAIxFCCgPDboi7DEA4QPXwYPDCAJ1GCDQJCA4YQPB4hTEdQYQXgCXDB4YQVawQDBxAFBBwYQVAAYQBGAoABCCCqBBAcAAAIQGXYQQPCIcAYApACCCgPFAAX/XoQ5FCCwPBGQIPFCCIREmYOCB44QRTAabHCCAA==")), + }, +]; + +const stoppingBuffers = [ + { + width: 44, + height: 60, + buffer: require("heatshrink").decompress(atob("iIAEzIACA4gOFAAwVTCQVEzOIDIdECsAWC7oWCAAIVkogVFCwIVgNggAECsARCAQX//4VC/4WJCqg8FCgIWCAQIVdM43/gEACYICBCriVBAAQVmIg4VmIQdETAVEogZBogVhzNE7oTB7oACCoYbBCrCbECQgVFIoIVeCwcACg4VbN4xrGCrgCBBwMAC4QNBBwYVeM4IABIYYABBgIUGCqv/iK1CCQOINwdEAwIVDC4QVVmf/G4QNBCooFCCoRRBCqv//4WBCpJsECrb0DC4QVDYYgHBCq8zCohEDNAIGBCrgTBCob4DZAQGCCgYZBCqoICAAqUBdwQUCKYLaCCsACBCwP/IAP/IBQVVCgQECCgRrBmYVfAAhACe4QUOCqzwCCoKCLCrYWDCiIVWeQYVY")), + }, + { + width: 48, + height: 60, + buffer: require("heatshrink").decompress(atob("iIAEzIAFBIYQFAAwXXDAVEzOIxAaCogXlDAPdDAYZBC8wPBC4oYBC8h2FC84VBB4QCB///C4X/DBQXWIIwWBDAQCBC8AWGCQMAgAVBAQIXgOwR4DC9JLGC83/JIuIoimColEDYNEC7wYCHYIACondCoPdAAQXDDoIXYCoT7CC4gAFC4RLBC7IWHDAkACw4XZToY9CJI51EC7ZICAoIJBCAMADIQJCC8IADCAQXDbIJHBDgIWJC7AABCgOIPAdEAwIXDDJIXWCwQXFAoQXCcIIXnOwgXjDIQXDaogHBC8JKDOQIGBC8zaGAwQWDDYIXeDYkADAIPBUoX/IxAXeAQIYB/5GB/5GMC64WCAgQWCOoMzC8YAEIwVEapIXhDANEC4KONC7oYDCyYXYDAIABC7QA==")), + }, +]; + +const waitingBuffers = [ + { + width: 36, + height: 62, + offset: -1, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECKYOComZxAVDogRXBQfdCQQABCLITCogRFCQIRYLAgAFCQwRQBoR7C/5aECKwwF/4ABBAQDBCK4TD/8AgH/HQYRUYYYRGzICCCKgUECIoDBCLA3DolEB4IDBAAQRb7oMB7vdHIIRWCAYRCAAoRWCoozDCBQRRBQI3FERIRPPIbGBPIQiICKAQCdQRZDAYIRXCQIJB/4RDEhQRQGYP//7FEGpARQCAYREgEzERQRNAAIRCLIKkBCLg4CYoIABB5IRWAQQRcBoIODCRYRQB4INECLkRgH/ZAMACRgRSmbGBCIIFBmYWCCLAAC7oCBdgNEAgIRWBQY2B7oTBAwMzCYoRUBIIQBCQQGBHIYRUABwRRA")), + }, + { + width: 38, + height: 62, + offset: 1, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECSoPComZxAWDogSZBYfdCYQABCTYUCogSFCYISaLogAFCY4SRBwSCC/5fECTAzF/4ABBAQDBCTIUD/8AgH/HoYSWZwYSGzICCCSwVECQoDBCTQ6DolECAIDBAAQSd7oNB7vdHgISYCIYSCAAoSYCwo2DCJYSSBYI6FEhQSQQAbRBQAQkJCSIRCe4ReDAYISZCYIKB/4SDExYSRGwP//7QEHBISRCIYSEgEzEhYSOAAISCLwKtBCTw7CaAIABCBQSXAQQSeBwIPDCZgSRCAIOECT0RgH/aYMACYUzAgQSambRBBoIFBmdEgAAECSwAC7oCBfINECgIABCSwLDHIPdCgIRDEYgSWBYIRBCYSFBQwwSTABKpICR4A=")), + }, +]; + +const bg = { + width: 384, + height: 153, + bpp: 8, + transparent: 254, + buffer: require("heatshrink").decompress(atob("i4ASj0evF4pFIDKYA/AEp//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5/bx+PB5vX64RFC54v3P/4v/P/4v/P/4v/P/4v/P/4v7CY5/VHIKxZACYvvP/4v/P/4v/P4/GAAR/BAAJP/P/5P/P/5P/F8sjkYnBO4oAHP64vHGKJfdF7p//P/5//P/4dTP4dIpB//P/5//P/5//F+p/TIIpDXMIohLF95//P/5//P/4APj0eP/5//P/5//P/4vxB4Z/XAH4AfP/4A/P/4A/P/4A/P/4A/P/4A/AB5/JTIaXJWZobFDpovvP/5//P/5//P/5//P/5//P/5//AAIPFCpp/JEpoABIIZHBL7IvvR45//P/5//P/5//P/5//P/5//P/IbTP6IAbJIpLXF+Z//F/5//F/5//F/5//F/5//F/5/tAH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH4AP4wACP4IABI/5//I/5//I/5//I/5//I/5//I/6B7PoNIpBF/P/5F/P/5F/P/5F/P/5F/P/5F/AHMejx//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//AHfGAAR/BAAJH/P/5H/P/5H/P/5H/P/5H/P/5H/QPZ9BpFIIv5//Iv4Aj6XSP/4A/P/4A/P/4A96XSP/4A/ACcejx//P/5//AEplB6XSAYJ//AH5//AH5//AHJlB6XSAYJ//AH5//AH5//AGPSAARhBA4oABA4ILBA4p//AH5//AH5//AE/SAARhBA4oABA4ILBA4p//AH5//AH5//AE5dB6QACAofX64HFAoZ//AH5//AH5//AFJdB6QACAofX64HFAoZ//AH5//AH5//AFJdB6XS6/XAYIFLCYJ//AH5//AH5//AFJdB6XS6/XAYIFLCYJ//AH5//AH5//AFZfB6XS6/XhgAEA4ILBB4J//AH5//AH5//AFpfB6XS6/XhgAEA4ILBB4J//AH5//AH5//AE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")), +}; + +const topSpeed = 15; +const timeout = 200; +let currentSpeed = 0; +let currentSonic = -1; + +let drawTimeout, drawInterval, waitTimeout; +let bgScroll = [0, null]; + +const start = () => { + if (drawTimeout) clearTimeout(drawTimeout); + if (waitTimeout) clearTimeout(waitTimeout); + if (drawInterval) clearInterval(drawInterval); + + drawInterval = setInterval(() => { + draw("start"); + bgScroll[0] += currentSpeed; + if (bgScroll[1]) bgScroll[1] += currentSpeed; + if (currentSpeed < topSpeed) currentSpeed++; + }, timeout); +}; + +const stop = () => { + if (drawTimeout) clearTimeout(drawTimeout); + if (drawInterval) clearInterval(drawInterval); + + drawInterval = setInterval(() => { + if (currentSpeed <= 0) { + clearInterval(drawInterval); + draw("reset"); + } else { + draw("stop"); + bgScroll[0] += currentSpeed; + if (bgScroll[1]) bgScroll[1] += currentSpeed; + currentSpeed--; + } + }, timeout); +}; + +const wait = () => { + currentSonic = -1; + currentSpeed = 0; + if (drawTimeout) clearTimeout(drawTimeout); + if (drawInterval) clearInterval(drawInterval); + Bangle.setLCDPower(1); + + drawInterval = setInterval(() => draw("wait"), timeout); + + waitTimeout = setTimeout(() => { + clearInterval(drawInterval); + currentSonic = -1; + draw("reset"); + }, 7500); +}; + +const queueDraw = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw("reset"); + }, 60000 - (Date.now() % 60000)); +}; + +const drawSonic = (action) => { + let target; + + if (action === "reset" || currentSonic === -1) { + target = defaultSonic; + } else if (action === "start") { + target = startingBuffers[currentSonic]; + } else if (action === "stop") { + if (currentSonic > 1) currentSonic = 0; + target = stoppingBuffers[currentSonic]; + } else { + target = waitingBuffers[currentSonic]; + } + + sonic.width = target.width; + sonic.height = target.height; + sonic.buffer = target.buffer; + sonic.offset = target.offset; + + g.drawImage( + sonic, + vw / 2 - 30 + (50 - sonic.width) + (sonic.offset || 0), + 86 + (65 - sonic.height) + ); + + if (action === "start") { + if (currentSonic === startingBuffers.length - 1) { + currentSonic = 6; + } else { + currentSonic++; + } + } else if (action === "stop") { + if (currentSpeed <= 2) { + currentSonic = -1; + } else if (currentSpeed <= 14) { + currentSonic = 1; + } else { + currentSonic = 0; + } + } else { + if (currentSonic === waitingBuffers.length - 1) { + currentSonic = 0; + } else { + currentSonic++; + } + } +}; + +const drawTime = () => { + const x = vw / 2; + const y = 24 + 25; + + const date = new Date(); + const timeStr = require("locale").time(date, 1).trim(); + const dateStr = require("locale").date(date).toUpperCase(); + + g.setColor("#000"); + g.setFontAlign(0, 0).setFont("6x8", 5); + g.drawString(timeStr, x + 3, y + 2); + + g.setFont("6x8", 1.5); + g.drawString(dateStr, x + 1, y + 29); + + g.setColor("#fff"); + g.setFontAlign(0, 0).setFont("6x8", 5); + g.drawString(timeStr, x, y); + + g.setFont("6x8", 1.5); + g.drawString(dateStr, x, y + 28); +}; + +const draw = (action) => { + if (bgWidth - bgScroll[0] < 0) { + bgScroll[0] = bgScroll[1]; + bgScroll[1] = null; + } + + g.drawImage(bg, -bgScroll[0], 24); + + if (bgWidth - bgScroll[0] < vw) { + bgScroll[1] = bgScroll[0] - bgWidth; + g.drawImage(bg, -bgScroll[1], 24); + } + + drawSonic(action); + drawTime(); + if (action === "reset") queueDraw(); +}; + +g.setTheme({ bg: "#0099ff", fg: "#fff", dark: true }).clear(); + +Bangle.on("lock", (locked) => { + if (locked) { + stop(); + } else { + start(); + } +}); + +Bangle.on("twist", () => wait()); + +Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 12000, + twistThreshold: 1600, +}); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +draw("reset"); + +if (Bangle.isLocked()) { + stop(); +} else { + start(); +} diff --git a/apps/sonicclk/app.png b/apps/sonicclk/app.png new file mode 100644 index 000000000..db359d1e5 Binary files /dev/null and b/apps/sonicclk/app.png differ diff --git a/apps/sonicclk/metadata.json b/apps/sonicclk/metadata.json new file mode 100644 index 000000000..5c907347f --- /dev/null +++ b/apps/sonicclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "sonicclk", + "name": "Sonic Clock", + "version": "0.02", + "description": "A classic sonic clock featuring run, stop and wait animations.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"sonicclk.app.js","url":"app.js"}, + {"name":"sonicclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/sonicclk/screenshot.png b/apps/sonicclk/screenshot.png new file mode 100644 index 000000000..63173989a Binary files /dev/null and b/apps/sonicclk/screenshot.png differ diff --git a/apps/speedalt/ChangeLog b/apps/speedalt/ChangeLog index de5c9c221..09d33f615 100644 --- a/apps/speedalt/ChangeLog +++ b/apps/speedalt/ChangeLog @@ -5,6 +5,6 @@ 0.05: Add setting to turn vibrate on/off. 0.06: Tweaks to vibration settings. 0.07: Switch to BTN1 for Max toggle and reset function. -1.00: New features. Added waypoints file and distance to selected waypoint display. Added integration with GPS Setup module to switch GPS to low power mode when screen off. Save display settings and restore when app restarted. -1.01: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight. -1.02: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. +0.08: New features. Added waypoints file and distance to selected waypoint display. Added integration with GPS Setup module to switch GPS to low power mode when screen off. Save display settings and restore when app restarted. +0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight. +0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json new file mode 100644 index 000000000..458023278 --- /dev/null +++ b/apps/speedalt/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "speedalt", + "name": "GPS Adventure Sports", + "shortName": "GPS Adv Sport", + "version": "0.10", + "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", + "icon": "app.png", + "type": "app", + "tags": "tool,outdoors", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"speedalt.app.js","url":"app.js"}, + {"name":"speedalt.img","url":"app-icon.js","evaluate":true}, + {"name":"speedalt.settings.js","url":"settings.js"} + ], + "data": [{"name":"speedalt.json"}] +} diff --git a/apps/speedalt2/ChangeLog b/apps/speedalt2/ChangeLog index 91f01988e..09a5eb8cc 100644 --- a/apps/speedalt2/ChangeLog +++ b/apps/speedalt2/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial import. -0.07: Add swipe to change screens. +0.02: Add swipe to change screens. +0.03: Misc memory and screen optimisations. diff --git a/apps/speedalt2/app.js b/apps/speedalt2/app.js index 0db9629c7..fd53cf7c3 100644 --- a/apps/speedalt2/app.js +++ b/apps/speedalt2/app.js @@ -1,11 +1,9 @@ /* Speed and Altitude [speedalt2] Mike Bennett mike[at]kereru.com -0.01 : Initial -0.06 : Add Posn screen -0.07 : Add swipe to change screens same as BTN3 +1.10 : add inverted colours */ -var v = '1.05'; +var v = '1.10'; /*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ var KalmanFilter = (function () { @@ -173,6 +171,15 @@ var KalmanFilter = (function () { var buf = Graphics.createArrayBuffer(240,160,2,{msb:true}); +let LED = // LED as minimal and only definition (as instance / singleton) +{ isOn: false // status on / off, not needed if you don't need to ask for it +, set: function(v) { // turn on w/ no arg or truey, else off + g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(40,10,10); } +, reset: function() { this.set(false); } // turn off +, write: function(v) { this.set(v); } // turn on w/ no arg or truey, else off +, toggle: function() { this.set( ! this.isOn); } // toggle the LED +}, LED1 = LED; // LED1 as 'synonym' for LED + // Load fonts //require("Font7x11Numeric7Seg").add(Graphics); @@ -183,17 +190,16 @@ var canDraw = 1; var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time. var tmrLP; // Timer for delay in switching to low power after screen turns off -var max = {}; -max.spd = 0; -max.alt = 0; -max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data. +var maxSpd = 0; +var maxAlt = 0; +var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data. var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values; var wp = {}; // Waypoint to use for distance from cur position. -function nxtWp(inc){ - cfg.wp+=inc; +function nxtWp(){ + cfg.wp++; loadWp(); } @@ -227,7 +233,8 @@ function drawScrn(dat) { if (!canDraw) return; buf.clear(); - + buf.setBgColor(0); + var n; n = dat.val.toString(); @@ -252,29 +259,21 @@ function drawScrn(dat) { buf.setFontVector(35); buf.drawString(dat.unit,5,164); - if ( dat.max ) drawMax(); // MAX display indicator - if ( dat.wp ) drawWP(); // Waypoint name - - //Sats - if ( dat.sat ) { - if ( dat.age > 10 ) { - if ( dat.age > 90 ) dat.age = '>90'; - drawSats('Age:'+dat.age); - } - else drawSats('Sats:'+dat.sats); - } - + drawMax(dat.max); // MAX display indicator + drawWP(dat.wp); // Waypoint name + drawSats(dat.sats); + g.reset(); g.drawImage(img,0,40); - if ( pwrSav ) LED1.reset(); - else LED1.set(); + LED1.write(!pwrSav); } function drawPosn(dat) { if (!canDraw) return; buf.clear(); + buf.setBgColor(0); var x, y; x=210; @@ -293,20 +292,12 @@ function drawPosn(dat) { buf.drawString(dat.ew,x,y+70); - //Sats - if ( dat.sat ) { - if ( dat.age > 10 ) { - if ( dat.age > 90 ) dat.age = '>90'; - drawSats('Age:'+dat.age); - } - else drawSats('Sats:'+dat.sats); - } + drawSats(dat.sats); g.reset(); g.drawImage(img,0,40); - if ( pwrSav ) LED1.reset(); - else LED1.set(); + LED1.write(!pwrSav); } @@ -314,6 +305,8 @@ function drawClock() { if (!canDraw) return; buf.clear(); + buf.setBgColor(0); + var x, y; x=185; y=0; @@ -329,19 +322,14 @@ function drawClock() { g.reset(); g.drawImage(img,0,40); - if ( pwrSav ) LED1.reset(); - else LED1.set(); + LED1.write(!pwrSav); } -function drawWP() { - var nm = wp.name; - if ( nm == undefined || nm == 'NONE' || cfg.modeA ==1 ) nm = ''; - buf.setColor(2); - +function drawWP(wp) { + buf.setColor(3); buf.setFontAlign(0,1); //left, bottom buf.setFontVector(48); - buf.drawString(nm.substring(0,8),120,140); - + buf.drawString(wp,120,140); } function drawSats(sats) { @@ -351,16 +339,15 @@ function drawSats(sats) { buf.drawString(sats,240,160); } -function drawMax() { +function drawMax(max) { buf.setFontVector(30); buf.setColor(2); buf.setFontAlign(0,1); //centre, bottom - buf.drawString('MAX',120,164); + buf.drawString(max,120,164); } function onGPS(fix) { - - if ( emulator ) { +if ( emulator ) { fix.fix = 1; fix.speed = 10 + (Math.random()*5); fix.alt = 354 + (Math.random()*50); @@ -382,10 +369,15 @@ function onGPS(fix) { var ns = ''; var ew = ''; var lon = '---.--'; + var sats = '---'; + // Waypoint name + var wpName = wp.name; + if ( wpName == undefined || wpName == 'NONE' ) wpName = ''; + wpName = wpName.substring(0,8); if (fix.fix) lf = fix; - + if (lf.fix) { // Smooth data @@ -393,10 +385,9 @@ function onGPS(fix) { if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed); if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt); lf.smoothed = 1; - if ( max.n <= 15 ) max.n++; + if ( maxN <= 15 ) maxN++; } - // Speed if ( cfg.spd == 0 ) { m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units @@ -407,18 +398,19 @@ function onGPS(fix) { if ( sp < 10 ) sp = sp.toFixed(1); else sp = Math.round(sp); + if (isNaN(sp)) sp = '---'; - if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = sp; + if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp; // Altitude al = lf.alt; al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); - - if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = al; + if (parseFloat(al) > parseFloat(maxAlt) && maxN > 15 ) maxAlt = al; + if (isNaN(al)) al = '---'; // Distance to waypoint di = distance(lf,wp); - if (isNaN(di)) di = 0; + if (isNaN(di)) di = '--------'; // Age of last fix (secs) age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); @@ -431,6 +423,13 @@ function onGPS(fix) { ew = 'E'; if ( lf.lon < 0 ) ew = 'W'; lon = Math.abs(lf.lon.toFixed(2)); + + // Sats + if ( age > 10 ) { + sats = 'Age:'+Math.round(age); + if ( age > 90 ) sats = 'Age:>90'; + } + else sats = 'Sats:'+lf.satellites; } @@ -438,23 +437,21 @@ function onGPS(fix) { // Speed if ( showMax ) drawScrn({ - val:max.spd, + val:maxSpd, unit:cfg.spd_unit, - sats:lf.satellites, + sats:sats, age:age, - max:true, - wp:false, - sat:true + max:'MAX', + wp:'' }); // Speed maximums else drawScrn({ val:sp, unit:cfg.spd_unit, - sats:lf.satellites, + sats:sats, age:age, - max:false, - wp:false, - sat:true + max:'SPD', + wp:'' }); } @@ -462,23 +459,21 @@ function onGPS(fix) { // Alt if ( showMax ) drawScrn({ - val:max.alt, + val:maxAlt, unit:cfg.alt_unit, - sats:lf.satellites, + sats:sats, age:age, - max:true, - wp:false, - sat:true + max:'MAX', + wp:'' }); // Alt maximums else drawScrn({ val:al, unit:cfg.alt_unit, - sats:lf.satellites, + sats:sats, age:age, - max:false, - wp:false, - sat:true + max:'ALT', + wp:'' }); } @@ -487,24 +482,22 @@ function onGPS(fix) { drawScrn({ val:di, unit:cfg.dist_unit, - sats:lf.satellites, + sats:sats, age:age, - max:false, - wp:true, - sat:true + max:'DST', + wp:wpName }); } if ( cfg.modeA == 3 ) { // Position - drawPosn({ - sats:lf.satellites, + drawPosn({ + sats:sats, age:age, lat:lat, lon:lon, ns:ns, - ew:ew, - sat:true + ew:ew }); } @@ -534,9 +527,9 @@ function nextFunc(dur) { if ( cfg.modeA == 0 || cfg.modeA == 1 ) { // Spd+Alt mode - Switch between fix and MAX if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display - else { max.spd = 0; max.alt = 0; } // Long press resets max values. + else { maxSpd = 0; maxAlt = 0; } // Long press resets max values. } - else if ( cfg.modeA == 2) nxtWp(1); // Dist mode - Select next waypoint + else if ( cfg.modeA == 2) nxtWp(); // Dist mode - Select next waypoint onGPS(lf); } @@ -545,7 +538,7 @@ function updateClock() { if (!canDraw) return; if ( cfg.modeA != 4 ) return; drawClock(); - if ( emulator ) {max.spd++;max.alt++;} + if ( emulator ) {maxSpd++;maxAlt++;} } function startDraw(){ @@ -585,7 +578,6 @@ function setButtons(){ setWatch(function(e){ pwrSav=!pwrSav; if ( pwrSav ) { - LED1.reset(); var s = require('Storage').readJSON('setting.json',1)||{}; var t = s.timeout||10; Bangle.setLCDTimeout(t); @@ -593,8 +585,8 @@ function setButtons(){ else { Bangle.setLCDTimeout(0); // Bangle.setLCDPower(1); - LED1.set(); } + LED1.write(!pwrSav); }, BTN2, {repeat:true,edge:"falling"}); // BTN3 - next screen @@ -690,7 +682,8 @@ var img = { }; if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]); -if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xFF800,0xFAE0,0xF813]); +if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xF800,0xFAE0,0xF813]); +if ( cfg.colour == 3 ) img.palette = new Uint16Array([0xFFFF,0x007F,0x0054,0x0054]); var SCREENACCESS = { withApp:true, diff --git a/apps/speedalt2/metadata.json b/apps/speedalt2/metadata.json new file mode 100644 index 000000000..a78039f54 --- /dev/null +++ b/apps/speedalt2/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "speedalt2", + "name": "GPS Adventure Sports II", + "shortName":"GPS Adv Sport II", + "version":"0.03", + "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", + "icon": "app.png", + "type": "app", + "tags": "tool,outdoors", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"speedalt2.app.js","url":"app.js"}, + {"name":"speedalt2.img","url":"app-icon.js","evaluate":true}, + {"name":"speedalt2.settings.js","url":"settings.js"} + ], + "data": [{"name":"speedalt2.json"}] +} diff --git a/apps/speedalt2/settings.js b/apps/speedalt2/settings.js index 96174a89b..fe30d88df 100644 --- a/apps/speedalt2/settings.js +++ b/apps/speedalt2/settings.js @@ -65,7 +65,8 @@ '< Back': function() { E.showMenu(appMenu); }, 'Default' : function() { setColour(0); }, 'Hi Contrast' : function() { setColour(1); }, - 'Night' : function() { setColour(2); } + 'Night' : function() { setColour(2); }, + 'Inverted' : function() { setColour(3); } }; const kalMenu = { diff --git a/apps/speedo/metadata.json b/apps/speedo/metadata.json new file mode 100644 index 000000000..f081e73da --- /dev/null +++ b/apps/speedo/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "speedo", + "name": "Speedo", + "version": "0.05", + "description": "Show the current speed according to the GPS", + "icon": "speedo.png", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"speedo.app.js","url":"speedo.js"}, + {"name":"speedo.img","url":"speedo-icon.js","evaluate":true} + ] +} diff --git a/apps/stepo/metadata.json b/apps/stepo/metadata.json new file mode 100644 index 000000000..6706b7eff --- /dev/null +++ b/apps/stepo/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "stepo", + "name": "Stepometer Clock", + "version": "0.03", + "description": "A large font watch, displays step count in a doughnut guage and warns of low battery, requires one of the steps widgets to be installed", + "icon": "stepo.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"stepo.app.js","url":"app.js"}, + {"name":"stepo.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/stetho/metadata.json b/apps/stetho/metadata.json new file mode 100644 index 000000000..6328f49de --- /dev/null +++ b/apps/stetho/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "stetho", + "name": "Stethoscope", + "version": "0.01", + "description": "Hear your heart rate", + "icon": "stetho.png", + "tags": "health", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"stetho.app.js","url":"stetho.js"}, + {"name":"stetho.img","url":"stetho-icon.js","evaluate":true} + ] +} diff --git a/apps/stopwatch/README.md b/apps/stopwatch/README.md index 30a9306d1..ceeafaefc 100644 --- a/apps/stopwatch/README.md +++ b/apps/stopwatch/README.md @@ -31,3 +31,6 @@ Which one is which ? ![](A.jpg) ![](B.jpg) + + +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/stopwatch/metadata.json b/apps/stopwatch/metadata.json new file mode 100644 index 000000000..e72d85af1 --- /dev/null +++ b/apps/stopwatch/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "stopwatch", + "name": "Stopwatch Touch", + "version": "0.01", + "description": "A touch based stop watch for Bangle JS 2", + "icon": "stopwatch.png", + "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], + "tags": "tools,app", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"stopwatch.app.js","url":"stopwatch.app.js"}, + {"name":"stopwatch.img","url":"stopwatch.icon.js","evaluate":true} + ] +} diff --git a/apps/supf/ChangeLog b/apps/supf/ChangeLog new file mode 100644 index 000000000..55ebb3e4b --- /dev/null +++ b/apps/supf/ChangeLog @@ -0,0 +1 @@ +0.01: New App diff --git a/apps/supf/README.md b/apps/supf/README.md new file mode 100644 index 000000000..258a69d4b --- /dev/null +++ b/apps/supf/README.md @@ -0,0 +1,4 @@ +# Simple Clock with Date +Simple Clock with seconds and date in custom language. Install 'Languages' to get localized names. + +![](screenshot_supf.png) diff --git a/apps/supf/app.js b/apps/supf/app.js new file mode 100644 index 000000000..d7a30aed9 --- /dev/null +++ b/apps/supf/app.js @@ -0,0 +1,56 @@ +require("Font7x11Numeric7Seg").add(Graphics); + +function draw() { + var d = new Date(); + var size = Math.floor(g.getWidth()/(7*6)); + var x = (g.getWidth()/2) - size*6, + y = (g.getHeight()/2) - size*7 - 0; + // y variable for ':' + var y_dop = 70 - 0; + g.reset().clearRect(0,y,g.getWidth(),y+size*28); + // draw hours in 24h format + g.setFont("7x11Numeric7Seg",size).setFontAlign(1,-1); + if (d.getHours().toString.length < 2) { + g.drawString('0'+d.getHours(), 58, y); + } + else { + g.drawString(d.getHours(), 58, y); + } + g.setFont("7x11Numeric7Seg",size/2).setFontAlign(1,-1); + g.drawString(":",64,y_dop); + g.setFont("7x11Numeric7Seg",size).setFontAlign(1,-1); + // draw minutes + g.drawString(("0"+d.getMinutes()).substr(-2),118,y); + g.setFont("7x11Numeric7Seg",size/2).setFontAlign(1,-1); + g.drawString(":",124,y_dop); + // draw seconds + g.setFont("7x11Numeric7Seg",size).setFontAlign(1,-1); + g.drawString(("0"+d.getSeconds()).substr(-2),178,y); + // date + g.setFont("6x8",size/2).setFontAlign(0,-1); + // draw name of day + g.drawString(require('locale').dow(new Date()),g.getWidth()/2, y + size*16); + // draw date and name of month + g.drawString(d.getDate()+' '+require('locale').month(new Date()),g.getWidth()/2, y + size*20); + // draw year + g.drawString((d.getFullYear()),g.getWidth()/2, y + size*24); + +} +// Only update when display turns on +if (process.env.BOARD!="SMAQ3") // hack for Q3 which is always-on +Bangle.on('lcdPower', function(on) { + if (secondInterval) + clearInterval(secondInterval); + secondInterval = undefined; + if (on) + secondInterval = setInterval(draw, 1000); + draw(); +}); + +g.clear(); +var secondInterval = setInterval(draw, 1000); +draw(); +// Show launcher when button pressed +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/supf/icon.js b/apps/supf/icon.js new file mode 100644 index 000000000..9b880b5e9 --- /dev/null +++ b/apps/supf/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UBAoP/AAXnnNVAH4ACoEABZJXBgoLUGaIAIgILLbf4AUnWqweq1gEB4QEBBZ0OwEA9k7h3C2ALGlk4BZAeBBZAvCBZOv/gLJ2EABZOggE7hQLFL5U+1XDBYYEB3jB/AClABRMBqoAXEhAiCBZdQBY8FHTAAj")) diff --git a/apps/supf/icon.png b/apps/supf/icon.png new file mode 100644 index 000000000..2ca4d6140 Binary files /dev/null and b/apps/supf/icon.png differ diff --git a/apps/supf/metadata.json b/apps/supf/metadata.json new file mode 100644 index 000000000..324e37b84 --- /dev/null +++ b/apps/supf/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "supf", + "name": "Simple Clock with Date", + "shortName": "supf Clock", + "version": "0.01", + "description": "Simple Clock with seconds and date in custom language. Install 'Languages' to get localized names.", + "icon": "icon.png", + "screenshots": [{"url":"screenshot_supf.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"supf.app.js","url":"app.js"}, + {"name":"supf.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/supf/screenshot_supf.png b/apps/supf/screenshot_supf.png new file mode 100644 index 000000000..18e391f28 Binary files /dev/null and b/apps/supf/screenshot_supf.png differ diff --git a/apps/supmariodark/metadata.json b/apps/supmariodark/metadata.json new file mode 100644 index 000000000..b56b19735 --- /dev/null +++ b/apps/supmariodark/metadata.json @@ -0,0 +1,25 @@ +{ + "id": "supmariodark", + "name": "Super mario clock night mode", + "shortName": "supmariodark", + "version": "0.01", + "description": "Super mario clock in night mode", + "icon": "supmariodark.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"supmariodark.app.js","url":"supmariodark.js"}, + {"name":"supmariodark.img","url":"supmariodark-icon.js","evaluate":true}, + {"name":"supmario30x24.bin","url":"supmario30x24.bin.js"}, + {"name":"supmario30x24.wdt","url":"supmario30x24.wdt.js"}, + {"name":"banner-up.img","url":"banner-up.js","evaluate":true}, + {"name":"banner-down.img","url":"banner-down.js","evaluate":true}, + {"name":"brick2.img","url":"brick2.js","evaluate":true}, + {"name":"enemy.img","url":"enemy.js","evaluate":true}, + {"name":"flower.img","url":"flower.js","evaluate":true}, + {"name":"flower_b.img","url":"flower_b.js","evaluate":true}, + {"name":"mario_wh.img","url":"mario_wh.js","evaluate":true}, + {"name":"pipe.img","url":"pipe.js","evaluate":true} + ] +} diff --git a/apps/svclock/metadata.json b/apps/svclock/metadata.json new file mode 100644 index 000000000..aa213f168 --- /dev/null +++ b/apps/svclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "svclock", + "name": "Simple V-Clock", + "version": "0.04", + "description": "Modification of Simple Clock 0.04 to use Vectorfont", + "icon": "vclock-simple.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"bangle2-simple-v-clock-screenshot.png"}], + "storage": [ + {"name":"svclock.app.js","url":"vclock-simple.js"}, + {"name":"svclock.img","url":"vclock-simple-icon.js","evaluate":true} + ] +} diff --git a/apps/swatch/metadata.json b/apps/swatch/metadata.json new file mode 100644 index 000000000..6c76d3fa2 --- /dev/null +++ b/apps/swatch/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "swatch", + "name": "Stopwatch", + "version": "0.07", + "description": "Simple stopwatch with Lap Time logging to a JSON file", + "icon": "stopwatch.png", + "tags": "health", + "supports": ["BANGLEJS"], + "readme": "README.md", + "interface": "interface.html", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-stopwatch-screenshot.png"}], + "storage": [ + {"name":"swatch.app.js","url":"stopwatch.js"}, + {"name":"swatch.img","url":"stopwatch-icon.js","evaluate":true} + ] +} diff --git a/apps/sweepclock/metadata.json b/apps/sweepclock/metadata.json new file mode 100644 index 000000000..179d5d95e --- /dev/null +++ b/apps/sweepclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "sweepclock", + "name": "Sweep Clock", + "version": "0.04", + "description": "Smooth sweep secondhand with single hour numeral. Use button 1 to toggle the numeral font, button 3 to change the colour theme and button 4 to change the date placement", + "icon": "sweepclock.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-sweep-clock-screenshot.png"}], + "storage": [ + {"name":"sweepclock.app.js","url":"sweepclock.js"}, + {"name":"sweepclock.img","url":"sweepclock-icon.js","evaluate":true} + ] +} diff --git a/apps/swiperclocklaunch/metadata.json b/apps/swiperclocklaunch/metadata.json new file mode 100644 index 000000000..733aaa032 --- /dev/null +++ b/apps/swiperclocklaunch/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "swiperclocklaunch", + "name": "Swiper Clock Launch", + "version": "0.02", + "description": "Navigate between clock and launcher with Swipe action", + "icon": "swiperclocklaunch.png", + "type": "bootloader", + "tags": "tools, system", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"swiperclocklaunch.boot.js","url":"boot.js"}, + {"name":"swiperclocklaunch.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/swlclk/metadata.json b/apps/swlclk/metadata.json new file mode 100644 index 000000000..2c0d39c5a --- /dev/null +++ b/apps/swlclk/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "swlclk", + "name": "SWL Clock / Short Wave Listner Clock", + "shortName": "SWL Clock", + "version": "0.02", + "description": "Display Local, UTC time and some programs on the shorts waves along the day, with the frequencies", + "icon": "swlclk.png", + "type": "clock", + "tags": "tool,clock", + "supports": ["BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-SWL-clock-screenshot.png"}], + "storage": [ + {"name":"swlclk.app.js","url":"app.js"}, + {"name":"swlclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/swp2clk/ChangeLog b/apps/swp2clk/ChangeLog new file mode 100644 index 000000000..ea6473980 --- /dev/null +++ b/apps/swp2clk/ChangeLog @@ -0,0 +1 @@ +0.01: Initial creation of "Swipe back to the Clock" App. Let's you swipe from left to right on any app to return back to the clock face. diff --git a/apps/swp2clk/README.md b/apps/swp2clk/README.md new file mode 100644 index 000000000..877a13ecc --- /dev/null +++ b/apps/swp2clk/README.md @@ -0,0 +1,22 @@ +# Swipe back to the Clock + +Let's you swipe from left to right on any app to return back to the clock face. + +## Configurable Modes: + +The swipe modes can be configured in the settings app, under "Swipe to Clock". + +- Always Off: Deactivated for all apps (Default) +- White List: Only activate for chosen apps, otherwise deactivated for all apps. +- Black List: Only disabled for chosen apps, otherwise activated for all apps. +- Always On: Active for all apps (Not actually recommended! E.g. Games need to be able to deal with swipe gestures) + +## Motivation: + +The goal is to further support touch-only usage of the BangleJS 2 watch. You can use the [Swiper Clock Launch](https://banglejs.com/apps/#swiper%20clock%20launch) or the [Pattern Launcher](https://banglejs.com/apps/#pattern%20launcher) to further enable touch-only support of the watch. + +## Credits: + +Initial creation: [crazysaem](https://github.com/crazysaem) + +Inspired by: [Swiper Clock Launch](https://banglejs.com/apps/#swiper%20clock%20launch) \ No newline at end of file diff --git a/apps/swp2clk/app.png b/apps/swp2clk/app.png new file mode 100644 index 000000000..b964520e6 Binary files /dev/null and b/apps/swp2clk/app.png differ diff --git a/apps/swp2clk/boot.js b/apps/swp2clk/boot.js new file mode 100644 index 000000000..bb8e792c4 --- /dev/null +++ b/apps/swp2clk/boot.js @@ -0,0 +1,109 @@ +/** + * How does this work? + * + * Every *boot.js file is executed everytime any app is loaded, including this one. + * We just need to figure out which app is currently loaded, in case we are in the white list / black list mode, + * to figure out if the swipe handler should be attached or not. + * It does not seem to be the case that this can be done easily, but we can work around it. + * It seems that every app is loaded via the global "load" function, which takes a fileName as it's first parameter to load any app + * or the default clock when the fileName is undefined. + * To be able to use this for us, we wrap the global "load" function, and determine before loading the next app, + * whether the swipe handler should be added or not, since we now know which app will be loaded. + * Note: We cannot add the swipe handler inside the wrapped "load" function, because once the "load" function is complete + * everything is cleaned up. That's why we merely save a flag, whether the swipe handler should be attached or not, + * which is evaluated once this file is executed again, which will be right after the load function completes + * (since every *boot.js file is executed everytime any app is loaded). + */ + +(function () { + var DEBUG = false; + var FILE = "swp2clk.data.json"; + + var main = () => { + var settings = readSettings(); + + if (settings.addSwipeHandler) { + var swipeHandler = (dir) => { + log("swipe"); + log(dir); + if (dir === 1) { + load(); + } + }; + Bangle.on("swipe", swipeHandler); + } + + var global_load = global.load; + global.load = (fileName) => { + log("loading filename!"); + log(fileName); + var settings = readSettings(); + + if (fileName) { + // "Off" + if (settings.mode === 0) { + settings.addSwipeHandler = false; + } + + // "White List" + if (settings.mode === 1) { + if (settings.whiteList.indexOf(fileName) >= 0) { + settings.addSwipeHandler = true; + } else { + settings.addSwipeHandler = false; + } + } + + // "Black List" + if (settings.mode === 2) { + if (settings.blackList.indexOf(fileName) >= 0) { + settings.addSwipeHandler = false; + } else { + settings.addSwipeHandler = true; + } + } + + // "Always" + if (settings.mode === 3) { + settings.addSwipeHandler = true; + } + } else { + // Clock will load + settings.addSwipeHandler = false; + } + + writeSettings(settings); + global_load(fileName); + }; + }; + + // lib functions + + var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } + }; + + var readSettings = () => { + log("reading settings"); + var settings = require("Storage").readJSON(FILE, 1) || { + mode: 0, + whiteList: [], + blackList: [], + addSwipeHandler: false, + }; + log(settings); + return settings; + }; + + var writeSettings = (settings) => { + log("writing settings"); + log(settings); + require("Storage").writeJSON(FILE, settings); + }; + + // start main function + + main(); +})(); diff --git a/apps/swp2clk/metadata.json b/apps/swp2clk/metadata.json new file mode 100644 index 000000000..aa95a6473 --- /dev/null +++ b/apps/swp2clk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "swp2clk", + "name": "Swipe back to the Clock", + "shortName": "Swipe to Clock", + "version": "0.01", + "description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.", + "icon": "app.png", + "type": "boot", + "tags": "tools", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "swp2clk.boot.js", "url": "boot.js" }, + {"name":"swp2clk.settings.js","url":"settings.js"} + ], + "data": [{"name":"swp2clk.data.json"}] +} diff --git a/apps/swp2clk/settings.js b/apps/swp2clk/settings.js new file mode 100644 index 000000000..a97b51fab --- /dev/null +++ b/apps/swp2clk/settings.js @@ -0,0 +1,181 @@ +(function (back) { + var DEBUG = false; + var FILE = "swp2clk.data.json"; + + var settings = {}; + + var showMainMenu = () => { + log("Loading main menu"); + + E.showMenu({ + "": { title: "Swipe to Clock" }, + "< Back": () => back(), + Mode: { + value: settings.mode, + min: 0, + max: 3, + format: (value) => ["Off", "White List", "Black List", "Always"][value], + onchange: (value) => { + settings.mode = value; + writeSettings(settings); + }, + }, + "White List": () => showWhiteListMenu(), + "Black List": () => showBlackListMenu(), + }); + }; + + var showWhiteListMenu = () => { + var appList = getAppList(); + + var whiteListMenu = { + "": { title: "White List" }, + "< Back": () => showMainMenu(), + "_Add App_": () => { + var addAppMenu = { + "": { title: "Add app to WL" }, + "< Back": () => showWhiteListMenu(), + }; + + appList.forEach((app) => { + if (settings.whiteList.indexOf(app.src) < 0) { + addAppMenu[app.name] = () => { + settings.whiteList.push(app.src); + writeSettings(settings); + showWhiteListMenu(); + }; + } + }); + + E.showMenu(addAppMenu); + }, + }; + + appList.forEach((app) => { + if (settings.whiteList.indexOf(app.src) >= 0) { + whiteListMenu[app.name] = () => { + E.showPrompt("Delete from WL?", { + title: "Delete from WL?", + buttons: { Yes: true, No: false }, + }).then(function (flag) { + if (flag) { + settings.whiteList.splice(index, 1); + writeSettings(settings); + } + + showWhiteListMenu(); + }); + }; + } + }); + + log("Loading white list menu"); + E.showMenu(whiteListMenu); + }; + + var showBlackListMenu = () => { + var appList = getAppList(); + + var blackListMenu = { + "": { title: "Black List" }, + "< Back": () => showMainMenu(), + "_Add App_": () => { + var addAppMenu = { + "": { title: "Add app to BL" }, + "< Back": () => showBlackListMenu(), + }; + + appList.forEach((app) => { + if (settings.blackList.indexOf(app.src) < 0) { + addAppMenu[app.name] = () => { + settings.blackList.push(app.src); + writeSettings(settings); + showBlackListMenu(); + }; + } + }); + + E.showMenu(addAppMenu); + }, + }; + + appList.forEach((app) => { + if (settings.blackList.indexOf(app.src) >= 0) { + blackListMenu[app.name] = () => { + E.showPrompt("Delete from BL?", { + title: "Delete from BL?", + buttons: { Yes: true, No: false }, + }).then(function (flag) { + if (flag) { + settings.blackList.splice(index, 1); + writeSettings(settings); + } + + showBlackListMenu(); + }); + }; + } + }); + + log("Loading black list menu"); + E.showMenu(blackListMenu); + }; + + // lib functions + + var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } + }; + + var readSettings = () => { + log("reading settings"); + var settings = require("Storage").readJSON(FILE, 1) || { + mode: 0, + whiteList: [], + blackList: [], + addSwipeHandler: false, + }; + log(settings); + return settings; + }; + + var writeSettings = (settings) => { + log("writing settings"); + log(settings); + require("Storage").writeJSON(FILE, settings); + }; + + 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; + }; + + // start main function + + settings = readSettings(); + showMainMenu(); +}); diff --git a/apps/tabata/metadata.json b/apps/tabata/metadata.json new file mode 100644 index 000000000..14429090f --- /dev/null +++ b/apps/tabata/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "tabata", + "name": "Tabata", + "shortName": "Tabata - Control High-Intensity Interval Training", + "version": "0.01", + "description": "Control high-intensity interval training (according to tabata: https://en.wikipedia.org/wiki/Tabata_method).", + "icon": "tabata.png", + "tags": "workout,health", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"tabata.app.js","url":"tabata.js"}, + {"name":"tabata.img","url":"tabata-icon.js","evaluate":true} + ] +} diff --git a/apps/tapelauncher/icon.js b/apps/tapelauncher/icon.js index bf323e5bf..25ca0a4c6 100644 --- a/apps/tapelauncher/icon.js +++ b/apps/tapelauncher/icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("AH4A/ACXd7vQC6vUpoBBDaQXEDaQXIDZwXMAIQZHC4R6BAAIZJDAwXIDY4XHAAodJ7oXMDpQXSAAiRHhoWN7zFLDY/e9ve9zeMhvQCIIBFC5ARIC5oVNC5EOCpwABC4vuCZYXPCIwXOCJAAFC5gAJ8AXFCpwuHgDjCFqQXC6lN6gbFf5gXEAInd6AXVDYndhoXKBoIbMC5QZLC44AFDpIXNDpQXdhoYMAAbwIC6oZQbxhOKC5gbKC6BUGC6oA/AHgA==")) +require("heatshrink").decompress(atob("mEw4UA///sH8ov+8GyJf4AIgt8BZV9voNIBYQNIBYgNGBYwMEBYNVqoMEoALGBoYLDBQILCAQVQBYoOEBZIABBYUAgILGsBiEBodWy2gN4soywACBYcI1QJDBYoJFBYkCBQ2qBYUKBIoLHBAQLHBAYACBYwAEwALBgwKG1S/DC4wWCa4Y3Efa19mALKvrLDfY7XGBwjvVBYjuHfYgLLBg4LEAAMVBZQNEBZBPCBZQA+A")) diff --git a/apps/tapelauncher/metadata.json b/apps/tapelauncher/metadata.json new file mode 100644 index 000000000..fd5051749 --- /dev/null +++ b/apps/tapelauncher/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "tapelauncher", + "name": "Tape Launcher", + "version": "0.02", + "description": "An App launcher, icons displayed in a horizontal tape, swipe or use buttons", + "icon": "icon.png", + "type": "launch", + "tags": "tool,system,launcher", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"tapelauncher.app.js","url":"app.js"}, + {"name":"tapelauncher.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/teatimer/README.md b/apps/teatimer/README.md new file mode 100644 index 000000000..71bec3ea8 --- /dev/null +++ b/apps/teatimer/README.md @@ -0,0 +1,45 @@ +# Tea Timer app + +A simple timer. You can easyly set up the time. The initial time is 2:30 + +On the first screen, you can +- tap to get help +- swipe up/down to change the timer by +/- one minute +- swipe left/right to change the time by +/- 15 seconds +- press Btn1 to start + +Press Btn1 again to stop the timer +- when time is up, your Bangle will buzz for 15 seconds +- and it will count up to 60 seconds and stop after that + +## Images +_1. Startscreen_ + +![](TeatimerStart.jpg) +Current time is displayed below the Title. Initial time is 2:30. + +_2. Help Screen_ + +![](TeatimerHelp.jpg) + +_3. Tea Timer running_ + +![](TeatimerRun.jpg) +Remainig time is shown in big font size. Above the initial time is shown. + +_4. When time is up_ + +![](TeatimerUp.jpg) +When time is up, the watch will buzz for 15 seconds. It will count up to 60 seconds. + +## Requests + +Please mail any issues to thomas.fehling@mailbox.org + +## Creator + +Thomas Fehling + +## Attributions + +Icons used in this app are from https://icons8.com diff --git a/apps/teatimer/TeatimerHelp.jpg b/apps/teatimer/TeatimerHelp.jpg new file mode 100644 index 000000000..e22960c66 Binary files /dev/null and b/apps/teatimer/TeatimerHelp.jpg differ diff --git a/apps/teatimer/TeatimerRun.jpg b/apps/teatimer/TeatimerRun.jpg new file mode 100644 index 000000000..a442d12a5 Binary files /dev/null and b/apps/teatimer/TeatimerRun.jpg differ diff --git a/apps/teatimer/TeatimerStart.jpg b/apps/teatimer/TeatimerStart.jpg new file mode 100644 index 000000000..4fa8f2fc4 Binary files /dev/null and b/apps/teatimer/TeatimerStart.jpg differ diff --git a/apps/teatimer/TeatimerUp.jpg b/apps/teatimer/TeatimerUp.jpg new file mode 100644 index 000000000..80b8c3c8a Binary files /dev/null and b/apps/teatimer/TeatimerUp.jpg differ diff --git a/apps/teatimer/app-icon.js b/apps/teatimer/app-icon.js new file mode 100644 index 000000000..ae91f88d7 --- /dev/null +++ b/apps/teatimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AAONjQAoxoutGAYusGAQutABs4qwAUnAwYik4vQAQnEUMTcxqoAOm6Sdm9PAB0TF7sTF90awIAOFz0bxIAOjYv/F/2s1eq5wAI1Wr1gvg4/GABXH1Yvg5wvL5xffjWIMBfH1lkFzwwC1YvJ1eIF0CRC5CNJF0KRK5CNiGBS8BRsQwJ468jAA2JGAJdpF4et1ms1uJF9YADF1GIxIAD1qQoFwgwpLwoADF1jABxJgjLpGt2YwBjYugjaMIF4IwBsgvgsgvMxIwfLxIwC66RhF/ezF9q/B6/XF9xfrLwYvhjWBABGsAAOmwWBFz0Tp4AOiYvdm4vumNVAB0xFrcUnF6ACE4iguYnFWACk4ebwAcxouu/wwsFwIABGFQuCA==")) diff --git a/apps/teatimer/app.js b/apps/teatimer/app.js new file mode 100644 index 000000000..dd7afdadb --- /dev/null +++ b/apps/teatimer/app.js @@ -0,0 +1,233 @@ +// Tea Timer +// Button press stops timer, next press restarts timer +let drag; +var counter = 0; +var counterStart = 150; // 150 seconds +var counterInterval; +const states = { + init: 1, // unused + help: 2, // show help text + start: 4, // show/change initial counter + count: 8, // count down + countUp: 16, // count up after timer finished + stop: 32 // timer stopped +}; +var state = states.start; +E.setTimeZone(1); + +// Title showing current time +function appTitle() { + return "Tea Timer " + currentTime(); +} + +function currentTime() { + min = Date().getMinutes(); + if (min < 10) min = "0" + min; + return Date().getHours() + ":" + min; +} + +function timeFormated(sec) { + var min = Math.floor(sec / 60); + sec = sec % 60; + if (sec < 10) sec = "0" + sec; + return min + ":" + sec; +} + +// initialize timer and show timer value => state: start +function initTimer() { + counter = counterStart; + setState(states.start); + showCounter(true); +} + +// timer value (counter) can be changed in state start +function changeCounter(diff) { + if (state == states.start) { + if (counter + diff > 0) { + counter = counter + diff; + showCounter(true); + } + } +} + +// start or restart timer => state: count +function startTimer() { + counterStart = counter; + setState(states.count); + countDown(); + if (!counterInterval) + counterInterval = setInterval(countDown, 1000); +} + +/* show current counter value at start and while count down + Show + - Title with current time + - initial timer value + - remaining time + - hint for help in state start +*/ +function showCounter(withHint) { + //g.clear(); + E.showMessage("", appTitle()); + g.setFontAlign(0,0); // center font + // draw the current counter value + g.setBgColor(-1).setColor(0,0,1); // blue + g.setFont("Vector",20); // vector font, 20px + g.drawString("Timer: " + timeFormated(counterStart),80,55); + g.setFont("Vector",60); // vector font, 60px + g.drawString(timeFormated(counter),83,100); + if (withHint) { + g.setFont("Vector",20); // vector font, 80px + g.drawString("Tap for help",80,150); + } +} + +// count down and update every second +// when time is up, start counting up +function countDown() { + counter--; + // Out of time + if (counter<=0) { + outOfTime(); + countUp(); + counterInterval = setInterval(countUp, 1000); + return; + } + showCounter(false); +} + +// +function outOfTime() { + E.showMessage("Time is up!",appTitle()); + setState(states.countUp); + resetTimer(); + Bangle.buzz(); + Bangle.buzz(); +} + +/* this counts up (one minute), after time is up + Show + - Title with current time + - initial timer value + - "Time is up!" + - time since timer finished +*/ +function countUp() { + // buzz for 15 seconds + counter++; + if (counter <=15) { + Bangle.buzz(); + } + // stop counting up after 60 seconds + if (counter > 60) { + outOfTime(); + return; + } + g.clear(); + E.showMessage("", appTitle()); + g.setFontAlign(0,0); // center font + g.setBgColor(-1).setColor(0,0,1); // blue + g.setFont("Vector",20); // vector font, 20px + g.drawString("Timer: " + timeFormated(counterStart),80,55); + g.setFont("Vector",30); // vector font, 80px + g.setBgColor(-1).setColor(1,0,0); // red + g.drawString("Time is up!",85,85); + g.setFont("Vector",40); // vector font, 80px + // draw the current counter value + g.drawString(timeFormated(counter),80,130); +} + +// reset when interupted by user oder 60 seconds after timer finished +function resetTimer() { + clearInterval(); + counterInterval = undefined; +} + +// timer is stopped by user => state: stop +function stopTimer() { + resetTimer(); + E.showMessage("Timer stopped!", appTitle()); + setState(states.stop); +} + +// timer is stopped by user while counting up => state: start +function stopTimer2() { + resetTimer(); + initTimer(); +} + + +function setState(st) { + state = st; +} + +function buttonPressed() { + switch(state) { + case states.init: + initTimer(); + break; + case states.help: + initTimer(); + break; + case states.start: + startTimer(); + break; + case states.count: + stopTimer(); + break; + case states.countUp: + stopTimer2(); + break; + case states.stop: + initTimer(); + break; + default: + initTimer(); + break; + } +} + +/* Change initial counter value by swiping + swipe up: +1 minute + swipe down: -1 minute + swipe right: +15 seconds + swipe left: -15 seconds */ +function initDragEvents() { + Bangle.on("drag", e => { + if (state == states.start) { + if (!drag) { // start dragging + drag = {x: e.x, y: e.y}; + } else if (!e.b) { // released + const dx = e.x-drag.x, dy = e.y-drag.y; + drag = null; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + changeCounter(dx>0 ? 15 : -15); + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + changeCounter(dy>0 ? -60 : 60); + } + } + } +}); +} + +// show help text while in start state (see initDragEvents()) +function showHelp() { + if (state == states.start) { + state = states.help; + E.showMessage("Swipe up/down\n+/- one minute\n\nSwipe left/right\n+/- 15 seconds\n\nPress Btn1 to start","Tea timer help"); + } + // return to start + else if (state == states.help) { + initTimer(); + } +} + +// drag events in start state (to change counter value) +initDragEvents(); +// Show help test in start state +Bangle.on('touch', function(button, xy) { showHelp(); }); +// event handling for button1 +setWatch(buttonPressed, BTN1, {repeat: true}); +initTimer(); diff --git a/apps/teatimer/metadata.json b/apps/teatimer/metadata.json new file mode 100644 index 000000000..acace0402 --- /dev/null +++ b/apps/teatimer/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "teatimer", + "name": "Tea Timer", + "version": "0.01", + "description": "A simple timer. You can easyly set up the time.", + "icon": "teatimer.png", + "type": "app", + "tags": "tool", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"teatimer.app.js","url":"app.js"}, + {"name":"teatimer.img","url":"app-icon.js","evaluate":true} + ], + "screenshots": [ + {"url":"TeatimerStart.jpg"}, + {"url":"TeatimerHelp.jpg"}, + {"url":"TeatimerRun.jpg"}, + {"url":"TeatimerUp.jpg"} + ] +} diff --git a/apps/teatimer/teatimer.png b/apps/teatimer/teatimer.png new file mode 100644 index 000000000..29ca58f0e Binary files /dev/null and b/apps/teatimer/teatimer.png differ diff --git a/apps/testuserinput/metadata.json b/apps/testuserinput/metadata.json new file mode 100644 index 000000000..e279ae509 --- /dev/null +++ b/apps/testuserinput/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "testuserinput", + "name": "Test User Input", + "shortName": "Test User Input", + "version": "0.06", + "description": "App to test the bangle.js input interface. It displays the user action in text, circle buttons or on/off switch UI elements.", + "icon": "app.png", + "tags": "input,interface,buttons,touch,UI", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"testuserinput.app.js","url":"app.js"}, + {"name":"testuserinput.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/themesetter/LICENSE b/apps/themesetter/LICENSE new file mode 100644 index 000000000..7487dd5da --- /dev/null +++ b/apps/themesetter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andreas Rozek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/themesetter/README.md b/apps/themesetter/README.md new file mode 100644 index 000000000..518d05c01 --- /dev/null +++ b/apps/themesetter/README.md @@ -0,0 +1,22 @@ +# Theme Setter # + +This little tool allows you to configure the global theme of all Bangle.js apps +(provided that they do not override global settings) in a more comfortable way +than through the settings menu. + +![](ThemeSetter-MainScreen.png) +![](ThemeSetter-DetailSelectionScreen.png) +![](ThemeSetter-ColorSelectionScreen.png) +![](ThemeSetter-ThemePreviewScreen.png) + +This app also acts as an example for a non-trivial Bangle.js application +using the "layout" library, custom controls and generic event dispatching. +See [GitHub](https://github.com/rozek/banglejs-2-activities) for details. + +## License ## + +[MIT License](LICENSE) + +## Credits ## + +The icon for this app was taken from [icons8.com](https://icons8.com/). \ No newline at end of file diff --git a/apps/themesetter/ThemeSetter-ColorSelectionScreen.png b/apps/themesetter/ThemeSetter-ColorSelectionScreen.png new file mode 100644 index 000000000..3ed4d857e Binary files /dev/null and b/apps/themesetter/ThemeSetter-ColorSelectionScreen.png differ diff --git a/apps/themesetter/ThemeSetter-DetailSelectionScreen.png b/apps/themesetter/ThemeSetter-DetailSelectionScreen.png new file mode 100644 index 000000000..79a983652 Binary files /dev/null and b/apps/themesetter/ThemeSetter-DetailSelectionScreen.png differ diff --git a/apps/themesetter/ThemeSetter-MainScreen.png b/apps/themesetter/ThemeSetter-MainScreen.png new file mode 100644 index 000000000..3a0fc215c Binary files /dev/null and b/apps/themesetter/ThemeSetter-MainScreen.png differ diff --git a/apps/themesetter/ThemeSetter-ThemePreviewScreen.png b/apps/themesetter/ThemeSetter-ThemePreviewScreen.png new file mode 100644 index 000000000..1d21935b2 Binary files /dev/null and b/apps/themesetter/ThemeSetter-ThemePreviewScreen.png differ diff --git a/apps/themesetter/app-icon.js b/apps/themesetter/app-icon.js new file mode 100644 index 000000000..31fc30449 --- /dev/null +++ b/apps/themesetter/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/ACHgDAQWBApfjCoXxAqHwg4FP+PHApY7EApheEAq3+g4FD/EPAofAj4QDgAQECwgQQ8E/Cwg+EAvYAhA==")) \ No newline at end of file diff --git a/apps/themesetter/app-icon.png b/apps/themesetter/app-icon.png new file mode 100644 index 000000000..078920a27 Binary files /dev/null and b/apps/themesetter/app-icon.png differ diff --git a/apps/themesetter/app-screenshot.png b/apps/themesetter/app-screenshot.png new file mode 100644 index 000000000..e7bf5a6a2 Binary files /dev/null and b/apps/themesetter/app-screenshot.png differ diff --git a/apps/themesetter/app.js b/apps/themesetter/app.js new file mode 100644 index 000000000..aa3c608cf --- /dev/null +++ b/apps/themesetter/app.js @@ -0,0 +1,498 @@ + let Layout = require('Layout'); + + let ScreenWidth = g.getWidth(), halfWidth = ScreenWidth/2; + let ScreenHeight = g.getHeight(); + + let normalizedColorSet = { + black:g.toColor(0,0,0), white: g.toColor(1,1,1), + red: g.toColor(1,0,0), yellow: g.toColor(1,1,0), + green:g.toColor(0,1,0), magenta:g.toColor(1,0,1), + blue: g.toColor(0,0,1), cyan: g.toColor(0,1,1) + }; + + let activeTheme = g.theme; // currently active theme + let pendingTheme = Object.assign({},activeTheme); + let chosenDetail = null; // one of 'fg','bg','fg2','bg2','fgH','bgH' + +/**** Label ****/ + + function Label (Text, Options) { + function renderLabel (Details) { + let halfWidth = Details.w/2, xAlignment = Details.halign || 0; + let halfHeight = Details.h/2, yAlignment = Details.valign || 0; + let Padding = Details.pad || 0; + + g.setColor(Details.col || g.theme.fg || '#000000'); + + if (Details.font != null) { g.setFont(Details.font); } + g.setFontAlign(xAlignment,yAlignment); + + let x = Details.x + halfWidth + xAlignment*(halfWidth+Padding); + let y = Details.y + halfHeight + yAlignment*(halfHeight+Padding); + + g.drawString(Details.label, x,y); + if (Details.bold) { + g.drawString(Details.label, x+1,y); + g.drawString(Details.label, x,y+1); + g.drawString(Details.label, x+1,y+1); + } + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderLabel, label:Text || '' + }); + let TextMetrics; + if (! Result.width || ! Result.height) { + if (Result.font != null) { g.setFont(Result.font); } + TextMetrics = g.stringMetrics(Result.label); + } + + Result.width = Result.width || TextMetrics.width + 2*(Result.pad || 0); + Result.height = Result.height || TextMetrics.height + 2*(Result.pad || 0); + return Result; + } + + if (g.drawRoundedRect == null) { + g.drawRoundedRect = function drawRoundedRect (x1,y1, x2,y2, r) { + let x,y; + if (x1 > x2) { x = x1; x1 = x2; x2 = x; } + if (y1 > y2) { y = y1; y1 = y2; y2 = y; } + + r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2); + + let cx1 = x1+r, cx2 = x2-r; + let cy1 = y1+r, cy2 = y2-r; + + this.drawLine(cx1,y1, cx2,y1); + this.drawLine(cx1,y2, cx2,y2); + this.drawLine(x1,cy1, x1,cy2); + this.drawLine(x2,cy1, x2,cy2); + + x = r; y = 0; + + let dx,dy, Error = 0; + while (y <= x) { + dy = 1 + 2*y; y++; Error -= dy; + if (Error < 0) { + dx = 1 - 2*x; x--; Error -= dx; + } + + this.setPixel(cx1 - x, cy1 - y); this.setPixel(cx1 - y, cy1 - x); + this.setPixel(cx2 + x, cy1 - y); this.setPixel(cx2 + y, cy1 - x); + this.setPixel(cx2 + x, cy2 + y); this.setPixel(cx2 + y, cy2 + x); + this.setPixel(cx1 - x, cy2 + y); this.setPixel(cx1 - y, cy2 + x); + } + }; + } + + +/**** Button ****/ + + function Button (Text, Options) { + function renderButton (Details) { + let x = Details.x, Width = Details.w, halfWidth = Width/2; + let y = Details.y, Height = Details.h, halfHeight = Height/2; + let Padding = Details.pad || 0; + + g.setColor(Details.col || g.theme.fg || '#000000'); + + if (Details.font != null) { g.setFont(Details.font); } + g.setFontAlign(0,0); + + g.drawRoundedRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1,8); + g.drawString(Details.label, x+halfWidth,y+halfHeight); + g.drawString(Details.label, x+halfWidth+1,y+halfHeight); + g.drawString(Details.label, x+halfWidth,y+halfHeight+1); + g.drawString(Details.label, x+halfWidth+1,y+halfHeight+1); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderButton, label:Text || 'Tap' + }); + let TextMetrics; + if (! Result.width || ! Result.height) { + if (Options.font != null) { g.setFont(Options.font); } + TextMetrics = g.stringMetrics(Result.label); + } + + Result.width = Result.width || TextMetrics.width + 2*10 + 2*(Result.pad || 0); + Result.height = Result.height || TextMetrics.height + 2*5 + 2*(Result.pad || 0); + return Result; + } + +/**** ColorDemo ****/ + + function ColorDemo (Text, Options) { + function renderDemo (Details) { + let x = Details.x, Width = Details.w, halfWidth = Width/2; + let y = Details.y, Height = Details.h, halfHeight = Height/2; + let Padding = Details.pad || 0; + + if (Details.font != null) { g.setFont(Details.font); } + g.setFontAlign(0,0); + + g.setColor(Details.bg); // do not use "bgCol"! + g.fillRect(x+Padding, y+Padding, x+Width-Padding, y+Height-Padding); + + g.setColor(Details.fg); + g.drawString(Details.label, x+halfWidth,y+halfHeight); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderDemo, label:Text || 'Test' + }); + let TextMetrics; + if (! Result.width || ! Result.height) { + if (Result.font != null) { g.setFont(Result.font); } + TextMetrics = g.stringMetrics(Result.label); + } + + Result.width = Result.width || TextMetrics.width + 2*2 + 2*(Result.pad || 0); + Result.height = Result.height || TextMetrics.height + 2*2 + 2*(Result.pad || 0); + return Result; + } + + +/**** ColorView ****/ + + function ColorView (Color, Options) { + function renderColorView (Details) { + let x = Details.x, Width = Details.w; + let y = Details.y, Height = Details.h; + let Padding = Details.pad || 0; + + g.setColor('#000000'); + g.drawRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1); + + g.setColor(Details.col); + g.fillRect(x+Padding+2, y+Padding+2, x+Width-Padding-3, y+Height-Padding-3); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderColorView, col:Color + }); + Result.width = Math.max(10, Result.width || 10) + 2*(Result.pad || 0); + Result.height = Math.max(10, Result.height || 10) + 2*(Result.pad || 0); + return Result; + } + + +/**** ColorSelectionView ****/ + + function ColorSelectionView (Color, Options) { + function renderColorView (Details) { + let x = Details.x, Width = Details.w; + let y = Details.y, Height = Details.h; + let Padding = Details.pad || 0; + + if (Details.selected) { + g.setColor(Details.selected ? '#FF0000' : '#000000'); + g.fillRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1); + + g.setColor('#FFFFFF'); + g.drawRect(x+Padding+4,y+Padding+4, x+Width-Padding-5,y+Height-Padding-5); + } else { + g.setColor('#000000'); + g.drawRect(x+Padding+3,y+Padding+3, x+Width-Padding-4,y+Height-Padding-4); + } + + g.setColor(Details.col); + g.fillRect(x+Padding+5, y+Padding+5, x+Width-Padding-6, y+Height-Padding-6); + } + + let Result = Object.assign(( + Options == null ? {} : Object.assign({}, Options.common || {}, Options) + ), { + type:'custom', render:renderColorView, col:Color + }); + Result.width = Math.max(10, Result.width || 10) + 2*(Result.pad || 0); + Result.height = Math.max(10, Result.height || 10) + 2*(Result.pad || 0); + return Result; + } + + +/**** EventConsumerAtPoint ****/ + + function EventConsumerAtPoint (HandlerName, x,y) { + let Layout = (activeLayout || {}).l; + if (Layout == null) { return; } + + function ConsumerIn (Control) { + if ( + (x < Control.x) || (x >= Control.x + Control.w) || + (y < Control.y) || (y >= Control.y + Control.h) + ) { return undefined; } + + if (typeof Control[HandlerName] === 'function') { return Control; } + + if (Control.c != null) { + let ControlList = Control.c; + for (let i = 0, l = ControlList.length; i < l; i++) { + let Consumer = ConsumerIn(ControlList[i]); + if (Consumer != null) { return Consumer; } + } + } + + return undefined; + } + + return ConsumerIn(Layout); + } + +/**** dispatchTouchEvent ****/ + + function dispatchTouchEvent () { + function handleTouchEvent (Button, xy) { + let Control = EventConsumerAtPoint('onTouch', xy.x,xy.y); + if (Control != null) { + Control.onTouch(Control, Button, xy); + } + } + Bangle.on('touch',handleTouchEvent); + } + dispatchTouchEvent(); + +/**** dispatchStrokeEvent ****/ + + function dispatchStrokeEvent () { + function handleStrokeEvent (Coordinates) { + let Control = EventConsumerAtPoint('onStroke', Coordinates.xy[0],Coordinates.xy[1]); + if (Control != null) { + Control.onStroke(Control, Coordinates); + } + } + Bangle.on('stroke',handleStrokeEvent); + } + dispatchStrokeEvent(); + + let ScreenSet = {}; + + g.setFont12x20(); // does not seem to be respected in layout! + let leftColumnWidth = Math.max( + g.stringWidth('Normal '), g.stringWidth('Accented '), g.stringWidth('Hilighted ') + ); + + let StdFont = { font:'12x20' }; + let legible = Object.assign({ col:'#000000', bgCol:'#FFFFFF' }, StdFont); + let leftAligned = Object.assign({ halign:-1, valign:0 }, legible); + let MainLabel = Object.assign({ pad:4, width:leftColumnWidth }, leftAligned); + let halfWidthButton = Object.assign({ pad:4, width:halfWidth }, legible); + + ScreenSet['MainScreen'] = new Layout({ + type:'v', c:[ + Label('Current Theme', { common:legible, pad:8, bold:true, filly:1 }), + { type:'h', c:[ + Label('Normal', { common:MainLabel }), + ColorDemo(' Demo ',{ common:StdFont, pad:2, id:'NormalDemo' }), + ] }, + { type:'h', c:[ + Label('Accented', { common:MainLabel }), + ColorDemo(' Demo ',{ common:StdFont, pad:2, id:'AccentedDemo' }), + ] }, + { type:'h', c:[ + Label('Hilighted', { common:MainLabel }), + ColorDemo(' Demo ',{ common:StdFont, pad:2, id:'HilitedDemo' }), + ] }, + { height:4 }, + { type:'h', c:[ + Button('Exit', { common:halfWidthButton, onTouch:() => load() }), + Button('Config', { common:halfWidthButton, onTouch:() => gotoScreen('DetailSelectionScreen') }) + ], filly:1 } + ] + }); + + let LabelWidth = Math.max( + g.stringWidth('Fg '), g.stringWidth('Fg2 '), g.stringWidth('FgH '), + g.stringWidth('Bg '), g.stringWidth('Bg2 '), g.stringWidth('BgH ') + ); + let LabelHeight = g.stringMetrics('FgH').height; + + let DetailLabel = Object.assign({ pad:4, width:LabelWidth }, leftAligned); + let DetailView = { width:30, height:LabelHeight, pad:2 }; + + ScreenSet['DetailSelectionScreen'] = new Layout({ + type:'v', c:[ + Label('Configure Detail', { font:'12x20', pad:8, col:'#000000', bgCol:'#FFFFFF', bold:true, filly:1 }), + { type:'h', c:[ + Label('fg', { common:DetailLabel, onTouch:() => configureDetail('fg') }), + ColorView(0, { common:DetailView, onTouch:() => configureDetail('fg'), id:'fgView' }), + { width:20 }, + Label('bg', { common:DetailLabel, onTouch:() => configureDetail('bg') }), + ColorView(0, { common:DetailView, onTouch:() => configureDetail('bg'), id:'bgView' }), + ] }, + { type:'h', c:[ + Label('fg2', { common:DetailLabel, onTouch:() => configureDetail('fg2') }), + ColorView(0, { common:DetailView, onTouch:() => configureDetail('fg2'), id:'fg2View' }), + { width:20 }, + Label('bg2', { common:DetailLabel, onTouch:() => configureDetail('bg2') }), + ColorView(0, { common:DetailView, onTouch:() => configureDetail('bg2'), id:'bg2View' }), + ] }, + { type:'h', c:[ + Label('fgH', { common:DetailLabel, onTouch:() => configureDetail('fgH') }), + ColorView(0, { common:DetailView, onTouch:() => configureDetail('fgH'), id:'fgHView' }), + { width:20 }, + Label('bgH', { common:DetailLabel, onTouch:() => configureDetail('bgH') }), + ColorView(0, { common:DetailView, onTouch:() => configureDetail('bgH'), id:'bgHView' }), + ] }, + { type:'h', c:[ + Button('Save', { common:halfWidthButton, onTouch:() => { applyChanges(); gotoScreen('MainScreen'); } }), + Button('Cancel', { common:halfWidthButton, onTouch:() => gotoScreen('MainScreen') }) + ], filly:1 }, + ] + }); + + let StdSelectionView = { width:40, height:40, pad:2 }; + + ScreenSet['ColorSelectionScreen'] = new Layout({ + type:'v', c:[ + Label('Choose Color', { font:'12x20', pad:8, col:'#000000', bgCol:'#FFFFFF', bold:true, filly:1 }), + { type:'h', c:[ + ColorSelectionView('#000000',{ common:StdSelectionView, id:'black', + onTouch:() => selectColor(0,0,0) }), + ColorSelectionView('#FF0000',{ common:StdSelectionView, id:'red', + onTouch:() => selectColor(1,0,0) }), + ColorSelectionView('#00FF00',{ common:StdSelectionView, id:'green', + onTouch:() => selectColor(0,1,0) }), + ColorSelectionView('#0000FF',{ common:StdSelectionView, id:'blue', + onTouch:() => selectColor(0,0,1) }), + ] }, + { type:'h', c:[ + ColorSelectionView('#FFFFFF',{ common:StdSelectionView, id:'white', + onTouch:() => selectColor(1,1,1) }), + ColorSelectionView('#FFFF00',{ common:StdSelectionView, id:'yellow', + onTouch:() => selectColor(1,1,0) }), + ColorSelectionView('#FF00FF',{ common:StdSelectionView, id:'magenta', + onTouch:() => selectColor(1,0,1) }), + ColorSelectionView('#00FFFF',{ common:StdSelectionView, id:'cyan', + onTouch:() => selectColor(0,1,1) }), + ] }, + { height:4 }, + { type:'h', c:[ + Button('Back', { common:halfWidthButton, onTouch:() => gotoScreen('DetailSelectionScreen') }), + Button('Preview', { common:halfWidthButton, onTouch:() => gotoScreen('ThemePreviewScreen') }) + ], filly:1 }, + ] + }); + + ScreenSet['ThemePreviewScreen'] = new Layout({ + type:'v', c:[ + Label('Theme Preview', { common:legible, bold:true, filly:1 }), + { type:'h', c:[ + Label('Normal', { common:MainLabel }), + ColorDemo(' Test ',{ common:StdFont, pad:2, id:'NormalTest' }), + ] }, + { type:'h', c:[ + Label('Accented', { common:MainLabel }), + ColorDemo(' Test ',{ common:StdFont, pad:2, id:'AccentedTest' }), + ] }, + { type:'h', c:[ + Label('Hilighted', { common:MainLabel }), + ColorDemo(' Test ',{ common:StdFont, pad:2, id:'HilitedTest' }), + ] }, + { height:4 }, + { type:'h', c:[ + Button('Back', { common:legible, pad:4, onTouch:() => gotoScreen('ColorSelectionScreen') }) + ], filly:1 } + ] + }); + + +/**** applyChanges ****/ + + function applyChanges () { + let pendingBg = pendingTheme.bg; + let R = ((pendingBg >> 11) & 0b11111) / 0b11111; + let G = ((pendingBg >> 5) & 0b111111) / 0b111111; + let B = (pendingBg & 0b11111) / 0b11111; + pendingTheme.dark = (0.2126*R + 0.7152*G + 0.0722*B < 0.5); + + activeTheme = Object.assign(activeTheme,pendingTheme); + + let globalSettings = Object.assign( + require('Storage').readJSON('setting.json', true) || {}, + { theme:activeTheme } + ); + require('Storage').writeJSON('setting.json', globalSettings); + } + +/**** configureDetail ****/ + + function configureDetail (Detail) { + chosenDetail = Detail; + gotoScreen('ColorSelectionScreen'); + } + +/**** updateColorSelection ****/ + + function updateColorSelection () { + let selectedColor = pendingTheme[chosenDetail]; + + for (let Key in normalizedColorSet) { + if (normalizedColorSet.hasOwnProperty(Key)) { + activeLayout[Key].selected = (selectedColor === normalizedColorSet[Key]); + } + } + } + +/**** selectColor ****/ + + function selectColor (R,G,B) { + let selectedColor = g.toColor(R,G,B); + pendingTheme[chosenDetail] = selectedColor; + + updateColorSelection(); + g.clear(); + activeLayout.render(); + } + +/**** gotoScreen ****/ + + let activeLayout; + + function gotoScreen (ScreenName) { + activeLayout = ScreenSet[ScreenName]; + + switch (ScreenName) { + case 'MainScreen': + activeLayout['NormalDemo'].fg = activeTheme.fg; + activeLayout['NormalDemo'].bg = activeTheme.bg; + activeLayout['AccentedDemo'].fg = activeTheme.fg2; + activeLayout['AccentedDemo'].bg = activeTheme.bg2; + activeLayout['HilitedDemo'].fg = activeTheme.fgH; + activeLayout['HilitedDemo'].bg = activeTheme.bgH; + break; + case 'DetailSelectionScreen': + activeLayout['fgView'].col = pendingTheme.fg; + activeLayout['bgView'].col = pendingTheme.bg; + activeLayout['fg2View'].col = pendingTheme.fg2; + activeLayout['bg2View'].col = pendingTheme.bg2; + activeLayout['fgHView'].col = pendingTheme.fgH; + activeLayout['bgHView'].col = pendingTheme.bgH; + break; + case 'ColorSelectionScreen': + updateColorSelection(); + break; + case 'ThemePreviewScreen': + activeLayout['NormalTest'].fg = pendingTheme.fg; + activeLayout['NormalTest'].bg = pendingTheme.bg; + activeLayout['AccentedTest'].fg = pendingTheme.fg2; + activeLayout['AccentedTest'].bg = pendingTheme.bg2; + activeLayout['HilitedTest'].fg = pendingTheme.fgH; + activeLayout['HilitedTest'].bg = pendingTheme.bgH; + } + + g.setColor('#000000'); g.setBgColor('#FFFFFF'); // assert legibility + g.clear(); + + activeLayout.render(); + } + gotoScreen('MainScreen'); + diff --git a/apps/themesetter/metadata.json b/apps/themesetter/metadata.json new file mode 100644 index 000000000..f6a319ec0 --- /dev/null +++ b/apps/themesetter/metadata.json @@ -0,0 +1,17 @@ +{ "id": "themesetter", + "name": "Theme Setter", + "shortName":"Theme Setter", + "version":"0.04", + "description": "a comfortable way to configure theme colors", + "icon": "app-icon.png", + "type": "app", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"app-screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"themesetter.app.js","url":"app.js"}, + {"name":"themesetter.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/thermom/ChangeLog b/apps/thermom/ChangeLog index 6ab6ba8e5..6d3a966e3 100644 --- a/apps/thermom/ChangeLog +++ b/apps/thermom/ChangeLog @@ -1,2 +1,6 @@ 0.02: New App! 0.03: Improved messages and added Celsius sign +0.04: Make temperature value readable on smaller screens +0.05: Use temperature from current locale + Update every 10s, average last 5 readings + Changes based on #1092 diff --git a/apps/thermom/app.js b/apps/thermom/app.js index 7eae9b3d4..0e45ed3e7 100644 --- a/apps/thermom/app.js +++ b/apps/thermom/app.js @@ -1,13 +1,27 @@ +// history of temperature readings +var history = []; + + +// When we get temperature... function onTemperature(p) { - g.reset(1).clearRect(0,24,g.getWidth(),g.getHeight()); + var rect = Bangle.appRect; + g.reset(1).clearRect(rect.x, rect.y, rect.x2, rect.y2); g.setFont("6x8",2).setFontAlign(0,0); - var x = g.getWidth()/2; - var y = g.getHeight()/2 + 10; + var x = (rect.x+rect.x2)/2; + var y = (rect.y+rect.y2)/2 + 10; g.drawString("Temperature:", x, y - 45); - g.setFontVector(70).setFontAlign(0,0); - g.drawString(p.temperature.toFixed(1) + " °C", x, y); + g.setFontVector(g.getWidth() > 200 ? 70 : 50).setFontAlign(0,0); + + // Average the last 5 temperature readings + while (history.length>4) history.shift(); + history.push(p.temperature); + var avrTemp = E.sum(history) / history.length; + // Draw the temperature + var t = require('locale').temp(avrTemp).replace("'","°"); + g.drawString(t, x, y); } +// Gets the temperature in the most accurate way (pressure sensor or inbuilt thermistor) function drawTemperature() { if (Bangle.getPressure) { Bangle.getPressure().then(onTemperature); @@ -18,11 +32,10 @@ function drawTemperature() { } } - setInterval(function() { drawTemperature(); -}, 20000); -drawTemperature(); +}, 10000); E.showMessage("Reading temperature..."); +drawTemperature(); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/thermom/metadata.json b/apps/thermom/metadata.json new file mode 100644 index 000000000..381f85e17 --- /dev/null +++ b/apps/thermom/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "thermom", + "name": "Thermometer", + "version": "0.05", + "description": "Displays the current temperature in degree Celsius/Fahrenheit (depending on locale), updates every 10 seconds with average of last 5 readings.", + "icon": "app.png", + "tags": "tool", + "supports": ["BANGLEJS", "BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"thermom.app.js","url":"app.js"}, + {"name":"thermom.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/thermom/screenshot.png b/apps/thermom/screenshot.png new file mode 100644 index 000000000..a12bbef1c Binary files /dev/null and b/apps/thermom/screenshot.png differ diff --git a/apps/thermomF/metadata.json b/apps/thermomF/metadata.json new file mode 100644 index 000000000..ff821d780 --- /dev/null +++ b/apps/thermomF/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "thermomF", + "name": "Fahrenheit Temp", + "version": "0.01", + "description": "[NOT RECOMMENDED] A modification of the Thermometer App to display temprature in Fahrenheit. Please use the 'Thermometer App' and install 'Languages' to get the temperature in the correct format for your locale.", + "icon": "thermf.png", + "tags": "tool", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"thermomF.app.js","url":"app.js"}, + {"name":"thermomF.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/tilthydro/metadata.json b/apps/tilthydro/metadata.json new file mode 100644 index 000000000..38fe599e7 --- /dev/null +++ b/apps/tilthydro/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "tilthydro", + "name": "Tilt Hydrometer Display", + "shortName": "Tilt Hydro", + "version": "0.01", + "description": "A display for the [Tilt Hydrometer](https://tilthydrometer.com/) - [more info here](http://www.espruino.com/Tilt+Hydrometer+Display)", + "icon": "app.png", + "tags": "tools,bluetooth", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"tilthydro.app.js","url":"app.js"}, + {"name":"tilthydro.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/timeandlife/ChangeLog b/apps/timeandlife/ChangeLog new file mode 100644 index 000000000..115067b80 --- /dev/null +++ b/apps/timeandlife/ChangeLog @@ -0,0 +1 @@ +0.01: New app diff --git a/apps/timeandlife/README.md b/apps/timeandlife/README.md new file mode 100644 index 000000000..4a638c952 --- /dev/null +++ b/apps/timeandlife/README.md @@ -0,0 +1,5 @@ +# Time and Life + +A simple watchface which displays the time when the screen is tapped and decays according to the rules of [Conway's game of life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). + +![](screenshot.png) diff --git a/apps/timeandlife/app-icon.js b/apps/timeandlife/app-icon.js new file mode 100644 index 000000000..d7608fca4 --- /dev/null +++ b/apps/timeandlife/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkB/4AGCY4PHC/4X/C/4X/C/4XvJ/4X/C/4X/C/4X3AH4A/AH4A/AH4A/")) diff --git a/apps/timeandlife/app.js b/apps/timeandlife/app.js new file mode 100644 index 000000000..4fe758815 --- /dev/null +++ b/apps/timeandlife/app.js @@ -0,0 +1,225 @@ +// Globals +const X = 176, + Y = 176; // screen resolution of bangle 2 +const STEP_TIMEOUT = 1000; +const PAUSE_TIME = 3000; + +const ONE = [ + [0, 1, 0], + [1, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [1, 1, 1], +]; +const TWO = [ + [0, 1, 0], + [1, 0, 1], + [0, 0, 1], + [0, 1, 0], + [1, 0, 0], + [1, 0, 0], + [1, 1, 1], +]; +const THREE = [ + [0, 1, 0], + [1, 0, 1], + [0, 0, 1], + [0, 1, 0], + [0, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const FOUR = [ + [0, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 1, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], +]; +const FIVE = [ + [1, 1, 1], + [1, 0, 0], + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const SIX = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 0], + [1, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const SEVEN = [ + [1, 1, 1], + [1, 0, 1], + [0, 0, 1], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], +]; +const EIGHT = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const NINE = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [0, 1, 1], + [0, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const ZERO = [ + [0, 1, 0], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + [0, 1, 0], +]; +const NUMBERS = [ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE]; + +// Arraybuffers to store game state +// 484 8 bit integers that are either 1 or 0 form the 22 x 22 grid +let data = new Uint8Array(484); +let nextData = new Uint8Array(484); + +let palette = new Uint16Array(256); // palette for rendering data +palette[0] = g.theme.bg; +palette[1] = g.theme.fg; + +let lastPaused = new Date(); + +// Conway's game of life +// if < 2 neighbours, set off +// if 2 or 3 neighbours, set on +// if > 3 neighbours, set off +/*const updateStateC = E.compiledC(` +// void run(int, int) +void run(char* n, char* m){ + // n is a pointer to the first byte in data, m is for nextdata + int count = 0; + for (int i=0;i<484;i++) { + // Add 8 neighbours, wrapping around + count = + *(n+(i+484-23)%484) + + *(n+(i+484-22)%484) + + *(n+(i+484-21)%484) + + *(n+(i+484-1)%484) + + *(n+(i+484+1)%484) + + *(n+(i+484+21)%484) + + *(n+(i+484+22)%484) + + *(n+(i+484+23)%484); + if (count < 2 || count > 3) { + *(m+i) = 0; + } else { + *(m+i) = 1; + } + } +} +`);*/ +// precompiled - taken from file downloaded from Bangle.js storage after +// Web IDE upload +const updateStateC=function(a){return a=atob('ACLwtU/08nMBJxZGAvLNFQL1536V+/P0A/sUVJ778/UD+xXlAvLPHhD4BMBEXZ778/UD+xXlZERFXQLy4x4sRJ778/UD+xXlAvLlHkVdLESe+/P1A/sV5QLy+R5FXSxEnvvz9QP7FeUC9f1+RV0sRJ778/UD+xXlAvL7HkVdLESe+/P1A/sV5UVdLEQCPAIsNL88RjRGjFQBMrL18n+10fC9AAA='),{run:E.nativeCall(1,'void(int, int)',a)}}(); + +function draw() { + g.drawImage({ + width:22, height:22, bpp: 8, + palette : palette, // ideally we'd just have BPP 1 and would render direct but it makes the code tricky + buffer : data.buffer, + },0,0,{scale:8}); +} + +const step = () => { + if (new Date() - lastPaused < PAUSE_TIME) { + return; + } + let startTime = new Date(); + const dataAddr = E.getAddressOf(data, true); + const nextDataAddr = E.getAddressOf(nextData, true); + updateStateC.run(dataAddr, nextDataAddr); + draw(); + data.set(nextData); +}; + +const setPixel = (i, j) => { + data[i * 22 + j] = 1; + nextData[i * 22 + j] = 1; +}; + +const setNum = (character, i, j) => { + const startJ = j; + character.forEach(row => { + j = startJ; + row.forEach(pixel => { + if (pixel) setPixel(i, j); + j++; + }); + i++; + }); +}; + +const setDots = () => { + setPixel(10, 10); + setPixel(12, 10); +}; + +const drawTime = () => { + lastPaused = new Date(); + g.clear(); + data.fill(0); + const d = new Date(); + const hourTens = Math.floor(d.getHours() / 10); + const hourOnes = d.getHours() % 10; + const minuteTens = Math.floor(d.getMinutes() / 10); + const minuteOnes = d.getMinutes() % 10; + setNum(NUMBERS[hourTens], 8, 1); + setNum(NUMBERS[hourOnes], 8, 6); + setDots(); + setNum(NUMBERS[minuteTens], 8, 13); + setNum(NUMBERS[minuteOnes], 8, 18); + draw(); +}; + +const start = () => { + Bangle.setUI("clock"); // Show launcher when middle button pressed + g.clear(); + Bangle.setLCDTimeout(20); // backlight/lock timeout in seconds + let stepInterval = setInterval(step, STEP_TIMEOUT); + + // Handlers + Bangle.on('touch', drawTime); + + // Sleep mode + Bangle.on('lock', isLocked => { + if (stepInterval) { + clearInterval(stepInterval); + } + stepInterval = undefined; + if (!isLocked) { + drawTime(); + stepInterval = setInterval(step, STEP_TIMEOUT); + } + }); + + drawTime(); +}; + +start(); diff --git a/apps/timeandlife/app.png b/apps/timeandlife/app.png new file mode 100644 index 000000000..b1e837d25 Binary files /dev/null and b/apps/timeandlife/app.png differ diff --git a/apps/timeandlife/metadata.json b/apps/timeandlife/metadata.json new file mode 100644 index 000000000..86800f16f --- /dev/null +++ b/apps/timeandlife/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "timeandlife", + "name": "Time and Life", + "shortName":"Time and Lfie", + "icon": "app.png", + "version":"0.01", + "description": "A simple watchface which displays the time when the screen is tapped and decay according to the rules of Conway's game of life.", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"timeandlife.app.js","url":"app.js"}, + {"name":"timeandlife.img","url":"app-icon.js","evaluate":true} + ] +} + diff --git a/apps/timeandlife/screenshot.png b/apps/timeandlife/screenshot.png new file mode 100644 index 000000000..3058c9346 Binary files /dev/null and b/apps/timeandlife/screenshot.png differ diff --git a/apps/timecal/metadata.json b/apps/timecal/metadata.json new file mode 100644 index 000000000..3237dd08a --- /dev/null +++ b/apps/timecal/metadata.json @@ -0,0 +1,13 @@ +{ "id": "timecal", + "name": "TimeCal", + "shortName":"TimeCal", + "icon": "icon.png", + "version":"0.01", + "description": "TimeCal shows the Time along with a 3 week calendar", + "tags": "clock", + "type": "clock", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"timecal.app.js","url":"timecal.app.js"} + ] +} diff --git a/apps/timerclk/ChangeLog b/apps/timerclk/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/timerclk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/timerclk/README.md b/apps/timerclk/README.md new file mode 100644 index 000000000..fd6d2b16b --- /dev/null +++ b/apps/timerclk/README.md @@ -0,0 +1,72 @@ +# Timer Clock + +A clock based on the Anton Clock with stopwatches, timers and alarms based on the Stopwatch Touch style and an alarm widget based on the one from Default alarm & timer. + +## Features + +* two slots for stopwatches / timers on the clock screen +* configurable font and size (Anton font has fixed size) +* stopwatch with modifiable start value +* timer that can be paused +* alarms +* multiple stopwatches, timers and alarms +* stopwatches and timers keep running in the background + +## Images + +![](screenshot.png) + +### Stopwatch + +![](screenshot_stopwatch1.png) + +![](screenshot_stopwatch2.png) + +### Settings + +![](screenshot_settings1.png) + +![](screenshot_settings2.png) + +![](screenshot_settings3.png) + +## Controls + +### Bangle.js 1 + +#### Clock + +* Left: Stopwatch +* Right: Timer +* Button 1 / 2: Alarm + +#### Stopwatch / Timer / Alarm + +* Button 1: + * edit mode: increase + * control mode: play / pause +* Button 2: switch between edit / control mode +* Button 3: + * edit mode: decrease + * control mode: reset / remove +* Left: + * edit mode: previous index + * control mode: previous stopwatch / timer / alarm +* Right: + * edit mode: next index + * control mode: next stopwatch / timer / alarm + +### Bangle.js 2 + +#### Clock + +* Swipe left: Stopwatch +* Swipe right: Timer +* Swipe over date: Alarm + +#### Stopwatch / Timer / Alarm + +* Swipe left: previous stopwatch / timer / alarm +* Swipe right: next stopwatch / timer / alarm +* Swipe up: increase index swiped over +* Swipe down: decrease index swiped over \ No newline at end of file diff --git a/apps/timerclk/alarm.alert.js b/apps/timerclk/alarm.alert.js new file mode 100644 index 000000000..f4b61822a --- /dev/null +++ b/apps/timerclk/alarm.alert.js @@ -0,0 +1,57 @@ +if (timerclkAlarmTimeout) clearInterval(timerclkAlarmTimeout); +var timerclk = require("timerclk.lib.js"); +var settings = require('Storage').readJSON("timerclk.json", true) || {}; +settings = Object.assign({ + "vibrate":10 +}, settings.alarm||{}); + +function showAlarm(alarm) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + Bangle.setLocked(false); + E.showPrompt("Alarm!",{ + title:"ALARM!", + buttons : {/*LANG*/"Ok":true} + }).then(function(ok) { + buzzCount = 0; + if (ok) { + alarm.last = new Date().getDate(); + } + require("Storage").write("timerclk.alarm.json",JSON.stringify(alarms)); + load(); + }); + function vibrate(counter) { + VIBRATE.write(1); + setTimeout(() => VIBRATE.write(0), 100); + if (--counter) setTimeout(() => vibrate(counter), 250); + } + function buzz() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence + vibrate(4); + if (buzzCount--) + setTimeout(buzz, 3000); + else { // auto-snooze + buzzCount = settings.vibrate; + setTimeout(buzz, 600000); + } + } + var buzzCount = settings.vibrate; + buzz(); +} + +// Check for alarms +console.log("checking for alarms..."); +var alarms = require("Storage").readJSON("timerclk.alarm.json",1)||[]; +var active = alarms.filter(e=>e.on); +if (active.length) { + // if there's an alarm, show it + active = active.sort((a,b)=>(a.time-b.time)+(a.last-b.last)*86400000); + if (active[0].last != new Date().getDate()) { + showAlarm(active[0]); + } else { + setTimeout(load, 100); + } +} else { + // otherwise just go back to default app + setTimeout(load, 100); +} diff --git a/apps/timerclk/alarm.info b/apps/timerclk/alarm.info new file mode 100644 index 000000000..1289f8cef --- /dev/null +++ b/apps/timerclk/alarm.info @@ -0,0 +1 @@ +{"id":"timerclk","name":"tclk Alarm","src":"timerclk.alarm.js","icon":"timerclk.img","version":"0.01","tags":"","files":"","sortorder":10} diff --git a/apps/timerclk/alarm.js b/apps/timerclk/alarm.js new file mode 100644 index 000000000..4acaa6cf0 --- /dev/null +++ b/apps/timerclk/alarm.js @@ -0,0 +1,116 @@ +var timerclk = require("timerclk.lib.js"); +const height = g.getHeight(), width = g.getWidth(); + +var all = require("Storage").readJSON("timerclk.alarm.json") || []; +var settings = require('Storage').readJSON("timerclk.json", true) || {}; +settings = Object.assign({ + "font":"Vector", + "fontSize":40, + "indexFont":"6x8", + "indexFontSize":3, + "buttonHeight":40, + "vibrate":4, +}, settings = settings.alarm||{}); +var defaultElement = {time:43200000, on:true, last:null}; + +var current = 0; +var editIndex = 0; +var drawInterval; +var drawIntervalTimeout; +var buttons; +var dragBorderHrsMins=0, dragBorderMinsSecs=0; + +function update() { + if (drawInterval) clearInterval(drawInterval); + if (drawIntervalTimeout) clearTimeout(drawIntervalTimeout); + if (all[current].start) { + drawIntervalTimeout = setTimeout(() => {drawInterval = setInterval(draw, 1000); draw();}, 1000 - (timerclk.getTime(all[current]) % 1000)); + } else { + drawInterval = null; + drawIntervalTimeout = null; + } + draw(); + drawButtons(); +} +function activate() { + all[current].on = !all[current].on; + all[current].last = null; + update(); + require("Storage").write("timerclk.alarm.json",JSON.stringify(all)); + timerclkCheckAlarms(); +} +function remove() { + all.splice(current, 1); + if (current == all.length) current--; + if (all.length == 0) { + all.push(defaultElement.clone()); + current++; + } + update(); + require("Storage").write("timerclk.alarm.json",JSON.stringify(all)); + timerclkCheckAlarms(); +} + +function edit(position, change) { + if (position == 1) all[current].time += change*1000; + else if (position == 2) all[current].time += change*60000; + else if (position == 3) all[current].time += change*3600000; + require("Storage").write("timerclk.alarm.json",JSON.stringify(all)); + timerclkCheckAlarms(); +} + +var buttons = { + reset: {pos:[0, height-settings.buttonHeight, width/2, height], callback: remove, img: timerclk.remove_img, col:"#f50"}, // remove + play: {pos:[width/2, height-settings.buttonHeight, width, height], callback: activate, img: timerclk.play_img, col:"#0ff"}, // active +}; + + +function drawButtons() { + if (all[current].on) { + buttons.play.img = timerclk.pause_img; + } else { + buttons.play.img = timerclk.play_img; + } + for (var button of buttons) { + g.setColor(button.col); + g.fillRect(button.pos[0], button.pos[1], button.pos[2], button.pos[3]); + g.setColor("#000"); + // scale 24px images + let iw = settings.buttonHeight-10; + var scale = iw/24; + let ix = button.pos[0] + ((button.pos[2]-button.pos[0] - iw) /2); + let iy = button.pos[1] + ((button.pos[3]-button.pos[1] - iw) /2); + g.drawImage(button.img, ix, iy, {scale: scale}); + } +} + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + + g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-settings.buttonHeight); + g.setFontAlign(0,0).setFont(settings.indexFont, settings.indexFontSize); + g.drawString(current+1, x, Bangle.appRect.y + (g.stringMetrics("0").height/2)); + g.setFontAlign(0,0).setFont(settings.font, settings.fontSize); + var timeStr = timerclk.formatTime(all[current].time, false, false, true); + g.drawString(timeStr,x,y); + var start = (width-g.stringMetrics(timeStr).width)/2; + timeStr = timeStr.split(":"); + var markerPosChange = g.stringMetrics("__").width/2; + if (editIndex == 3) x = start + g.stringMetrics(timeStr[0]).width - markerPosChange; + else if (editIndex == 2) x = start + g.stringMetrics(timeStr[0]+":"+timeStr[1]).width - markerPosChange; + else if (editIndex == 1) x = start + g.stringMetrics(timeStr[0]+":"+timeStr[1]+":"+timeStr[2]).width - markerPosChange; + else x = 0; + if (x) g.drawString("__", x, y); + dragBorderHrsMins = start+g.stringMetrics(timeStr[0]).width+g.stringMetrics(":").width/2; + dragBorderMinsSecs = start+g.stringMetrics(timeStr[0]+":"+timeStr[1]).width+g.stringMetrics(":").width/2; +} + +if (all.length == 0) { + all.push(defaultElement.clone()); +} +timerclk.registerControls(this); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +update(); diff --git a/apps/timerclk/app-icon.js b/apps/timerclk/app-icon.js new file mode 100644 index 000000000..278cf4bb6 --- /dev/null +++ b/apps/timerclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AFHzvmf+f8z/8tnv/vs9/1t/v+kv94jR/H4n/wn4CBAYPwnEP8AFDg/AAoUwAoPgmABBwfQAonwAo0/4gFC4AFE4gFLmGEAoQDBxgFCwEQAIIFIj4FD/k//hNBAoZZBAoc8j6oS8/P+1NAoP63+7+wMCz/u/YEB/v/v4dI1+pAQIFBx/J/2/AoP5tFJr71eA==")) diff --git a/apps/timerclk/app-icon.png b/apps/timerclk/app-icon.png new file mode 100644 index 000000000..074db4ed7 Binary files /dev/null and b/apps/timerclk/app-icon.png differ diff --git a/apps/timerclk/app.js b/apps/timerclk/app.js new file mode 100644 index 000000000..eeb3ac4cd --- /dev/null +++ b/apps/timerclk/app.js @@ -0,0 +1,151 @@ +Graphics.prototype.setFontAnton = function(scale) { +// Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78+(scale<<8)+(1<<16)); +}; + +var timerclk = require("timerclk.lib.js"); +var settings = require('Storage').readJSON("timerclk.json", true) || {}; +settings = Object.assign({ + "timeFont":"Anton", + "timeFontSize":0, + "dateFont":"6x8", + "dateFontSize":2, + "dowFont":"6x8", + "dowFontSize":2, + "specialFont":"6x8", + "specialFontSize":2, + "shortDate":true, + "showStopwatches":true, + "showTimers":true, +}, settings.clock||{}); + +var stopwatches = [], timers = []; +if (settings.showStopwatches) { + stopwatches = require("Storage").readJSON("timerclk.stopwatch.json") || []; + stopwatches = stopwatches.filter(e=>e.start||e.time); +} +if (settings.showTimers) { + timers = require("Storage").readJSON("timerclk.timer.json") || []; + timers = timers.filter(e=>e.start||e.timeAdd); +} + +// timeout used to update every minute +var drawTimeout; +var drawSpecialTimeout; +// border between time and date/dow +var dragBorder = g.getHeight()/2; + +// schedule a draw for the next minute +function queueDraw(timeout, interval, func) { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(function() { + timeout = undefined; + func(); + }, interval - (Date.now() % interval)); +} + +function drawSpecial() { + var interval = 60000; + var stopwatch = 0, timer = 0, time; + var x = g.getWidth()/4; + g.setColor(g.theme.fg); + g.setFontAlign(0,0).setFont(settings.specialFont, settings.specialFontSize); + var y = Bangle.appRect.y + g.stringMetrics("00:00").height/2; + g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y+g.stringMetrics("00:00").height); + + if (stopwatches.length) { + time = timerclk.getTime(stopwatches[stopwatch]); + g.drawString(timerclk.formatTime(time, true), x, y); + if (Math.floor(time/3600000) === 0) interval = 1000; + stopwatch++; + } else if (timers.length > 1) { + time = timers[timer].time - timerclk.getTime(timers[timer]); + g.drawString(timerclk.formatTime(time, true), x, y); + if (Math.floor(time/3600000) === 0) interval = 1000; + timer++; + } + x += g.getWidth()/2; + if (timers.length) { + time = timers[timer].time - timerclk.getTime(timers[timer]); + g.drawString(timerclk.formatTime(time, true), x, y); + if (Math.floor(time/3600000) === 0) interval = 1000; + } else if (stopwatches.length > 1) { + time = timerclk.getTime(stopwatches[stopwatch]); + g.drawString(timerclk.formatTime(time, true), x, y); + if (Math.floor(time/3600000) === 0) interval = 1000; + } + queueDraw(drawSpecialTimeout, interval, drawSpecial); +} + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date,settings.shortDate).toUpperCase(); + var dowStr = require("locale").dow(date).toUpperCase(); + + // draw time + if (settings.timeFont == "Anton") { + g.setFontAlign(0,0).setFont("Anton"); + } else { + g.setFontAlign(0,0).setFont(settings.timeFont, settings.timeFontSize); + } + g.clearRect(Bangle.appRect.x, x-g.stringMetrics(timeStr).height/2, Bangle.appRect.x2, Bangle.appRect.y2); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += g.stringMetrics(timeStr).height/2; + g.setFontAlign(0,0).setFont(settings.dateFont, settings.dateFontSize); + dragBorder = y; + y += g.stringMetrics(dateStr).height/2; + g.drawString(dateStr,x,y); + //draw day of week + y += g.stringMetrics(dateStr).height/2; + g.setFontAlign(0,0).setFont(settings.dowFont, settings.dowFontSize); + y += g.stringMetrics(dowStr).height/2; + g.drawString(dowStr,x,y); + // queue draw in one minute + queueDraw(drawTimeout, 60000, draw); +} + +if (process.env.HWVERSION==1) { + setWatch(()=>load("timerclk.stopwatch.js"), BTN4); + setWatch(()=>load("timerclk.timer.js"), BTN5); + setWatch(()=>load("timerclk.alarm.js"), BTN3); + setWatch(()=>load("timerclk.alarm.js"), BTN1); +} else { + var absY, lastX, lastY; + Bangle.on('drag', e=>{ + if (!e.b) { + if (lastX > 50) { // right + if (absY < dragBorder) { // drag over time + load("timerclk.timer.js"); + }else { // drag over date/dow + load("timerclk.alarm.js"); + } + } else if (lastX < -50) { // left + if (absY < dragBorder) { // drag over time + load("timerclk.stopwatch.js"); + }else { // drag over date/dow + load("timerclk.alarm.js"); + } + } else if (lastY > 50) { // down + } else if (lastY < -50) { // up + } + lastX = 0; + lastY = 0; + } else { + lastX = lastX + e.dx; + lastY = lastY + e.dy; + absY = e.y; + } + }); +} + +Bangle.setUI("clock"); // Show launcher when middle button pressed +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); +if (stopwatches || timers) drawSpecial(); diff --git a/apps/timerclk/boot.js b/apps/timerclk/boot.js new file mode 100644 index 000000000..9a09f68f3 --- /dev/null +++ b/apps/timerclk/boot.js @@ -0,0 +1,48 @@ +var timerclkTimerTimeout; +var timerclkAlarmTimeout; +function timerclkCheckTimers() { + if (timerclkTimerTimeout) clearTimeout(timerclkTimerTimeout); + var timers = require('Storage').readJSON('timerclk.timer.json',1)||[]; + timers = timers.filter(e=>e.start); + if (timers.length) { + timers = timers.sort((a,b)=>{ + var at = a.timeAdd; + if (a.start) at += Date.now()-a.start; + at = a.period-at; + var bt = b.timeAdd; + if (b.start) bt += Date.now()-b.start; + bt = b.period-bt; + return at-bt; + }); + if (!require('Storage').read("timerclk.timer.alert.js")) { + console.log("No timer app!"); + } else { + var time = timers[0].timeAdd; + if (timers[0].start) time += Date.now()-timers[0].start; + time = timers[0].time - time; + if (time<1000) t=1000; + if (timerclkTimerTimeout) clearTimeout(timerclkTimerTimeout); + timerclkTimerTimeout = setTimeout(() => load("timerclk.timer.alert.js"),time); + } + } +} +function timerclkCheckAlarms() { + if (timerclkAlarmTimeout) clearTimeout(timerclkAlarmTimeout); + var alarms = require('Storage').readJSON('timerclk.alarm.json',1)||[]; + var currentTime = require("timerclk.lib.js").getCurrentTime(); + alarms = alarms.filter(e=>e.on); + if (alarms.length) { + alarms = alarms.sort((a,b)=>(a.time-b.time)+(a.last-b.last)*86400000); + if (!require('Storage').read("timerclk.alarm.alert.js")) { + console.log("No alarm app!"); + } else { + var time = alarms[0].time-currentTime; + if (alarms[0].last == new Date().getDate() || time < 0) time += 86400000; + if (time<1000) t=1000; + if (timerclkAlarmTimeout) clearTimeout(timerclkAlarmTimeout); + timerclkAlarmTimeout = setTimeout(() => load("timerclk.alarm.alert.js"),time); + } + } +} +timerclkCheckTimers(); +timerclkCheckAlarms(); diff --git a/apps/timerclk/lib.js b/apps/timerclk/lib.js new file mode 100644 index 000000000..718962fe0 --- /dev/null +++ b/apps/timerclk/lib.js @@ -0,0 +1,127 @@ +exports.pause_img = atob("GBiBAf///////////+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B+D/B////////////w=="); +exports.play_img = atob("GBiBAf////////////P///D///A///Af//AH//AB//AAf/AAH/AAB/AAB/AAH/AAf/AB//AH//Af//A///D///P//////////////w=="); +exports.reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w=="); +exports.remove_img = atob("GBiBAf///////////+P/x+H/h+D/B/B+D/g8H/wYP/4Af/8A//+B//+B//8A//4Af/wYP/g8H/B+D+D/B+H/h+P/x////////////w=="); + +exports.formatTime = function(t, short, tnthEnable, fullTime) { + var negative = ""; + if (t < 0) { + t = t*(-1); + negative = "-"; + } + let hrs = Math.floor(t/3600000); + let mins = Math.floor(t/60000)%60; + let secs = Math.floor(t/1000)%60; + var tnth = ""; + if (tnthEnable) { + tnth = Math.floor(t/100)%10; + tnth = "."+tnth; + } + var hrsStr = hrs; + if (hrs < 10 && !negative) hrsStr = "0"+hrs; + var text; + if (short) { + if (hrs === 0) text = negative + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + else text = negative + hrsStr + "/" + ("0"+mins).substr(-2); + } else { + if (hrs === 0 && !fullTime) text = negative + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + tnth; + else text = negative + hrsStr + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + } + return text; +}; + +exports.getTime = function(e) { + var time = e.timeAdd; + if (e.start) { + time += Date.now() - e.start; + } + return time; +}; + +exports.getCurrentTime = function() { + var date = new Date(); + return date.getHours()*3600000+date.getMinutes()*60000+date.getSeconds()*1000+date.getMilliseconds(); +}; + +exports.registerControls = function(o) { + if (process.env.HWVERSION==1) { + setWatch(()=>{ + if (o.editIndex == 0) o.buttons.play.callback(); + else o.edit(o.editIndex, 1); + o.draw(); + }, BTN1, {repeat:true}); + setWatch(()=>{ + o.editIndex = !o.editIndex; + o.draw(); + }, BTN2, {repeat:true}); + setWatch(()=>{ + if (o.editIndex == 0) o.buttons.reset.callback(); + else o.edit(o.editIndex, -1); + o.draw(); + }, BTN3, {repeat:true}); + setWatch(()=>{ + if (o.editIndex) { + o.editIndex++; + if (o.editIndex > 3) o.editIndex = 1; + } else if (o.current > 0) o.current--; + o.update(); + }, BTN4, {repeat:true}); + setWatch(()=>{ + if (o.editIndex) { + o.editIndex--; + if (o.editIndex < 1) o.editIndex = 3; + } else { + o.current++; + if (o.current == o.all.length) o.all.push(o.defaultElement.clone()); + } + o.update(); + }, BTN5, {repeat:true}); + } else { + setWatch(()=>load(), BTN1); + Bangle.on('touch',(n,e)=>{ + for (var button of o.buttons) { + if (e.x>=button.pos[0] && e.y>=button.pos[1] && + e.x{ + if (!e.b) { + if (lastX > 40) { // right + o.current++; + if (o.current == o.all.length) o.all.push(o.defaultElement.clone()); + } else if (lastX < -40) { // left + if (o.current > 0) { + o.current--; + } + } else if (lastY > 30) { // down + if (absX < o.dragBorderHrsMins) { + o.edit(3, -1); + } else if (absX > o.dragBorderHrsMins && absX < o.dragBorderMinsSecs) { + o.edit(2, -1); + } else { + o.edit(1, -1); + } + } else if (lastY < -30) { // up + if (absX < o.dragBorderHrsMins) { + o.edit(3, 1); + } else if (absX > o.dragBorderHrsMins && absX < o.dragBorderMinsSecs) { + o.edit(2, 1); + } else { + o.edit(1, 1); + } + } + lastX = 0; + lastY = 0; + o.update(); + } else { + absX = e.x; + lastX = lastX + e.dx; + lastY = lastY + e.dy; + } + }); + } +}; diff --git a/apps/timerclk/metadata.json b/apps/timerclk/metadata.json new file mode 100644 index 000000000..6b415c0fc --- /dev/null +++ b/apps/timerclk/metadata.json @@ -0,0 +1,38 @@ +{ + "id": "timerclk", + "name": "Timer Clock", + "shortName":"Timer Clock", + "version":"0.01", + "description": "A clock with stopwatches, timers and alarms build in.", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS","BANGLEJS2"], + "screenshots": [ + {"url":"screenshot.png"}, + {"url":"screenshot_stopwatch1.png"}, + {"url":"screenshot_stopwatch2.png"}, + {"url":"screenshot_settings1.png"}, + {"url":"screenshot_settings2.png"}, + {"url":"screenshot_settings3.png"} + ], + "readme": "README.md", + "storage": [ + {"name":"timerclk.app.js","url":"app.js"}, + {"name":"timerclk.img","url":"app-icon.js","evaluate":true}, + {"name":"timerclk.boot.js","url":"boot.js"}, + {"name":"timerclk.lib.js","url":"lib.js"}, + {"name":"timerclk.wid.js","url":"wid.js"}, + {"name":"timerclk.settings.js","url":"settings.js"}, + {"name":"timerclk.stopwatch.js","url":"stopwatch.js"}, + {"name":"timerclk.timer.js","url":"timer.js"}, + {"name":"timerclk.timer.alert.js","url":"timer.alert.js"}, + {"name":"timerclk.alarm.js","url":"alarm.js"}, + {"name":"timerclk.alarm.alert.js","url":"alarm.alert.js"}, + {"name":"timerclk.stopwatch.info","url":"stopwatch.info"}, + {"name":"timerclk.timer.info","url":"timer.info"}, + {"name":"timerclk.alarm.info","url":"alarm.info"} + ], + "data": [{"name":"timerclk.json"},{"name":"timerclk.stopwatch.json"},{"name":"timerclk.timer.json"},{"name":"timerclk.alarm.json"}], + "sortorder": 0 +} diff --git a/apps/timerclk/pause-24.png b/apps/timerclk/pause-24.png new file mode 100644 index 000000000..7ff72e906 Binary files /dev/null and b/apps/timerclk/pause-24.png differ diff --git a/apps/timerclk/play-24.png b/apps/timerclk/play-24.png new file mode 100644 index 000000000..26fa8d99c Binary files /dev/null and b/apps/timerclk/play-24.png differ diff --git a/apps/timerclk/remove-24.png b/apps/timerclk/remove-24.png new file mode 100644 index 000000000..b59505bcb Binary files /dev/null and b/apps/timerclk/remove-24.png differ diff --git a/apps/timerclk/reset-24.png b/apps/timerclk/reset-24.png new file mode 100644 index 000000000..73fb28dec Binary files /dev/null and b/apps/timerclk/reset-24.png differ diff --git a/apps/timerclk/screenshot.png b/apps/timerclk/screenshot.png new file mode 100644 index 000000000..1bccf6807 Binary files /dev/null and b/apps/timerclk/screenshot.png differ diff --git a/apps/timerclk/screenshot_settings1.png b/apps/timerclk/screenshot_settings1.png new file mode 100644 index 000000000..da187e496 Binary files /dev/null and b/apps/timerclk/screenshot_settings1.png differ diff --git a/apps/timerclk/screenshot_settings2.png b/apps/timerclk/screenshot_settings2.png new file mode 100644 index 000000000..4b12848d0 Binary files /dev/null and b/apps/timerclk/screenshot_settings2.png differ diff --git a/apps/timerclk/screenshot_settings3.png b/apps/timerclk/screenshot_settings3.png new file mode 100644 index 000000000..b1ef2381f Binary files /dev/null and b/apps/timerclk/screenshot_settings3.png differ diff --git a/apps/timerclk/screenshot_stopwatch1.png b/apps/timerclk/screenshot_stopwatch1.png new file mode 100644 index 000000000..f50d7a1d1 Binary files /dev/null and b/apps/timerclk/screenshot_stopwatch1.png differ diff --git a/apps/timerclk/screenshot_stopwatch2.png b/apps/timerclk/screenshot_stopwatch2.png new file mode 100644 index 000000000..89f91ae1b Binary files /dev/null and b/apps/timerclk/screenshot_stopwatch2.png differ diff --git a/apps/timerclk/settings.js b/apps/timerclk/settings.js new file mode 100644 index 000000000..556dded98 --- /dev/null +++ b/apps/timerclk/settings.js @@ -0,0 +1,292 @@ +(function(back) { + const FILE = "timerclk.json"; + const BOOL_FORMAT = v=>v?/*LANG*/"On":/*LANG*/"Off"; + // Load settings + var settings = require('Storage').readJSON(FILE, true) || {} + settings.clock = Object.assign({ + "timeFont":"Anton", + "timeFontSize":0, + "dateFont":"6x8", + "dateFontSize":2, + "dowFont":"6x8", + "dowFontSize":2, + "specialFont":"6x8", + "specialFontSize":2, + "shortDate":true, + "showStopwatches":true, + "showTimers":true, + }, settings.clock||{}); + settings.stopwatch = Object.assign({ + "font":"Vector", + "fontSize":40, + "indexFont":"6x8", + "indexFontSize":3, + "buttonHeight":40, + }, settings.stopwatch||{}); + settings.timer = Object.assign({ + "font":"Vector", + "fontSize":40, + "indexFont":"6x8", + "indexFontSize":3, + "buttonHeight":40, + "vibrate":10, + }, settings.timer||{}); + settings.alarm = Object.assign({ + "font":"Vector", + "fontSize":40, + "indexFont":"6x8", + "indexFontSize":3, + "buttonHeight":40, + "vibrate":10, + }, settings.alarm||{}); + var timeFonts = ["Anton"].concat(g.getFonts()); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + var mainMenu = { + "" : { "title" : "Timer Clock" }, + "< Back" : () => back(), + "Clock": ()=>{E.showMenu(clockMenu);}, + "Stopwatch": ()=>{E.showMenu(stopwatchMenu);}, + "Timer": ()=>{E.showMenu(timerMenu);}, + "Alarm": ()=>{E.showMenu(alarmMenu);}, + }; + var clockMenu = { + "" : { "title" : "Clock" }, + "< Back" : () => E.showMenu(mainMenu), + "time font":{ + value: 0|timeFonts.indexOf(settings.clock.timeFont), + format: v => timeFonts[v], + min: 0, max: timeFonts.length-1, + onchange: v => { + settings.clock.timeFont = timeFonts[v]; + writeSettings(); + } + }, + "time size":{ + value: 0|settings.clock.timeFontSize, + min: 0, + onchange: v => { + settings.clock.timeFontSize = v; + writeSettings(); + } + }, + "date font":{ + value: 0|g.getFonts().indexOf(settings.clock.dateFont), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.clock.dateFont = g.getFonts()[v]; + writeSettings(); + } + }, + "date size":{ + value: 0|settings.clock.dateFontSize, + min: 0, + onchange: v => { + settings.clock.dateFontSize = v; + writeSettings(); + } + }, + "dow font":{ + value: 0|g.getFonts().indexOf(settings.clock.dowFont), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.clock.dowFont = g.getFonts()[v]; + writeSettings(); + } + }, + "dow size":{ + value: 0|settings.clock.dowFontSize, + min: 0, + onchange: v => { + settings.clock.dowFontSize = v; + writeSettings(); + } + }, + "short date": { + value: !!settings.clock.shortDate, + format: BOOL_FORMAT, + onchange: v => { + settings.clock.shortDate = v; + writeSettings(); + } + }, + "stopwatches": { + value: !!settings.clock.showStopwatches, + format: v=>v?/*LANG*/"Show":/*LANG*/"Hide", + onchange: v => { + settings.clock.showStopwatches = v; + writeSettings(); + } + }, + "timers": { + value: !!settings.clock.showTimers, + format: v=>v?/*LANG*/"Show":/*LANG*/"Hide", + onchange: v => { + settings.clock.showTimers = v; + writeSettings(); + } + }, + }; + + var stopwatchMenu = { + "" : { "title" : "Stopwatch" }, + "< Back" : () => E.showMenu(mainMenu), + "font":{ + value: 0|g.getFonts().indexOf(settings.stopwatch.font), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.settings.stopwatch.font = g.getFonts()[v]; + writeSettings(); + } + }, + "fontsize":{ + value: 0|settings.stopwatch.fontSize, + min: 0, + onchange: v => { + settings.stopwatch.fontSize = v; + writeSettings(); + } + }, + "index font":{ + value: 0|g.getFonts().indexOf(settings.stopwatch.indexFont), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.settings.stopwatch.indexFont = g.getFonts()[v]; + writeSettings(); + } + }, + "index size":{ + value: 0|settings.stopwatch.indexFontSize, + min: 0, + onchange: v => { + settings.stopwatch.indexFontSize = v; + writeSettings(); + } + }, + "button height":{ + value: 0|settings.stopwatch.buttonHeight, + min: 0, + onchange: v => { + settings.stopwatch.buttonHeight = v; + writeSettings(); + } + }, + }; + var timerMenu = { + "" : { "title" : "Timer" }, + "< Back" : () => E.showMenu(mainMenu), + "font":{ + value: 0|g.getFonts().indexOf(settings.timer.font), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.settings.timer.font = g.getFonts()[v]; + writeSettings(); + } + }, + "fontsize":{ + value: 0|settings.timer.fontSize, + min: 0, + onchange: v => { + settings.timer.fontSize = v; + writeSettings(); + } + }, + "index font":{ + value: 0|g.getFonts().indexOf(settings.timer.indexFont), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.settings.timer.indexFont = g.getFonts()[v]; + writeSettings(); + } + }, + "index size":{ + value: 0|settings.timer.indexFontSize, + min: 0, + onchange: v => { + settings.timer.indexFontSize = v; + writeSettings(); + } + }, + "button height":{ + value: 0|settings.timer.buttonHeight, + min: 0, + onchange: v => { + settings.timer.buttonHeight = v; + writeSettings(); + } + }, + "vibrate":{ + value: 0|settings.timer.vibrate, + min: 0, + onchange: v=>{ + settings.timer.vibrate = v; + writeSettings(); + } + } + }; + var alarmMenu = { + "" : { "title" : "Alarm" }, + "< Back" : () => E.showMenu(mainMenu), + "font":{ + value: 0|g.getFonts().indexOf(settings.alarm.font), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.settings.alarm.font = g.getFonts()[v]; + writeSettings(); + } + }, + "fontsize":{ + value: 0|settings.alarm.fontSize, + min: 0, + onchange: v => { + settings.alarm.fontSize = v; + writeSettings(); + } + }, + "index font":{ + value: 0|g.getFonts().indexOf(settings.alarm.indexFont), + format: v => g.getFonts()[v], + min: 0, max: g.getFonts().length-1, + onchange: v => { + settings.settings.alarm.indexFont = g.getFonts()[v]; + writeSettings(); + } + }, + "index size":{ + value: 0|settings.alarm.indexFontSize, + min: 0, + onchange: v => { + settings.alarm.indexFontSize = v; + writeSettings(); + } + }, + "button height":{ + value: 0|settings.alarm.buttonHeight, + min: 0, + onchange: v => { + settings.alarm.buttonHeight = v; + writeSettings(); + } + }, + "vibrate":{ + value: 0|settings.alarm.vibrate, + min: 0, + onchange: v=>{ + settings.alarm.vibrate = v; + writeSettings(); + } + } + }; + E.showMenu(mainMenu); +}); diff --git a/apps/timerclk/stopwatch.info b/apps/timerclk/stopwatch.info new file mode 100644 index 000000000..72ad418b1 --- /dev/null +++ b/apps/timerclk/stopwatch.info @@ -0,0 +1 @@ +{"id":"timerclk","name":"tclk Stopwatch","src":"timerclk.stopwatch.js","icon":"timerclk.img","version":"0.01","tags":"","files":"","sortorder":10} diff --git a/apps/timerclk/stopwatch.js b/apps/timerclk/stopwatch.js new file mode 100644 index 000000000..8ac6d30a7 --- /dev/null +++ b/apps/timerclk/stopwatch.js @@ -0,0 +1,135 @@ +var timerclk = require("timerclk.lib.js"); +const height = g.getHeight(), width = g.getWidth(); + +var all = require("Storage").readJSON("timerclk.stopwatch.json") || []; + +var settings = require('Storage').readJSON("timerclk.json", true) || {}; +settings = Object.assign({ + "font":"Vector", + "fontSize":40, + "indexFont":"6x8", + "indexFontSize":3, + "buttonHeight":40, +}, settings.stopwatch||{}); +var defaultElement = {start:null, timeAdd:0}; +var current = 0; +var editIndex = 0; +var drawInterval; +var drawIntervalTimeout; +var buttons; + +function update() { + if (drawInterval) clearInterval(drawInterval); + if (drawIntervalTimeout) clearTimeout(drawIntervalTimeout); + var interval = Math.floor(timerclk.getTime(all[current])/3600000)?1000:100; + if (all[current].start) { + drawIntervalTimeout = setTimeout(() => {drawInterval = setInterval(draw, interval); draw();}, interval - (timerclk.getTime(all[current]) % interval)); + } else { + drawInterval = null; + drawIntervalTimeout = null; + } + draw(); + drawButtons(); +} +function play() { + if (all[current].start) { // running + all[current].timeAdd += Date.now() - all[current].start; + all[current].start = null; + update(); + } else { // paused + all[current].start = Date.now(); + update(); + } + require("Storage").write("timerclk.stopwatch.json",JSON.stringify(all)); +} +function reset() { + all[current] = defaultElement.clone(); + update(); + require("Storage").write("timerclk.stopwatch.json",JSON.stringify(all)); +} +function remove() { + all.splice(current, 1); + if (current == all.length) current--; + if (all.length == 0) { + all.push(defaultElement.clone()); + current++; + } + update(); + require("Storage").write("timerclk.stopwatch.json",JSON.stringify(all)); +} + +function edit(position, change) { + if (position == 1) all[current].timeAdd += change*1000; + else if (position == 2) all[current].timeAdd += change*60000; + else if (position == 3) all[current].timeAdd += change*3600000; + require("Storage").write("timerclk.stopwatch.json",JSON.stringify(all)); +} + + +var buttonsRunning = { + reset: {pos:[0, height-settings.buttonHeight, width/2, height], callback: reset, img: timerclk.reset_img, col:"#f50"}, + play: {pos:[width/2, height-settings.buttonHeight, width, height], callback: play, img: timerclk.play_img, col:"#0ff"}, +}; +var buttonsNormal = { + reset: {pos:[0, height-settings.buttonHeight, width/2, height], callback: remove, img: timerclk.remove_img, col:buttonsRunning.reset.col}, + play: {pos:[width/2, height-settings.buttonHeight, width, height], callback: play, img: timerclk.play_img, col:buttonsRunning.play.col}, +}; +buttons = buttonsNormal; + +function drawButtons() { + if (all[current].start || all[current].time) { + buttons = buttonsRunning; + if (all[current].start) { + buttons.play.img = timerclk.pause_img; + } else { + buttons.play.img = timerclk.play_img; + } + } else { + buttons = buttonsNormal; + } + for (var button of buttons) { + g.setColor(button.col); + g.fillRect(button.pos[0], button.pos[1], button.pos[2], button.pos[3]); + g.setColor("#000"); + // scale 24px images + let iw = settings.buttonHeight-10; + var scale = iw/24; + let ix = button.pos[0] + ((button.pos[2]-button.pos[0] - iw) /2); + let iy = button.pos[1] + ((button.pos[3]-button.pos[1] - iw) /2); + g.drawImage(button.img, ix, iy, {scale: scale}); + } +} + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + + var timeStr = timerclk.formatTime(timerclk.getTime(all[current]), false, true); + g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-settings.buttonHeight); + g.setFontAlign(0,0).setFont(settings.indexFont, settings.indexFontSize); + g.drawString(current+1, x, Bangle.appRect.y + (g.stringMetrics("0").height/2)); + g.setFontAlign(0,0).setFont(settings.font, settings.fontSize); + g.drawString(timeStr,x,y); + + var start = (width-g.stringMetrics(timeStr).width)/2; + timeStr = timeStr.split(".")[0].split(":"); + if (timeStr.length < 3) timeStr = [""].concat(timeStr); + var markerPosChange = g.stringMetrics("__").width/2; + if (editIndex == 3) x = start + g.stringMetrics(timeStr[0]).width - markerPosChange; + else if (editIndex == 2) x = start + g.stringMetrics(timeStr[0]+":"+timeStr[1]).width - markerPosChange; + else if (editIndex == 1) x = start + g.stringMetrics(timeStr[0]+":"+timeStr[1]+":"+timeStr[2]).width - markerPosChange; + else x = 0; + if (x) g.drawString("__", x, y); + dragBorderHrsMins = start+g.stringMetrics(timeStr[0]).width+g.stringMetrics(":").width/2; + dragBorderMinsSecs = start+g.stringMetrics(timeStr[0]+":"+timeStr[1]).width+g.stringMetrics(":").width/2; +} + +if (all.length == 0) { + all.push(defaultElement.clone()); +} +timerclk.registerControls(this); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +update(); diff --git a/apps/timerclk/timer.alert.js b/apps/timerclk/timer.alert.js new file mode 100644 index 000000000..f51ea6767 --- /dev/null +++ b/apps/timerclk/timer.alert.js @@ -0,0 +1,62 @@ +if (timerclkTimerTimeout) clearInterval(timerclkTimerTimeout); +var timerclk = require("timerclk.lib.js"); +var settings = require('Storage').readJSON("timerclk.json", true) || {}; +settings = Object.assign({ + "vibrate":10 +}, settings.timer||{}); + +function showTimer(timer) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + Bangle.setLocked(false); + E.showPrompt("Timer finished!",{ + title:"TIMER!", + buttons : {/*LANG*/"Ok":true} + }).then(function(ok) { + buzzCount = 0; + if (ok) { + timer.time += Date.now() - timer.start; + timer.start = null; + } + require("Storage").write("timerclk.timer.json",JSON.stringify(timers)); + load(); + }); + function vibrate(counter) { + VIBRATE.write(1); + setTimeout(() => VIBRATE.write(0), 100); + if (--counter) setTimeout(() => vibrate(counter), 250); + } + function buzz() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence + vibrate(4); + if (buzzCount--) + setTimeout(buzz, 3000); + else { // auto-snooze + buzzCount = settings.vibrate; + setTimeout(buzz, 600000); + } + } + var buzzCount = settings.vibrate; + buzz(); +} + +// Check for timers +console.log("checking for timers..."); +var timers = require("Storage").readJSON("timerclk.timer.json",1)||[]; +var active = timers.filter(e=>e.start); +if (active.length) { + // if there's an timer, show it + active = active.sort((a,b)=>{ + var at = a.time; + if (a.start) at += Date.now()-a.start; + at = a.period-at; + var bt = b.time; + if (b.start) bt += Date.now()-b.start; + bt = b.period-bt; + return at-bt; + }); + showTimer(active[0]); +} else { + // otherwise just go back to default app + setTimeout(load, 100); +} diff --git a/apps/timerclk/timer.info b/apps/timerclk/timer.info new file mode 100644 index 000000000..39a338693 --- /dev/null +++ b/apps/timerclk/timer.info @@ -0,0 +1 @@ +{"id":"timerclk","name":"tclk Timer","src":"timerclk.timer.js","icon":"timerclk.img","version":"0.01","tags":"","files":"","sortorder":10} diff --git a/apps/timerclk/timer.js b/apps/timerclk/timer.js new file mode 100644 index 000000000..060c07813 --- /dev/null +++ b/apps/timerclk/timer.js @@ -0,0 +1,139 @@ +var timerclk = require("timerclk.lib.js"); +const height = g.getHeight(), width = g.getWidth(); + +var all = require("Storage").readJSON("timerclk.timer.json") || []; +var settings = require('Storage').readJSON("timerclk.json", true) || {}; +settings = Object.assign({ + "font":"Vector", + "fontSize":40, + "indexFont":"6x8", + "indexFontSize":3, + "buttonHeight":40, + "vibrate":4, +}, settings = settings.timer||{}); +var defaultElement = {time:300000, start:null, timeAdd:0}; + +var current = 0; +var editIndex = 0; +var drawInterval; +var drawIntervalTimeout; +var buttons; +var dragBorderHrsMins=0, dragBorderMinsSecs=0; + +function update() { + if (drawInterval) clearInterval(drawInterval); + if (drawIntervalTimeout) clearTimeout(drawIntervalTimeout); + if (all[current].start) { + drawIntervalTimeout = setTimeout(() => {drawInterval = setInterval(draw, 1000); draw();}, 1000 - (timerclk.getTime(all[current]) % 1000)); + } else { + drawInterval = null; + drawIntervalTimeout = null; + } + draw(); + drawButtons(); +} +function play() { + if (all[current].start) { // running + all[current].timeAdd += Date.now() - all[current].start; + all[current].start = null; + update(); + } else { // paused + all[current].start = Date.now(); + update(); + } + require("Storage").write("timerclk.timer.json",JSON.stringify(all)); + timerclkCheckTimers(); +} +function reset() { + all[current] = defaultElement.clone(); + update(); + require("Storage").write("timerclk.timer.json",JSON.stringify(all)); + timerclkCheckTimers(); +} +function remove() { + all.splice(current, 1); + if (current == all.length) current--; + if (all.length == 0) { + all.push(defaultElement.clone()); + current++; + } + update(); + require("Storage").write("timerclk.timer.json",JSON.stringify(all)); + timerclkCheckTimers(); +} + +function edit(position, change) { + if (position == 1) all[current].time += change*1000; + else if (position == 2) all[current].time += change*60000; + else if (position == 3) all[current].time += change*3600000; + require("Storage").write("timerclk.timer.json",JSON.stringify(all)); + timerclkCheckTimers(); +} + +var buttonsRunning = { + reset: {pos:[0, height-settings.buttonHeight, width/2, height], callback: reset, img: timerclk.reset_img, col:"#f50"}, + play: {pos:[width/2, height-settings.buttonHeight, width, height], callback: play, img: timerclk.play_img, col:"#0ff"}, +}; +var buttonsNormal = { + reset: {pos:[0, height-settings.buttonHeight, width/2, height], callback: remove, img: timerclk.remove_img, col:buttonsRunning.reset.col}, + play: {pos:[width/2, height-settings.buttonHeight, width, height], callback: play, img: timerclk.play_img, col:buttonsRunning.play.col}, +}; +buttons = buttonsNormal; + + +function drawButtons() { + if (all[current].start || all[current].timeAdd) { + buttons = buttonsRunning; + if (all[current].start) { + buttons.play.img = timerclk.pause_img; + } else { + buttons.play.img = timerclk.play_img; + } + } else { + buttons = buttonsNormal; + } + for (var button of buttons) { + g.setColor(button.col); + g.fillRect(button.pos[0], button.pos[1], button.pos[2], button.pos[3]); + g.setColor("#000"); + // scale 24px images + let iw = settings.buttonHeight-10; + var scale = iw/24; + let ix = button.pos[0] + ((button.pos[2]-button.pos[0] - iw) /2); + let iy = button.pos[1] + ((button.pos[3]-button.pos[1] - iw) /2); + g.drawImage(button.img, ix, iy, {scale: scale}); + } +} + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + + var time = all[current].time - timerclk.getTime(all[current]); + g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-settings.buttonHeight); + g.setFontAlign(0,0).setFont(settings.indexFont, settings.indexFontSize); + g.drawString(current+1, x, Bangle.appRect.y + (g.stringMetrics("0").height/2)); + g.setFontAlign(0,0).setFont(settings.font, settings.fontSize); + var timeStr = timerclk.formatTime(time, false, false, true); + g.drawString(timeStr,x,y); + + var start = (width-g.stringMetrics(timeStr).width)/2; + timeStr = timeStr.split(":"); + var markerPosChange = g.stringMetrics("__").width/2; + if (editIndex == 3) x = start + g.stringMetrics(timeStr[0]).width - markerPosChange; + else if (editIndex == 2) x = start + g.stringMetrics(timeStr[0]+":"+timeStr[1]).width - markerPosChange; + else if (editIndex == 1) x = start + g.stringMetrics(timeStr[0]+":"+timeStr[1]+":"+timeStr[2]).width - markerPosChange; + else x = 0; + if (x) g.drawString("__", x, y); + dragBorderHrsMins = start+g.stringMetrics(timeStr[0]).width+g.stringMetrics(":").width/2; + dragBorderMinsSecs = start+g.stringMetrics(timeStr[0]+":"+timeStr[1]).width+g.stringMetrics(":").width/2; +} + +if (all.length == 0) { + all.push(defaultElement.clone()); +} +timerclk.registerControls(this); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +update(); diff --git a/apps/timerclk/wid.js b/apps/timerclk/wid.js new file mode 100644 index 000000000..e3ddeb791 --- /dev/null +++ b/apps/timerclk/wid.js @@ -0,0 +1,7 @@ +WIDGETS["timerclk.alarm"]={area:"tl",width:0,draw:function() { + if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); + },reload:function() { + WIDGETS["timerclk.alarm"].width = (require('Storage').readJSON('timerclk.alarm.json',1)||[]).some(alarm=>alarm.on) ? 24 : 0; + } +}; +WIDGETS["timerclk.alarm"].reload(); diff --git a/apps/tinydraw/ChangeLog b/apps/tinydraw/ChangeLog new file mode 100644 index 000000000..2ee16e6b5 --- /dev/null +++ b/apps/tinydraw/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial release +0.02: Don't start drawing with white colour on white canvas diff --git a/apps/tinydraw/README.md b/apps/tinydraw/README.md new file mode 100644 index 000000000..a4acd9a72 --- /dev/null +++ b/apps/tinydraw/README.md @@ -0,0 +1,14 @@ +TinyDraw +======== + +This is a simple drawing application to make sketches +using different brushes and colors for your BangleJS2 watch! + +* Brush types: dot, brush, circle, square + +It is my first BangleJS application, I plan +to continue improving this app over time, but +if you want to contribute or provide feedback +don't hesitate to contact me! + +--pancake diff --git a/apps/tinydraw/app-icon.js b/apps/tinydraw/app-icon.js new file mode 100644 index 000000000..1f0eaae27 --- /dev/null +++ b/apps/tinydraw/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/ACEF7vd6oXTroXB7tQC6QWC7vQC6Xf//9C6n4xGPC4VM5nMoAXNxH/xH96EECwPM4gXNx///AXBhgXC5gXRqAXOt3u91gC4S/BC5sGCwPu8wXCuAXOhwXC9wXcR5oXJX5oXHxGIuF3DYQXRDIWHC5SPH/7yBAIN3u6/QC4JME+AXIg5WCC44CBJRN4KwQXHAgOAC5BWCC6gOCC6xUBC6cIUIQbCAAwXJvAMBQ4QXRg6fEC6SQBLQQXRJAYAJC5UIFpIXMMAQXUVIYXVGBQXLh4XKw4XKgCpCAA34F5apK/FwC5ZIJxAWLSJP4LxhhJxBGMMIeIJQX4xH3Cxz0CxAACu4WQAH4A/AAwA==")) diff --git a/apps/tinydraw/app.js b/apps/tinydraw/app.js new file mode 100644 index 000000000..b0b3ef15b --- /dev/null +++ b/apps/tinydraw/app.js @@ -0,0 +1,159 @@ +(function () { + var pen = 'circle'; + var discard = null; + var kule = [0, 255, 255]; // R, G, B + var oldLock = false; + + setInterval(() => { + if (Bangle.isLocked()) { + if (oldLock) { + return; + } + g.setColor('#fff'); + g.fillRect(0, 0, g.getWidth(), 20); + g.setFont('6x8', 2); + g.setColor('#000'); + g.drawString('PLEASE UNLOCK', 10, 2); + oldLock = true; + } else { + oldLock = false; + drawUtil(); + } + }, 1000); + + function nextColor () { + kule[0] = Math.random(); + kule[1] = Math.random(); + kule[2] = Math.random(); + } + + function nextPen () { + switch (pen) { + case 'circle': pen = 'pixel'; break; + case 'pixel': pen = 'crayon'; break; + case 'crayon': pen = 'square'; break; + case 'square': pen = 'circle'; break; + default: pen = 'pixel'; break; + } + console.log('set time'); + drawUtil(); + + discard = setTimeout(function () { console.log('timeout'); discard = null; }, 500); + } + + function drawUtil () { + if (Bangle.isLocked()) { + // do something to tell the user to unlock the screen + } + // titlebar + g.setColor(kule[0], kule[1], kule[2]); + g.fillRect(0, 0, g.getWidth(), 20); + // clear button + g.setColor('#000'); // black + g.fillCircle(10, 10, 8, 8); + g.setColor('#fff'); + g.drawLine(8, 8, 13, 13); + g.drawLine(13, 8, 8, 13); + // tool button + g.setColor('#fff'); + g.fillCircle(g.getWidth() - 10, 10, 8); + g.setColor('#000'); + + var w = g.getWidth(); + switch (pen) { + case 'circle': + g.fillCircle(w - 10, 10, 5); + break; + case 'square': + g.fillRect(w - 5, 5, w - 15, 15); + break; + case 'pixel': + g.setPixel(10, 10); + g.fillCircle(w - 10, 10, 2); + break; + case 'crayon': + var tap = { x: 10, y: 15, dy: -5, dx: 5 }; + g.drawLine(w - tap.x, tap.y, w - tap.x + tap.dx, tap.y + tap.dy); + g.drawLine(w - tap.x + 1, tap.y + 2, w - tap.x + tap.dx, tap.y + tap.dy - 2); + g.drawLine(w - tap.x + 2, tap.y + 2, w - tap.x + tap.dx, tap.y + tap.dy + 2); + break; + } + } + var tapTimer = null; + Bangle.on('drag', function (tap) { + if (tap.b === 0) { + if (tapTimer !== null) { + clearTimeout(tapTimer); + tapTimer = null; + } + } + // tap and hold the clear button + if (tap.x < 32 && tap.y < 32) { + if (tap.b === 1) { + if (tapTimer === null) { + tapTimer = setTimeout(function () { + g.clear(); + drawUtil(); + tapTimer = null; + }, 800); + } + if (discard) { + clearTimeout(discard); discard = null; + return; + } + } + return; + } + if (tap.x > g.getWidth() - 32 && tap.y < 32) { + if (tap.b === 1) { + if (tapTimer === null) { + tapTimer = setTimeout(function () { + g.clear(); + drawUtil(); + tapTimer = null; + }, 800); + } + if (discard) { + clearTimeout(discard); + discard = null; + return; + } + nextPen(); + } + drawUtil(); + return; + } else if (tap.y < 32) { + nextColor(); + drawUtil(); + return; + } + + g.setColor(kule[0], kule[1], kule[2]); + + switch (pen) { + case 'pixel': + g.setPixel(tap.x, tap.y); + g.drawLine(tap.x, tap.y, tap.x + tap.dx, tap.y + tap.dy); + break; + case 'crayon': + g.drawLine(tap.x, tap.y, tap.x + tap.dx, tap.y + tap.dy); + g.drawLine(tap.x + 1, tap.y + 2, tap.x + tap.dx, tap.y + tap.dy - 2); + g.drawLine(tap.x + 2, tap.y + 2, tap.x + tap.dx, tap.y + tap.dy + 2); + break; + case 'circle': + var XS = tap.dx / 10; + var YS = tap.dy / 10; + for (i = 0; i < 10; i++) { + g.fillCircle(tap.x + (i * XS), tap.y + (i * YS), 4, 4); + } + break; + case 'square': + g.fillRect(tap.x - 10, tap.y - 10, tap.x + 10, tap.y + 10); + break; + } + drawUtil(); + }); + + g.clear(); + drawUtil(); +})(); diff --git a/apps/tinydraw/app.png b/apps/tinydraw/app.png new file mode 100644 index 000000000..01eda0a60 Binary files /dev/null and b/apps/tinydraw/app.png differ diff --git a/apps/tinydraw/metadata.json b/apps/tinydraw/metadata.json new file mode 100644 index 000000000..357fcc1d0 --- /dev/null +++ b/apps/tinydraw/metadata.json @@ -0,0 +1,19 @@ +{ "id": "tinydraw", + "name": "TinyDraw", + "shortName":"TinyDraw", + "version":"0.02", + "type": "app", + "description": "Draw stuff in your wrist", + "icon": "app.png", + "allow_emulator": true, + "tags": "tools, keyboard, text, scribble", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"tinydraw.app.js","url":"app.js"}, + {"name":"tinydraw.img","url":"app-icon.js","evaluate":true} + ], + "screenshots":[ + { "url":"screenshot.png" } + ] +} diff --git a/apps/tinydraw/screenshot.png b/apps/tinydraw/screenshot.png new file mode 100644 index 000000000..27d5cc56c Binary files /dev/null and b/apps/tinydraw/screenshot.png differ diff --git a/apps/torch/metadata.json b/apps/torch/metadata.json new file mode 100644 index 000000000..39655dbba --- /dev/null +++ b/apps/torch/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "torch", + "name": "Torch", + "shortName": "Torch", + "version": "0.02", + "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets", + "icon": "app.png", + "tags": "tool,torch", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"torch.app.js","url":"app.js"}, + {"name":"torch.wid.js","url":"widget.js"}, + {"name":"torch.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/toucher/app.js b/apps/toucher/app.js index 8ac198f52..aab50fbda 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -293,9 +293,9 @@ Bangle.on('swipe', dir => { else next(); }); -// close launcher when lcd is off -Bangle.on('lcdPower', on => { - if(!on) return load(); +// close launcher when screen is locked +Bangle.on('lock', on => { + if(on) return load(); }); if (process.env.HWVERSION == 1) { diff --git a/apps/toucher/metadata.json b/apps/toucher/metadata.json new file mode 100644 index 000000000..8b2715f0c --- /dev/null +++ b/apps/toucher/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "toucher", + "name": "Touch Launcher", + "shortName": "Toucher", + "version": "0.07", + "description": "Touch enable left to right launcher.", + "icon": "app.png", + "type": "launch", + "tags": "tool,system,launcher", + "screenshots": [{"url":"screenshot1.jpg"}], + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"toucher.app.js","url":"app.js"}, + {"name":"toucher.settings.js","url":"settings.js"} + ], + "data": [{"name":"toucher.json"}] +} diff --git a/apps/touchmenu/ChangeLog b/apps/touchmenu/ChangeLog new file mode 100644 index 000000000..c5d90cb57 --- /dev/null +++ b/apps/touchmenu/ChangeLog @@ -0,0 +1,2 @@ +0.01: App launched +0.02: Menu uses the correct screen area and properly resets when closed diff --git a/apps/touchmenu/README.md b/apps/touchmenu/README.md new file mode 100644 index 000000000..0e81f3755 --- /dev/null +++ b/apps/touchmenu/README.md @@ -0,0 +1,40 @@ +# TouchMenu + +A redesign of the built-in `E.showMenu()` to take advantage of the full touch screen on the Bangle.js 2. + +![screenshot](touchmenu.gif) + +## Features + +- All of the features of the built-in `E.showMenu()` +- Icon support for menu items: + ```javascript + menu.items[0].icon = Graphics.createImage(...); + ``` +- Custom accent colors: + ```javascript + E.showMenu({ + "": { + cAB: g.theme.bg2, // Accent background + cAF: g.theme.fg2 // Accent foreground + } + }) + ``` +- Automatic back button detection - name a button `< Back` and it will be given a special position and icon + +## Controls + +- Scroll through the options +- Tap on an option to select it +- Tap on a button again to use it +- Tap on a selected Boolean to toggle it +- Tap on a selected number to change - tap the right side of the screen to decrease, left side to increase +- If detected, tap on the back button in the upper left to go back + +## Requests + +Contact information is on my website: [kyleplo](https://kyleplo.com) + +## Creator + +[kyleplo](https://kyleplo.com) diff --git a/apps/touchmenu/metadata.json b/apps/touchmenu/metadata.json new file mode 100644 index 000000000..825989d99 --- /dev/null +++ b/apps/touchmenu/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "touchmenu", + "name": "TouchMenu", + "version": "0.02", + "description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2", + "screenshots": [{"url":"touchmenu.gif"}], + "icon": "touchmenu.png", + "type": "bootloader", + "tags": "tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"touchmenu.boot.js","url":"touchmenu.boot.js"} + ] +} diff --git a/apps/touchmenu/touchmenu.boot.js b/apps/touchmenu/touchmenu.boot.js new file mode 100644 index 000000000..6141c6a00 --- /dev/null +++ b/apps/touchmenu/touchmenu.boot.js @@ -0,0 +1,201 @@ +E.showMenu = function(items) { + var ar = Bangle.appRect; + Bangle.removeAllListeners("drag"); + if(!items){ + g.clearRect(ar.x, ar.y, ar.x2, ar.y2); + return false; + } + var loc = require("locale"); + var m = { + info: { + title: "Menu", + cB: g.theme.bg, + cF: g.theme.fg, + cHB: g.theme.bgH, + cHF: g.theme.fgH, + cAB: g.theme.bg2, + cAF: g.theme.fg2, + predraw : () => {}, + preflip : () => {} + }, + scroll: 0, + items: [], + selected: -1, + draw: () => { + g.reset().setFont('12x20'); + m.info.predraw(g); + g.setColor(m.info.cB).fillRect(ar.x, ar.y + 20, ar.x2, ar.y2).setColor(m.info.cF); + m.items.forEach((e, i) => { + const s = (i * 48) - m.scroll + ar.y + 20; + if(s < ar.y || s > ar.y2 - 44){ + return false; + } + if(i == m.selected){ + g.setColor(m.info.cHB).fillRect(ar.x, s, ar.x2, Math.min(s + 48, ar.y2)).setColor(m.info.cHF); + }else{ + g.setColor(m.info.cF); + } + g.drawString(e.title, ar.x + (e.icon ? 30 : 10), s + 5); + if(e.icon){ + g.drawImage(e.icon, ar.x + 5, s + 5); + } + if(e.type && s < ar.y2 - 42){ + if(e.format){ + g.setFontAlign(1, -1, 0).drawString(e.format(e.value), ar.x2 - 10, s + 25).setFontAlign(-1, -1, 0); + }else{ + g.setFontAlign(1, -1, 0).drawString(e.value, ar.x2 - 10, s + 25).setFontAlign(-1, -1, 0); + } + } + }); + g.setColor(m.info.cAB).fillRect(ar.x, ar.y, ar.x2, ar.y + 20); + g.setColor(m.info.cAF).drawString(m.info.title, ar.x + (m.back ? 30 : 10), ar.y + 2); + if(m.back){ + g.drawLine(ar.x + 5, ar.y + 10, ar.x + 20, ar.y + 10); + g.drawLine(ar.x + 5, ar.y + 10, ar.x + 15, ar.y + 17); + g.drawLine(ar.x + 5, ar.y + 10, ar.x + 15, ar.y + 3); + } + m.info.preflip(g, m.scroll > 0, m.scroll < (m.items.length - 1) * 48); + }, + select: (x, y) => { + if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - ar.y - 20) / 48), m.items.length - 1), 0)){ + if(y){ + if(y < ar.y + 20 || y > ar.y2){ + return false; + }else{ + m.selected = Math.max(Math.min(Math.floor((y + m.scroll - ar.y - 20) / 48), m.items.length - 1), 0); + } + }else{ + m.selected = Math.floor(m.scroll / 48); + } + m.draw(); + }else{ + if(m.items[m.selected].type && m.items[m.selected].type === "boolean"){ + m.items[m.selected].value = !m.items[m.selected].value; + m.items[m.selected].onchange(m.items[m.selected].value); + m.draw(); + }else if(m.items[m.selected].type && m.items[m.selected].type === "number"){ + if(x && x < ((ar.x + ar.x2) / 2)){ + m.items[m.selected].value = m.items[m.selected].value - (m.items[m.selected].step ? m.items[m.selected].step : 1); + }else{ + m.items[m.selected].value = m.items[m.selected].value + (m.items[m.selected].step ? m.items[m.selected].step : 1); + } + if(m.items[m.selected].value > (m.items[m.selected].max ? m.items[m.selected].max : Infinity)){ + m.items[m.selected].value = m.items[m.selected].min ? m.items[m.selected].min : 0; + } + if(m.items[m.selected].value < (m.items[m.selected].min ? m.items[m.selected].min : 0)){ + m.items[m.selected].value = m.items[m.selected].max ? m.items[m.selected].max : 10; + } + m.items[m.selected].onchange(m.items[m.selected].value); + m.draw(); + }else{ + if(m.items[m.selected]){ + m.items[m.selected](); + } + } + } + }, + move: d => { + m.scroll += (d * 48); + m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48); + m.selected = Math.max(Math.min(Math.floor((m.scroll - ar.y - 20) / 48), m.items.length - 1), 0); + m.draw(); + }, + }; + Object.keys(items).forEach(i => { + if(i == ""){ + m.info = Object.assign(m.info, items[i]); + }else if(i === "< Back" && items[i]){ + m.back = items[i]; + }else if(items[i]){ + m.items.push(items[i]); + m.items[m.items.length - 1].title = loc.translate(i); + if(items[i].hasOwnProperty("value")){ + if(typeof items[i].value === "boolean"){ + m.items[m.items.length - 1].type = "boolean"; + }else{ + m.items[m.items.length - 1].type = "number"; + } + } + } + }); + m.info.title = loc.translate(m.info.title); + m.draw(); + Bangle.on("drag", d => { + if(!d.b){ + return false; + } + if(d.dx == 0 && d.dy == 0){ + if(d.x < ar.x + 30 && d.y < ar.y + 20){ + m.back(); + return false; + } + m.select(d.x, d.y); + }else{ + m.selected = -1; + m.scroll -= d.dy; + m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48); + m.draw(); + } + }); + return m; +}; + +E.showAlert = function (e, t){ + if(!e){ + E.showMenu(); + return false; + } + return new Promise(r => { + const menu = { + "": { + "title": (t ? t : "Alert") + }, + Ok: () => { + E.showMenu(); + r(); + } + }; + menu[e] = () => {}; + E.showMenu(menu); + }); +}; +E.showMessage = E.showAlert; + +E.showPrompt = function (e, t){ + if(!e){ + E.showMenu(); + return false; + } + return new Promise(r => { + const menu = { + "": { + "title": (t && t.title ? t.title : "Choose") + } + }; + menu[e] = () => {}; + if(t && t.buttons){ + Object.keys(t.buttons).forEach(b => { + menu[b] = () => { + E.showMenu(); + r(t.buttons[b]); + }; + }); + }else{ + menu.Yes = () => { + E.showMenu(); + r(true); + }; + menu.No = () => { + E.showMenu(); + r(false); + }; + } + E.showMenu(menu); + }); +}; + +const bsl = Bangle.showLauncher; +Bangle.showLauncher = function (){ + Bangle.removeAllListeners("drag"); + bsl(); +}; diff --git a/apps/touchmenu/touchmenu.gif b/apps/touchmenu/touchmenu.gif new file mode 100644 index 000000000..3df4b3462 Binary files /dev/null and b/apps/touchmenu/touchmenu.gif differ diff --git a/apps/touchmenu/touchmenu.png b/apps/touchmenu/touchmenu.png new file mode 100644 index 000000000..58733cbc7 Binary files /dev/null and b/apps/touchmenu/touchmenu.png differ diff --git a/apps/touchtimer/0_dark_timer_edit.png b/apps/touchtimer/0_dark_timer_edit.png new file mode 100644 index 000000000..2160ef38d Binary files /dev/null and b/apps/touchtimer/0_dark_timer_edit.png differ diff --git a/apps/touchtimer/0_light_timer_edit.png b/apps/touchtimer/0_light_timer_edit.png new file mode 100644 index 000000000..361223af9 Binary files /dev/null and b/apps/touchtimer/0_light_timer_edit.png differ diff --git a/apps/touchtimer/1_dark_timer_ready.png b/apps/touchtimer/1_dark_timer_ready.png new file mode 100644 index 000000000..236aef7f9 Binary files /dev/null and b/apps/touchtimer/1_dark_timer_ready.png differ diff --git a/apps/touchtimer/1_light_timer_ready.png b/apps/touchtimer/1_light_timer_ready.png new file mode 100644 index 000000000..5e2ca3c9d Binary files /dev/null and b/apps/touchtimer/1_light_timer_ready.png differ diff --git a/apps/touchtimer/2_dark_timer_running.png b/apps/touchtimer/2_dark_timer_running.png new file mode 100644 index 000000000..4de00cc61 Binary files /dev/null and b/apps/touchtimer/2_dark_timer_running.png differ diff --git a/apps/touchtimer/2_light_timer_running.png b/apps/touchtimer/2_light_timer_running.png new file mode 100644 index 000000000..2a0d314f4 Binary files /dev/null and b/apps/touchtimer/2_light_timer_running.png differ diff --git a/apps/touchtimer/3_dark_timer_finished.png b/apps/touchtimer/3_dark_timer_finished.png new file mode 100644 index 000000000..c6dd77b82 Binary files /dev/null and b/apps/touchtimer/3_dark_timer_finished.png differ diff --git a/apps/touchtimer/3_light_timer_finished.png b/apps/touchtimer/3_light_timer_finished.png new file mode 100644 index 000000000..18ee015e3 Binary files /dev/null and b/apps/touchtimer/3_light_timer_finished.png differ diff --git a/apps/touchtimer/ChangeLog b/apps/touchtimer/ChangeLog new file mode 100644 index 000000000..01904c6ea --- /dev/null +++ b/apps/touchtimer/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial creation of the touch timer app +0.02: Add settings menu \ No newline at end of file diff --git a/apps/touchtimer/README.md b/apps/touchtimer/README.md new file mode 100644 index 000000000..39afba8e5 --- /dev/null +++ b/apps/touchtimer/README.md @@ -0,0 +1,29 @@ +# Touch Timer + +Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad. + +## How to + +- First input the timer time via the input buttons +- If you need to correct the time, press "<-". +- If the timer time is correct, press "OK". +- If you have accidentially pressed "OK", press "STOP" to go cancel. +- Press "START" to start the timer, if the time is correct. +- The timer will run the time until 0. Once it hits zero the watch will buzz for 1 second every 1 seconds for a total of 3 times, or until you press "STOP" +- -> The number of buzzes, the buzz duration, and the pause between buzzes is configurable in the settings app + +## Screenshots + +### Light Theme + +![](0_light_timer_edit.png) +![](1_light_timer_ready.png) +![](2_light_timer_running.png) +![](3_light_timer_finished.png) + +### Dark Theme + +![](0_dark_timer_edit.png) +![](1_dark_timer_ready.png) +![](2_dark_timer_running.png) +![](3_dark_timer_finished.png) \ No newline at end of file diff --git a/apps/touchtimer/app-icon.js b/apps/touchtimer/app-icon.js new file mode 100644 index 000000000..d58446bcc --- /dev/null +++ b/apps/touchtimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE/4A3mUQIAMRkYWQkBaFiQWQgMjn8zGYUDCxkxFA3zD4MfCxXygECMAURiReCDAM/IpUBFIJ2CAAIeB+ZJKBYI8BCwMBiABBDARSBC5EwFwMwEwUwh5FCEIJhJiEfGIIXC+IQBSwQeBNYR1Gn4xB+MDDYITBiEzFoIOCC4vwEAIxBAwQzBAoQtCBgaNEh4iEAwMwRQXxHgRnBLwsvFQJdCFoIGBl55DH4QAEEIK/BC4KjBC4RECiED+RnBXooxCn4uBKwPwgIiB+fxgQQCRwgeBLwRbBkAXBh5yCBwoACEAoVBC4fwJ4I+DC5EjJQQXDBYP/kJWDC4qmBBYYXFfIQXKiQvUL6AXGR5LzBR4YXIBAS/BC4UCeAQOFC4rvDN4LvCFYMgd4IXJmEABgMxC4bWBiADDC45EBZIRHBMYINCBQQXIIgIkB//wgIFDmBKBC5QNB+UDboU/kEzgCRBC5QTBNwUxLoZRDC5J5EmAqBkEAiYMCC5XzFIMRkECAgILDC5YYDAAUBIoQXNDAMhiMRkYJEC5oAKC7qKBACDfCK4IWRPwjqBkczAB0yGAcQGgYAOmByCfAYAP+MBC4QWR//yC4ciACMhC4YATC4T9BACUSLiQAdA=")) \ No newline at end of file diff --git a/apps/touchtimer/app.js b/apps/touchtimer/app.js new file mode 100644 index 000000000..ffa1af80a --- /dev/null +++ b/apps/touchtimer/app.js @@ -0,0 +1,456 @@ +var DEBUG = false; +var FILE = "touchtimer.data.json"; + +var main = () => { + var settings = readSettings(); + + var button1 = new Button({ x1: 1, y1: 35, x2: 58, y2: 70 }, 1); + var button2 = new Button({ x1: 60, y1: 35, x2: 116, y2: 70 }, 2); + var button3 = new Button({ x1: 118, y1: 35, x2: 174, y2: 70 }, 3); + + var button4 = new Button({ x1: 1, y1: 72, x2: 58, y2: 105 }, 4); + var button5 = new Button({ x1: 60, y1: 72, x2: 116, y2: 105 }, 5); + var button6 = new Button({ x1: 118, y1: 72, x2: 174, y2: 105 }, 6); + + var button7 = new Button({ x1: 1, y1: 107, x2: 58, y2: 140 }, 7); + var button8 = new Button({ x1: 60, y1: 107, x2: 116, y2: 140 }, 8); + var button9 = new Button({ x1: 118, y1: 107, x2: 174, y2: 140 }, 9); + + var buttonOK = new Button({ x1: 1, y1: 142, x2: 58, y2: 174 }, "OK"); + var button0 = new Button({ x1: 60, y1: 142, x2: 116, y2: 174 }, 0); + var buttonDelete = new Button({ x1: 118, y1: 142, x2: 174, y2: 174 }, "<-"); + + var timerNumberButtons = [ + button1, + button2, + button3, + button4, + button5, + button6, + button7, + button8, + button9, + button0, + ]; + + var timerInputButtons = [ + button1, + button2, + button3, + button4, + button5, + button6, + button7, + button8, + button9, + buttonOK, + button0, + buttonDelete, + ]; + + var buttonStartPause = new Button( + { x1: 1, y1: 35, x2: 174, y2: 105 }, + "START" + ); + var buttonStop = new Button({ x1: 1, y1: 107, x2: 174, y2: 174 }, "STOP"); + + var timerRunningButtons = [buttonStartPause, buttonStop]; + + var timerEdit = new TimerEdit(); + timerNumberButtons.forEach((numberButton) => { + numberButton.setOnClick((number) => { + log("number button clicked"); + log(number); + timerEdit.appendNumber(number); + timerEdit.draw(); + }); + }); + + buttonDelete.setOnClick(() => { + log("delete button clicked"); + timerEdit.removeNumber(); + timerEdit.draw(); + }); + + buttonOK.setOnClick(() => { + if (timerEdit.timeStr.length === 0) { + return; + } + + g.clear(); + timerEdit.draw(); + + timerInputButtons.forEach((button) => button.disable()); + + timerRunningButtons.forEach((button) => { + button.enable(); + button.draw(); + }); + }); + + var timerIntervalId = undefined; + var buzzIntervalId = undefined; + var timerCountDown = undefined; + buttonStartPause.setOnClick(() => { + if (buttonStartPause.value === "PAUSE") { + if (timerCountDown) { + timerCountDown.pause(); + } + + buttonStartPause.value = "START"; + buttonStartPause.draw(); + + if (timerIntervalId) { + clearInterval(timerIntervalId); + timerIntervalId = undefined; + } + + if (buzzIntervalId) { + clearInterval(buzzIntervalId); + buzzIntervalId = undefined; + } + + return; + } + + if (buttonStartPause.value === "START") { + if (!timerCountDown) { + timerCountDown = new TimerCountDown(timerEdit.timeStr); + } else { + timerCountDown.unpause(); + } + + buttonStartPause.value = "PAUSE"; + buttonStartPause.draw(); + + timerIntervalId = setInterval(() => { + timerCountDown.draw(); + + if (timerCountDown.isFinished()) { + buttonStartPause.value = "FINISHED!"; + buttonStartPause.draw(); + + if (timerIntervalId) { + clearInterval(timerIntervalId); + timerIntervalId = undefined; + } + + var buzzCount = 1; + Bangle.buzz(settings.buzzDuration * 1000, 1); + buzzIntervalId = setInterval(() => { + if (buzzCount >= settings.buzzCount) { + clearInterval(buzzIntervalId); + buzzIntervalId = undefined; + return; + } else { + Bangle.buzz(settings.buzzDuration * 1000, 1); + buzzCount++; + } + }, settings.buzzDuration * 1000 + settings.pauseBetween * 1000); + } + }, 1000); + + return; + } + }); + + buttonStop.setOnClick(() => { + if (timerCountDown) { + timerCountDown = undefined; + } + + if (timerIntervalId) { + clearInterval(timerIntervalId); + timerIntervalId = undefined; + } + + if (buzzIntervalId) { + clearInterval(buzzIntervalId); + buzzIntervalId = undefined; + } + + buttonStartPause.value = "START"; + buttonStartPause.draw(); + + g.clear(); + timerEdit.reset(); + timerEdit.draw(); + + timerRunningButtons.forEach((button) => button.disable()); + + timerInputButtons.forEach((button) => { + button.enable(); + button.draw(); + }); + }); + + // initalize + g.clear(); + timerEdit.draw(); + timerInputButtons.forEach((button) => { + button.enable(); + button.draw(); + }); +}; + +// lib functions + +var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } +}; + +var touchHandlers = []; + +Bangle.on("touch", (_button, xy) => { + log("touch"); + log(xy); + + var x = Math.min(Math.max(xy.x, 1), 174); + var y = Math.min(Math.max(xy.y, 1), 174); + + touchHandlers.forEach((touchHandler) => { + touchHandler(x, y); + }); +}); + +var BUTTON_BORDER_WITH = 2; + +class Button { + constructor(position, value) { + this.position = position; + this.value = value; + + this.touchHandler = undefined; + this.highlightTimeoutId = undefined; + } + + draw(highlight) { + g.setColor(g.theme.fg); + g.fillRect( + this.position.x1, + this.position.y1, + this.position.x2, + this.position.y2 + ); + + if (highlight) { + g.setColor(g.theme.bgH); + } else { + g.setColor(g.theme.bg); + } + g.fillRect( + this.position.x1 + BUTTON_BORDER_WITH, + this.position.y1 + BUTTON_BORDER_WITH, + this.position.x2 - BUTTON_BORDER_WITH, + this.position.y2 - BUTTON_BORDER_WITH + ); + + g.setColor(g.theme.fg); + g.setFontAlign(0, 0); + g.setFont("Vector", 35); + g.drawString( + this.value, + this.position.x1 + (this.position.x2 - this.position.x1) / 2 + 2, + this.position.y1 + (this.position.y2 - this.position.y1) / 2 + 2 + ); + } + + setOnClick(callback) { + this.touchHandler = (x, y) => { + if ( + x >= this.position.x1 && + x <= this.position.x2 && + y >= this.position.y1 && + y <= this.position.y2 + ) { + this.draw(true); + this.highlightTimeoutId = setTimeout(() => { + this.draw(); + this.highlightTimeoutId = undefined; + }, 100); + setTimeout(() => callback(this.value), 25); + } + }; + } + + disable() { + log("disable button"); + log(this.value); + var touchHandlerIndex = touchHandlers.indexOf(this.touchHandler); + if (touchHandlerIndex > -1) { + log("clearing touch handler"); + touchHandlers.splice(touchHandlerIndex, 1); + } + + if (this.highlightTimeoutId) { + log("clearing higlight timeout"); + clearTimeout(this.highlightTimeoutId); + this.highlightTimeoutId = undefined; + } + } + + enable() { + if (this.touchHandler) { + touchHandlers.push(this.touchHandler); + } + } +} + +class TimerEdit { + constructor() { + this.timeStr = ""; + } + + appendNumber(number) { + if (number === 0 && this.timeStr.length === 0) { + return; + } + + if (this.timeStr.length <= 6) { + this.timeStr = this.timeStr + number; + } + } + + removeNumber() { + if (this.timeStr.length > 0) { + this.timeStr = this.timeStr.slice(0, -1); + } + } + + reset() { + this.timeStr = ""; + } + + draw() { + log("drawing timer edit"); + var timeStrPadded = this.timeStr.padStart(6, "0"); + var timeStrDisplay = + "" + + timeStrPadded.slice(0, 2) + + "h " + + timeStrPadded.slice(2, 4) + + "m " + + timeStrPadded.slice(4, 6) + + "s"; + log(timeStrPadded); + log(timeStrDisplay); + + g.clearRect(0, 0, 176, 34); + g.setColor(g.theme.fg); + g.setFontAlign(-1, -1); + g.setFont("Vector:26x40"); + g.drawString(timeStrDisplay, 2, 0); + } +} + +class TimerCountDown { + constructor(timeStr) { + log("creating timer"); + this.timeStr = timeStr; + log(this.timeStr); + this.start = Math.floor(Date.now() / 1000); + log(this.start); + this.pausedTime = undefined; + } + + getAdjustedTime() { + var elapsedTime = Math.floor(Date.now() / 1000) - this.start; + + var timeStrPadded = this.timeStr.padStart(6, "0"); + var timeStrHours = parseInt(timeStrPadded.slice(0, 2), 10); + var timeStrMinutes = parseInt(timeStrPadded.slice(2, 4), 10); + var timeStrSeconds = parseInt(timeStrPadded.slice(4, 6), 10); + + var hours = timeStrHours; + var minutes = timeStrMinutes; + var seconds = timeStrSeconds - elapsedTime; + + if (seconds < 0) { + var neededMinutes = Math.ceil(Math.abs(seconds) / 60); + + seconds = seconds + neededMinutes * 60; + minutes = minutes - neededMinutes; + + if (minutes < 0) { + var neededHours = Math.ceil(Math.abs(minutes) / 60); + + minutes = minutes + neededHours * 60; + hours = hours - neededHours; + } + } + + if (hours < 0 || minutes < 0 || seconds < 0) { + hours = 0; + minutes = 0; + seconds = 0; + } + + return { hours: hours, minutes: minutes, seconds: seconds }; + } + + pause() { + this.pausedTime = Math.floor(Date.now() / 1000); + } + + unpause() { + if (this.pausedTime) { + this.start += Math.floor(Date.now() / 1000) - this.pausedTime; + } + + this.pausedTime = undefined; + } + + draw() { + log("drawing timer count down"); + var adjustedTime = this.getAdjustedTime(); + var hours = adjustedTime.hours; + var minutes = adjustedTime.minutes; + var seconds = adjustedTime.seconds; + + var timeStrDisplay = + "" + + hours.toString().padStart(2, "0") + + "h " + + minutes.toString().padStart(2, "0") + + "m " + + seconds.toString().padStart(2, "0") + + "s"; + log(timeStrDisplay); + + g.clearRect(0, 0, 176, 34); + g.setColor(g.theme.fg); + g.setFontAlign(-1, -1); + g.setFont("Vector:26x40"); + g.drawString(timeStrDisplay, 2, 0); + } + + isFinished() { + var adjustedTime = this.getAdjustedTime(); + var hours = adjustedTime.hours; + var minutes = adjustedTime.minutes; + var seconds = adjustedTime.seconds; + + if (hours <= 0 && minutes <= 0 && seconds <= 0) { + return true; + } else { + return false; + } + } +} + +var readSettings = () => { + log("reading settings"); + var settings = require("Storage").readJSON(FILE, 1) || { + buzzCount: 3, + buzzDuration: 1, + pauseBetween: 1, + }; + log(settings); + return settings; +}; + +// start main function + +main(); diff --git a/apps/touchtimer/app.png b/apps/touchtimer/app.png new file mode 100644 index 000000000..8ccdb17f0 Binary files /dev/null and b/apps/touchtimer/app.png differ diff --git a/apps/touchtimer/metadata.json b/apps/touchtimer/metadata.json new file mode 100644 index 000000000..645a0ce18 --- /dev/null +++ b/apps/touchtimer/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "touchtimer", + "name": "Touch Timer", + "shortName": "Touch Timer", + "version": "0.02", + "description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.", + "icon": "app.png", + "tags": "tools", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"0_light_timer_edit.png"},{"url":"1_light_timer_ready.png"},{"url":"2_light_timer_running.png"},{"url":"3_light_timer_finished.png"}], + "storage": [ + { "name": "touchtimer.app.js", "url": "app.js" }, + { "name":"touchtimer.settings.js", "url":"settings.js"}, + { "name": "touchtimer.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{"name":"touchtimer.data.json"}] +} diff --git a/apps/touchtimer/settings.js b/apps/touchtimer/settings.js new file mode 100644 index 000000000..885670f57 --- /dev/null +++ b/apps/touchtimer/settings.js @@ -0,0 +1,77 @@ +(function (back) { + var DEBUG = false; + var FILE = "touchtimer.data.json"; + + var settings = {}; + + var showMainMenu = () => { + log("Loading main menu"); + + E.showMenu({ + "": { title: "Touch Timer" }, + "< Back": () => back(), + "Buzz Count": { + value: settings.buzzCount, + min: 1, + max: 3, + step: 1, + onchange: (value) => { + settings.buzzCount = value; + writeSettings(settings); + }, + }, + "Buzz Duration": { + value: settings.buzzDuration, + min: 1, + max: 10, + step: 0.5, + format: (value) => value + "s", + onchange: (value) => { + settings.buzzDuration = value; + writeSettings(settings); + }, + }, + "Pause Between": { + value: settings.pauseBetween, + min: 1, + max: 5, + step: 1, + format: (value) => value + "s", + onchange: (value) => { + settings.pauseBetween = value; + writeSettings(settings); + }, + }, + }); + }; + + // lib functions + + var log = (message) => { + if (DEBUG) { + console.log(JSON.stringify(message)); + } + }; + + var readSettings = () => { + log("reading settings"); + var settings = require("Storage").readJSON(FILE, 1) || { + buzzCount: 3, + buzzDuration: 1, + pauseBetween: 1, + }; + log(settings); + return settings; + }; + + var writeSettings = (settings) => { + log("writing settings"); + log(settings); + require("Storage").writeJSON(FILE, settings); + }; + + // start main function + + settings = readSettings(); + showMainMenu(); +}); diff --git a/apps/trex/metadata.json b/apps/trex/metadata.json new file mode 100644 index 000000000..8344ba161 --- /dev/null +++ b/apps/trex/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "trex", + "name": "T-Rex", + "version": "0.04", + "description": "T-Rex game in the style of Chrome's offline game", + "icon": "trex.png", + "screenshots": [{"url":"screenshot_trex.png"}], + "tags": "game", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"trex.app.js","url":"trex.js"}, + {"name":"trex.img","url":"trex-icon.js","evaluate":true}, + {"name":"trex.settings.js","url":"settings.js"} + ], + "data": [{"name":"trex.score","storageFile":true}] +} diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog new file mode 100644 index 000000000..6ba096f88 --- /dev/null +++ b/apps/vectorclock/ChangeLog @@ -0,0 +1,4 @@ +0.01: New watch face +0.02: Use Bangle.setUI for button/launcher handling +0.03: Bangle.js 2 support +0.04: Adds costumizable colours and the respective settings menu diff --git a/apps/vectorclock/Changelog b/apps/vectorclock/Changelog deleted file mode 100644 index 43190331b..000000000 --- a/apps/vectorclock/Changelog +++ /dev/null @@ -1,2 +0,0 @@ -0.1: New watch face -0.2: Use Bangle.setUI for button/launcher handling diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js index a98c9f97b..e2e4c1e64 100644 --- a/apps/vectorclock/app.js +++ b/apps/vectorclock/app.js @@ -1,13 +1,19 @@ const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; const locale = require("locale"); +var settings = require('Storage').readJSON("vectorclock.json", true) || {}; +var dowcol = settings.dowcol || g.theme.fg; +var timecol = settings.timecol || g.theme.fg; +var datecol = settings.datecol || g.theme.fg; + 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 +26,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); +function drawVectorText(text, size, x, y, alignX, alignY, color) { + g.setFont("Vector", size).setColor(color).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 +52,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 +66,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, timecol); + pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1, timecol); + if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1, timecol); y += timeHeight + spacer; - pushCommand(drawVectorText, dowText, dowFontSize, g.getWidth()/2, y, 0, -1); + pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1, dowcol); y += dowHeight + spacer; - pushCommand(drawVectorText, dateText, dateFontSize, g.getWidth()/2, y, 0, -1); + pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1, datecol); 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 +96,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/vectorclock/metadata.json b/apps/vectorclock/metadata.json new file mode 100644 index 000000000..3da93ccad --- /dev/null +++ b/apps/vectorclock/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "vectorclock", + "name": "Vector Clock", + "version": "0.04", + "description": "A digital clock that uses the built-in vector font.", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "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}, + {"name":"vectorclock.settings.js","url":"settings.js"} + ], + "data": [{"name":"vectorclock.json"}] +} diff --git a/apps/vectorclock/settings.js b/apps/vectorclock/settings.js new file mode 100644 index 000000000..a6f4dd807 --- /dev/null +++ b/apps/vectorclock/settings.js @@ -0,0 +1,45 @@ +(function(back) { + var FILE = "vectorclock.json"; + // Load settings + var settings = Object.assign({ + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + var colnames = ["white", "yellow", "green", "cyan", "red", "orange", "magenta", "black"]; + var colvalues = [0xFFFF, 0xFFE0, 0x07E0, 0x07FF, 0xF800, 0xFD20, 0xF81F, 0x0000]; + // Show the menu + E.showMenu({ + "" : { "title" : "VectorClock colours" }, + "< Back" : () => back(), + 'Time': { + value: Math.max(0 | colvalues.indexOf(settings.timecol),0), + min: 0, max: colvalues.length-1, + format: v => colnames[v], + onchange: v => { + settings.timecol = colvalues[v]; + writeSettings(); + } + }, + 'Weekday': { + value: Math.max(0 | colvalues.indexOf(settings.dowcol),0), + min: 0, max: colvalues.length-1, + format: v => colnames[v], + onchange: v => { + settings.dowcol = colvalues[v]; + writeSettings(); + } + }, + 'Date': { + value: Math.max(0 | colvalues.indexOf(settings.datecol),0), + min: 0, max: colvalues.length-1, + format: v => colnames[v], + onchange: v => { + settings.datecol = colvalues[v]; + writeSettings(); + } + }, + }); +}) diff --git a/apps/vernierrespirate/Vernier-go-direct-respiration-belt-screenshot.png b/apps/vernierrespirate/Vernier-go-direct-respiration-belt-screenshot.png new file mode 100644 index 000000000..f5d095582 Binary files /dev/null and b/apps/vernierrespirate/Vernier-go-direct-respiration-belt-screenshot.png differ diff --git a/apps/vernierrespirate/metadata.json b/apps/vernierrespirate/metadata.json new file mode 100644 index 000000000..5e2baf2bb --- /dev/null +++ b/apps/vernierrespirate/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "vernierrespirate", + "name": "Vernier Go Direct Respiration Belt", + "shortName": "Respiration Belt", + "version": "0.01", + "description": "Connects to a Go Direct Respiration Belt and shows respiration rate", + "icon": "app.png", + "tags": "health,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"Vernier-go-direct-respiration-belt-screenshot.png"}], + "storage": [ + {"name":"vernierrespirate.app.js","url":"app.js"}, + {"name":"vernierrespirate.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"vernierrespirate.json"}] +} diff --git a/apps/verticalface/metadata.json b/apps/verticalface/metadata.json new file mode 100644 index 000000000..da41b3f0d --- /dev/null +++ b/apps/verticalface/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "verticalface", + "name": "Vertical watch face", + "shortName": "Vertical Face", + "version": "0.09", + "description": "A simple vertical watch face with the date. Heart rate monitor is toggled with BTN1", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-vertical-watch-face-screenshot.png"}], + "storage": [ + {"name":"verticalface.app.js","url":"app.js"}, + {"name":"verticalface.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/vibrclock/metadata.json b/apps/vibrclock/metadata.json new file mode 100644 index 000000000..d05553960 --- /dev/null +++ b/apps/vibrclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "vibrclock", + "name": "Vibrate Clock", + "version": "0.03", + "description": "When BTN1 is pressed, vibrate out the time as a series of buzzes, one digit at a time. Hours, then Minutes. Zero is signified by one long buzz. Otherwise a simple digital clock.", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "allow_emulator": true, + "screenshots": [{"url":"bangle1-vibrate-clock-screenshot.png"}], + "storage": [ + {"name":"vibrclock.app.js","url":"app.js"}, + {"name":"vibrclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/viewstl/metadata.json b/apps/viewstl/metadata.json new file mode 100644 index 000000000..8d1cc3f98 --- /dev/null +++ b/apps/viewstl/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "viewstl", + "name": "STL file viewer", + "shortName": "ViewSTL", + "version": "0.02", + "description": "This app allows you to view STL 3D models on your watch", + "icon": "icons8-octahedron-48.png", + "tags": "tool", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"viewstl.app.js","url":"viewstl.min.js"}, + {"name":"viewstl.img","url":"viewstl-icon.js","evaluate":true}, + {"name":"tetra.stl","url":"tetra.stl"}, + {"name":"cube.stl","url":"cube.stl"}, + {"name":"icosa.stl","url":"icosa.stl"} + ] +} diff --git a/apps/walkersclock/metadata.json b/apps/walkersclock/metadata.json new file mode 100644 index 000000000..3d93cffdf --- /dev/null +++ b/apps/walkersclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "walkersclock", + "name": "Walkers Clock", + "shortName": "Walkers Clock", + "version": "0.04", + "description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference", + "icon": "walkersclock48.png", + "type": "clock", + "tags": "clock,gps,tools,outdoors", + "supports": ["BANGLEJS"], + "readme": "README.md", + "storage": [ + {"name":"walkersclock.app.js","url":"app.js"}, + {"name":"walkersclock.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/waveclk/metadata.json b/apps/waveclk/metadata.json new file mode 100644 index 000000000..503c9c11c --- /dev/null +++ b/apps/waveclk/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "waveclk", + "name": "Wave Clock", + "version": "0.02", + "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", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"waveclk.app.js","url":"app.js"}, + {"name":"waveclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json new file mode 100644 index 000000000..cb477107b --- /dev/null +++ b/apps/waypointer/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "waypointer", + "name": "Way Pointer", + "version": "0.01", + "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", + "icon": "waypointer.png", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS"], + "readme": "README.md", + "interface": "waypoints.html", + "storage": [ + {"name":"waypointer.app.js","url":"app.js"}, + {"name":"waypointer.img","url":"icon.js","evaluate":true} + ], + "data": [{"name":"waypoints.json","url":"waypoints.json"}] +} diff --git a/apps/wclock/metadata.json b/apps/wclock/metadata.json new file mode 100644 index 000000000..f22b53dc1 --- /dev/null +++ b/apps/wclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "wclock", + "name": "Word Clock", + "version": "0.03", + "description": "Display Time as Text", + "icon": "clock-word.png", + "screenshots": [{"url":"screenshot_word.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"wclock.app.js","url":"clock-word.js"}, + {"name":"wclock.img","url":"clock-word-icon.js","evaluate":true} + ] +} diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index c1a0504a4..101da48e1 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -8,3 +8,7 @@ 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 +0.14: Use weather condition code for icon selection +0.15: Fix widget icon diff --git a/apps/weather/app.js b/apps/weather/app.js index 8c8526fbd..efd9b0209 100644 --- a/apps/weather/app.js +++ b/apps/weather/app.js @@ -9,7 +9,7 @@ var layout = new Layout({type:"v", bgCol: g.theme.bg, c: [ {filly: 1}, {type: "h", filly: 0, c: [ {type: "custom", width: g.getWidth()/2, height: g.getWidth()/2, valign: -1, txt: "unknown", id: "icon", - render: l => weather.drawIcon(l.txt, l.x+l.w/2, l.y+l.h/2, l.w/2-5)}, + render: l => weather.drawIcon(l, l.x+l.w/2, l.y+l.h/2, l.w/2-5)}, {type: "v", fillx: 1, c: [ {type: "h", pad: 2, c: [ {type: "txt", font: "18%", id: "temp", label: "000"}, @@ -47,6 +47,7 @@ function formatDuration(millis) { function draw() { layout.icon.txt = current.txt; + layout.icon.code = current.code; const temp = locale.temp(current.temp-273.15).match(/^(\D*\d*)(.*)$/); layout.temp.label = temp[1]; layout.tempUnit.label = temp[2]; diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 76ed2aaa4..1d48116e1 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -16,7 +16,7 @@ function scheduleExpiry(json) { function update(weatherEvent) { let json = storage.readJSON('weather.json')||{}; - + if (weatherEvent) { let weather = weatherEvent.clone(); delete weather.t; @@ -53,15 +53,73 @@ exports.get = function() { scheduleExpiry(storage.readJSON('weather.json')||{}); +/** + * + * @param cond Weather condition, as one of: + * {number} code: (Preferred form) https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + * {string} weather description (in English: breaks for other languages!) + * {object} use cond.code if present, or fall back to cond.txt + * @param x Left + * @param y Top + * @param r Icon Size + */ exports.drawIcon = function(cond, x, y, r) { + var palette; + + 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 +136,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 +146,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 +182,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 +210,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 +245,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,40 +255,86 @@ 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); } - function chooseIcon(condition) { - if (!condition) return () => {}; - condition = condition.toLowerCase(); - if (condition.includes("thunderstorm")) return drawThunderstorm; - if (condition.includes("freezing")||condition.includes("snow")|| - condition.includes("sleet")) { + /* + * Choose weather icon to display based on weather description + */ + function chooseIconByTxt(txt) { + if (!txt) return () => {}; + txt = txt.toLowerCase(); + if (txt.includes("thunderstorm")) return drawThunderstorm; + if (txt.includes("freezing")||txt.includes("snow")|| + txt.includes("sleet")) { return drawSnow; } - if (condition.includes("drizzle")|| - condition.includes("shower")) { + if (txt.includes("drizzle")|| + txt.includes("shower")) { return drawRain; } - if (condition.includes("rain")) return drawShowerRain; - if (condition.includes("clear")) return drawSun; - if (condition.includes("few clouds")) return drawFewClouds; - if (condition.includes("scattered clouds")) return drawCloud; - if (condition.includes("clouds")) return drawBrokenClouds; - if (condition.includes("mist") || - condition.includes("smoke") || - condition.includes("haze") || - condition.includes("sand") || - condition.includes("dust") || - condition.includes("fog") || - condition.includes("ash") || - condition.includes("squalls") || - condition.includes("tornado")) { + if (txt.includes("rain")) return drawShowerRain; + if (txt.includes("clear")) return drawSun; + if (txt.includes("few clouds")) return drawFewClouds; + if (txt.includes("scattered clouds")) return drawCloud; + if (txt.includes("clouds")) return drawBrokenClouds; + if (txt.includes("mist") || + txt.includes("smoke") || + txt.includes("haze") || + txt.includes("sand") || + txt.includes("dust") || + txt.includes("fog") || + txt.includes("ash") || + txt.includes("squalls") || + txt.includes("tornado")) { return drawMist; } return drawUnknown; } + /* + * Choose weather icon to display based on weather conditition code + * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + */ + function chooseIconByCode(code) { + const codeGroup = Math.round(code / 100); + switch (codeGroup) { + case 2: return drawThunderstorm; + case 3: return drawRain; + case 5: + switch (code) { + case 511: return drawSnow; + case 520: return drawShowerRain; + case 521: return drawShowerRain; + case 522: return drawShowerRain; + case 531: return drawShowerRain; + default: return drawRain; + } + case 6: return drawSnow; + case 7: return drawMist; + case 8: + switch (code) { + case 800: return drawSun; + case 801: return drawFewClouds; + case 802: return drawCloud; + default: return drawBrokenClouds; + } + default: return drawUnknown; + } + } + + function chooseIcon(cond) { + if (typeof (cond)==="object") { + if ("code" in cond) return chooseIconByCode(cond.code); + if ("txt" in cond) return chooseIconByTxt(cond.txt); + } else if (typeof (cond)==="number") { + return chooseIconByCode(cond.code); + } else if (typeof (cond)==="string") { + return chooseIconByTxt(cond.txt); + } + return drawUnknown; + } chooseIcon(cond)(x, y, r); + }; diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json new file mode 100644 index 000000000..1d0b6b469 --- /dev/null +++ b/apps/weather/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "weather", + "name": "Weather", + "version": "0.15", + "description": "Show Gadgetbridge weather report", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "tags": "widget,outdoors", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "readme.md", + "storage": [ + {"name":"weather.app.js","url":"app.js"}, + {"name":"weather.wid.js","url":"widget.js"}, + {"name":"weather","url":"lib.js"}, + {"name":"weather.img","url":"icon.js","evaluate":true}, + {"name":"weather.settings.js","url":"settings.js"} + ], + "data": [{"name":"weather.json"}] +} 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..2905d776b 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,14 +45,15 @@ 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(); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); - if (w.txt) { - weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5); + if (w.code||w.txt) { + weather.drawIcon(w, this.x+10, this.y+8, 7.5); } if (w.temp) { let t = require('locale').temp(w.temp-273.15); // applies conversion @@ -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..a6a12c297 --- /dev/null +++ b/apps/weatherClock/ChangeLog @@ -0,0 +1,5 @@ +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 +0.05: Use weather condition code for icon selection 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..1a7f53f05 --- /dev/null +++ b/apps/weatherClock/app.js @@ -0,0 +1,156 @@ +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; +} + +/* +* Choose weather icon to display based on weather conditition code +* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 +*/ +function chooseIconByCode(code) { + const codeGroup = Math.round(code / 100); + switch (codeGroup) { + case 2: return stormIcon; + case 3: return rainIcon; + case 5: return rainIcon; + case 6: return snowIcon; + case 7: return cloudIcon; + case 8: + switch (code) { + case 800: return sunIcon; + case 801: return partSunIcon; + default: return cloudIcon; + } + break; + default: 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]; + const code = currentWeather.code || -1; + if (code > 0) { + clockLayout.weatherIcon.src = chooseIconByCode(code); + } else { + 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/metadata.json b/apps/weatherClock/metadata.json new file mode 100644 index 000000000..cf8bd899e --- /dev/null +++ b/apps/weatherClock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "weatherClock", + "name": "Weather Clock", + "version": "0.05", + "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} + ] +} 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/welcome/metadata.json b/apps/welcome/metadata.json new file mode 100644 index 000000000..b132c2d51 --- /dev/null +++ b/apps/welcome/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "welcome", + "name": "Welcome", + "shortName": "Welcome", + "version": "0.14", + "description": "Appears at first boot and explains how to use Bangle.js", + "icon": "app.png", + "screenshots": [{"url":"screenshot_welcome.png"}], + "tags": "start,welcome", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"welcome.boot.js","url":"boot.js"}, + {"name":"welcome.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, + {"name":"welcome.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]}, + {"name":"welcome.settings.js","url":"settings.js"}, + {"name":"welcome.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"welcome.json"}] +} diff --git a/apps/whereworld/metadata.json b/apps/whereworld/metadata.json new file mode 100644 index 000000000..1106a3b39 --- /dev/null +++ b/apps/whereworld/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "whereworld", + "name": "Where in the World?", + "shortName": "Where World", + "version": "0.01", + "description": "Shows your current location on the world map", + "icon": "app.png", + "tags": "gps", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"whereworld.app.js","url":"app.js"}, + {"name":"whereworld.img","url":"app-icon.js","evaluate":true}, + {"name":"whereworld.worldmap","url":"worldmap"} + ] +} diff --git a/apps/widChargingStatus/ChangeLog b/apps/widChargingStatus/ChangeLog new file mode 100644 index 000000000..1033c0cd3 --- /dev/null +++ b/apps/widChargingStatus/ChangeLog @@ -0,0 +1 @@ +0.01: First release. diff --git a/apps/widChargingStatus/metadata.json b/apps/widChargingStatus/metadata.json new file mode 100644 index 000000000..f68ccf5b4 --- /dev/null +++ b/apps/widChargingStatus/metadata.json @@ -0,0 +1,13 @@ +{ "id": "widChargingStatus", + "name": "Charging Status", + "shortName":"ChargingStatus", + "icon": "widget.png", + "version":"0.01", + "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"} + ] +} 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..a111d259a 100644 --- a/apps/wid_a_battery_widget/ChangeLog +++ b/apps/wid_a_battery_widget/ChangeLog @@ -1,2 +1,4 @@ -1.00: Release for Bangle 2 (2021/11/18) -1.01: Internal id update to wid_* as per Gordon's request (2021/11/21) +0.01: Release for Bangle 2 (2021/11/18) +0.02: Internal id update to wid_* as per Gordon's request (2021/11/21) +0.03: Support dark themes +0.04: Increase screen update rate when charging diff --git a/apps/wid_a_battery_widget/metadata.json b/apps/wid_a_battery_widget/metadata.json new file mode 100644 index 000000000..6c507b7b3 --- /dev/null +++ b/apps/wid_a_battery_widget/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "wid_a_battery_widget", + "name": "A Battery Widget (with percentage)", + "shortName":"A Battery Widget", + "icon": "widget.png", + "version":"0.04", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "description": "Simple and slim battery widget with charge status and percentage", + "tags": "widget,battery", + "storage": [ + {"name":"wid_a_battery_widget.wid.js","url":"widget.js"} + ] +} diff --git a/apps/wid_a_battery_widget/widget.js b/apps/wid_a_battery_widget/widget.js index 9fb06e320..74c76784d 100644 --- a/apps/wid_a_battery_widget/widget.js +++ b/apps/wid_a_battery_widget/widget.js @@ -1,9 +1,12 @@ (function(){ + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + 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", }; @@ -36,10 +39,14 @@ g.setFontAlign(0,0); g.setFont('6x8'); g.drawString(l, x + 14, y + 10); + + if (Bangle.isCharging()) changeInterval(id, intervalHigh); + else changeInterval(id, intervalLow); } + Bangle.on('charging',function(charging) { draw(); }); - setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), 60000); + var id = setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), intervalLow); WIDGETS["wid_a_battery_widget"]={area:"tr",width:30,draw:draw}; })(); diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog new file mode 100644 index 000000000..2fa857bd8 --- /dev/null +++ b/apps/wid_edit/ChangeLog @@ -0,0 +1 @@ +0.01: new Widget Editor! \ No newline at end of file diff --git a/apps/wid_edit/README.md b/apps/wid_edit/README.md new file mode 100644 index 000000000..e5003280c --- /dev/null +++ b/apps/wid_edit/README.md @@ -0,0 +1,16 @@ +# Widget Editor + +This adds a setting menu which allows you to change the location of widgets. + +## Settings + +There is no app icon in the launcher; you can find the settings under +`Apps`->`Widget Editor`. + +For every widget, you have these options: +* **Side**: On which side to draw the widget. +* **Sort Order**: Changes the order if several widgets use the same side. + +## Creator + +Richard de Boer diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js new file mode 100644 index 000000000..872965c97 --- /dev/null +++ b/apps/wid_edit/boot.js @@ -0,0 +1,24 @@ +Bangle.loadWidgets = function() { + global.WIDGETS={}; + require("Storage").list(/\.wid\.js$/) + .forEach(w=>{ + try { eval(require("Storage").read(w)); } + catch (e) { print(w, e); } + }); + const s = require("Storage").readJSON("wid_edit.json", 1) || {}, + c = s.custom || {}; + for (const w in c){ + if (!(w in WIDGETS)) continue; // widget no longer exists + // store defaults of customized values in _WIDGETS + global._WIDGETS=global._WIDGETS||{}; + _WIDGETS[w] = {}; + Object.keys(c[w]).forEach(k => _WIDGETS[w][k] = WIDGETS[w][k]); + Object.assign(WIDGETS[w], c[w]); + } + const W = WIDGETS; + WIDGETS = {}; + Object.keys(W) + .sort() + .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) + .forEach(k => WIDGETS[k] = W[k]); +} \ No newline at end of file diff --git a/apps/wid_edit/icon.png b/apps/wid_edit/icon.png new file mode 100644 index 000000000..1d072c381 Binary files /dev/null and b/apps/wid_edit/icon.png differ diff --git a/apps/wid_edit/metadata.json b/apps/wid_edit/metadata.json new file mode 100644 index 000000000..66d0192f6 --- /dev/null +++ b/apps/wid_edit/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "wid_edit", + "version": "0.01", + "name": "Widget Editor", + "icon": "icon.png", + "description": "Customize widget locations", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "type": "bootloader", + "tags": "widget,tool", + "storage": [ + {"name":"wid_edit.boot.js","url":"boot.js"}, + {"name":"wid_edit.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"wid_edit.json"} + ] +} diff --git a/apps/wid_edit/settings.js b/apps/wid_edit/settings.js new file mode 100644 index 000000000..0969ed533 --- /dev/null +++ b/apps/wid_edit/settings.js @@ -0,0 +1,190 @@ +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const names = {}; + const settings = require("Storage").readJSON("wid_edit.json", 1) || {}; + if (!('custom' in settings)) settings.custom = {}; + global._WIDGETS = global._WIDGETS || {}; + + let cleanup = false; + for (const id in settings.custom) { + if (!(id in WIDGETS)) { + // widget which no longer exists + cleanup = true; + delete settings.custom[id]; + } + } + if (cleanup) { + if (!Object.keys(settings.custom).length) delete settings.custom; + require("Storage").writeJSON("wid_edit.json", settings); + } + + /** + * Sort & redraw all widgets + */ + function redrawWidgets() { + let W = WIDGETS; + global.WIDGETS = {}; + Object.keys(W) + .sort() + .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) + .forEach(k => {WIDGETS[k] = W[k]}); + Bangle.drawWidgets(); + } + + /** + * Try to find app name for widget + * @param {string} widget WIDGETS key + * @return {string} widget name + */ + function name(widget) { + if (!(widget in names)) { + let infoFile = widget+".info"; + // widget names don't always correspond to appid :-( + // so we try both with and without 'wid'-prefix + if (!require("Storage").list(new RegExp(`^${infoFile}$`)).length) { + infoFile = (widget.substr(0, 3)==="wid") ? infoFile.substr(3) : ("wid"+infoFile); + } + names[widget] = (require("Storage").readJSON(infoFile, 1) || {}).name || widget; + } + return names[widget]; + } + + function edit(id) { + let WIDGET = WIDGETS[id], + def = {area: WIDGET.area, sortorder: WIDGET.sortorder|0}; // default values + Object.assign(def, _WIDGETS[id]||{}); // defaults were saved in _WIDGETS + + settings.custom = settings.custom||{}; + let saved = settings.custom[id] || {}, + area = saved.area || def.area, + sortorder = ("sortorder" in saved) ? saved.sortorder : def.sortorder; + + /** + * Draw highlighted widget + */ + function highlight() { + if (WIDGET.width > 0) { + // draw widget, then draw a highlighted border on top + WIDGET.draw(); + g.setColor(g.theme.fgH) + .drawRect(WIDGET.x, WIDGET.y, WIDGET.x+WIDGET.width-1, WIDGET.y+23); + } else { + // hidden widget: fake a width and provide our own draw() + const draw = WIDGET.draw, width = WIDGET.width; + WIDGET.width = 24; + WIDGET.draw = function() { + g.setColor(g.theme.bgH).setColor(g.theme.fgH) + .clearRect(this.x, this.y, this.x+23, this.y+23) + .drawRect(this.x, this.y, this.x+23, this.y+23) + .drawLine(this.x, this.y, this.x+23, this.y+23) + .drawLine(this.x, this.y+23, this.x+23, this.y); + }; + // re-layout+draw all widgets with our placeholder in between + redrawWidgets(); + // and restore original values + WIDGET.draw = draw; + WIDGET.width = width; + } + } + highlight(); + + /** + * Save widget and redraw with new settings + */ + function save() { + // we only save non-default values + saved = {}; + if ((area!==def.area) || (sortorder!==def.sortorder)) { + if (area!==def.area) saved.area = area; + if (sortorder!==def.sortorder) saved.sortorder = sortorder; + settings.custom = settings.custom || {}; + settings.custom[id] = saved; + } else if (settings.custom) { + delete settings.custom[id] + } + if (!Object.keys(settings.custom).length) delete settings.custom; + require("Storage").writeJSON("wid_edit.json", settings); + Object.assign(WIDGET, def, saved); + if (WIDGET.sortorder === undefined) delete WIDGET.sortorder; // default can be undefined, but don't put that in the widget + // if we assigned custom values, store defaults in _WIDGETS + let _W = {}; + if (saved.area) _W.area = def.area; + if ('sortorder' in saved) _W.sortorder = def.sortorder; + if (Object.keys(_W).length) _WIDGETS[id] = _W; + else delete _WIDGETS[id]; + + // drawWidgets won't clear e.g. bottom bar if we just disabled the last bottom widget + redrawWidgets(); + + highlight(); + m.draw(); + } + + const menu = { + "": {"title": name(id)}, + /*LANG*/"< Back": () => { + redrawWidgets(); + mainMenu(); + }, + /*LANG*/"Side": { + value: (area === 'tl'), + format: tl => tl ? /*LANG*/"Left" : /*LANG*/"Right", + onchange: tl => { + area = tl ? "tl" : "tr"; + save(); + } + }, + /*LANG*/"Sort Order": { + value: sortorder, + onchange: o => { + sortorder = o; + save(); + } + }, + /*LANG*/"Reset": () => { + area = def.area; + sortorder = def.sortorder; + save(); + mainMenu(); // changing multiple values made the rest of the menu wrong, so take the easy out + } + } + + let m = E.showMenu(menu); + } + + + function mainMenu() { + let menu = { + "": {"title": /*LANG*/"Widgets"}, + }; + menu[/*LANG*/"< Back"] = ()=>{ + if (!Object.keys(_WIDGETS).length) delete _WIDGETS; // no defaults to remember + back(); + }; + Object.keys(WIDGETS).forEach(id=>{ + // mark customized widgets with asterisk + menu[name(id)+((id in _WIDGETS) ? " *" : "")] = () => edit(id); + }); + if (Object.keys(_WIDGETS).length) { // only show reset if there is anything to reset + menu[/*LANG*/"Reset All"] = () => { + E.showPrompt(/*LANG*/"Reset all widgets?").then(confirm => { + if (confirm) { + delete settings.custom; + require("Storage").writeJSON("wid_edit.json", settings); + for(let id in _WIDGETS) { + Object.assign(WIDGETS[id], _WIDGETS[id]) // restore defaults + } + global._WIDGETS = {}; + redrawWidgets(); + } + mainMenu(); // reload with reset widgets + }) + } + } + + E.showMenu(menu); + } + mainMenu(); +}); diff --git a/apps/widancs/ChangeLog b/apps/widancs/ChangeLog deleted file mode 100644 index 471507736..000000000 --- a/apps/widancs/ChangeLog +++ /dev/null @@ -1,7 +0,0 @@ -0.01: New Widget! -0.02: Version using connect back -0.03: Version using modified firmware -0.04: Works on both standard and modified firmware -0.05: Bug fixes w.r.t. reconnection -0.06: Update README - Release version -0.07: Respect Quiet Mode diff --git a/apps/widancs/README.md b/apps/widancs/README.md deleted file mode 100644 index d3ee0bdc4..000000000 --- a/apps/widancs/README.md +++ /dev/null @@ -1,70 +0,0 @@ -## ANCS - iPhone notifications for Bangle.js - -The ANCS widget allows you to answer or cancel iPhone incoming calls and also displays messages and notifications. It connects to the Apple Notification Center Service which is already on all iPhones, so you do not need to install any additional iPhone apps to use this widget. - -## Firmware -The widget will run on the standard firmware, however, installation of a slightly modified version - the zip file is available from [this directory](https://github.com/jeffmer/JeffsBangleAppsDev/tree/master/apps/widancs) - will increase the performance of the app by an order of magnitude in terms of the time to connect or reconnect to the iPhone. In addition, the Bangle will stay connected to the iPhone over a greater separation distance than with the standard firmware. - - -![](widget_pic.jpg) - -## Installation - -After the widget is uploaded to the Bangle, it needs to be enabled in the Bangle Settings app:- `ANCS Widget` will appear in `APP/Widget settings`. There is also a menu in these settings to let you configure the categories of notifications that you want to be displayed. You must disconnect from the App Loader before enabling the widget. - -## Compatible Apps - -The widget will only run with a compatible app - for the reason for this see Issue 1 below. The apps that are compatible with the ANCS widget are:- **Multi Clock**, **Navigation Compass** and **GPS Navigation**. When you switch to an app that is not compatible, the ANCS phone icon will not appear. - -## iPhone Pairing -Once enabled, the widget icon should be displayed coloured grey (its green in the photo). Go to the phone's Bluetooth settings menu and your Bangle should appear under Other devices. If this is the first time you have connected with the Bangle from your iPhone, it may be named Accessory. Click on the name and the iPhone should connect and start pairing. The widget icon will turn red and the iPhone will ask you to enter a pairing code - the traditional 123456. You have 10 seconds to enter this after which you will need to start pairing again. After that, the iPhone may also ask to allow the device access to ANCS. Once pairing is complete, the widget icon should go blue and eventually green. The range of colours is: - -* **Grey** - not connected - advertising -* **Red** - connected - not paired. -* **Blue** - paired and connected - getting services -* **Yellow** - got Services. -* **Green** - waiting for new notifications. - -After pairing the first time, the Bangle should connect automatically when the widget is running. Sometimes you may need you to click on the Bangle name in `Settings:Bluetooth:My devices` on the iPhone or disable and then enable Bluetooth to start connection. If you need to load other apps from the iPhone, it will be necessary to ask the iPhone to forget the pairing and you will also need to disable the widget in Settings and restart the Bangle by turning it off in Settings and then pressing BTN1 to restart. If you are loading apps from a different device, you simply need to turn off the iPhone bluetooth which will retain the pairing. You still need to disable the widget and restart the Bangle. - -![](message_pic.jpg) - -## Messages & Calls -Messages are displayed as shown above until BTN2 is pressed to dismiss it. I strongly advise disabling the BTN2 LCD wake function in the Settings App as otherwise when the screen times out and you press BTN2 to wake the LCD, the screen will turn on and the Message Alert will be dismissed!. Calls can be answered or dropped. - -![](call_pic.jpg) ![](missed_pic.jpg) - - -## Issues -1. With GadgetBridge, the Android phone has a Central-Client role with the Bangle as Peripheral-Server. With the ANCS widget there is the fairly unusual situation in which the Bangle is Peripheral-Client to the iPhone's Central-Server role. Since Espruino does not deal explicitly with Bangle as Peripheral-Client an additional function has been added in the modified firmware: `var gatt = NRF.getGattforCentralServer(addr);`. This returns a bluetooth remote GATT server given the address of the iPhone which has just connected to the Bangle. With the standard firmware, the widget reconnects to the iPhone as a Client - however this has greatly degraded performance. See [Issue 1800.](https://github.com/espruino/Espruino/issues/1800) for more details. - -2. When the Bangle switches apps, all state - including widget state - is lost unless explicitly stored. The consequence of this is that when the Bangle switches apps, the connection to iPhone has to be re-established to restore the remote GATT server and characteristics state. This is quite slow. To minimise reconnection, the widget needs to grab the screen from the running app to signal messages and calls. To allow this to work, the app needs to implement the `SCREENACCESS` interface. In essence, the widget only connects when running with compatible apps that implement this interface. An example implementation is: - -``` -var SCREENACCESS = { - withApp:true, - request:function(){ - this.withApp=false; - stopdraw(); //clears redraw timers etc - clearWatch(); //clears button handlers - }, - release:function(){ - this.withApp=true; - startdraw(); //redraw app screen, restart timers etc - setButtons(); //install button event handlers - } -} - -Bangle.on('lcdPower',function(on) { - if (!SCREENACCESS.withApp) return; - if (on) { - startdraw(); - } else { - stopdraw(); - } -}); -``` - -## Support - -Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev). \ No newline at end of file diff --git a/apps/widancs/ancs.js b/apps/widancs/ancs.js deleted file mode 100644 index 50720cd23..000000000 --- a/apps/widancs/ancs.js +++ /dev/null @@ -1,266 +0,0 @@ -(() => { - - var s = require("Storage").readJSON("widancs.json",1)||{settings:{enabled:false, category:[1,2,4]}}; - var ENABLED = s.settings.enabled; - var CATEGORY = s.settings.category; - - function advert(){ - NRF.setAdvertising([ - 0x02, //length - 0x01, //flags - 0x06, // - 0x11, //length - 0x15, //solicited Service UUID - 0xD0,0x00,0x2D,0x12,0x1E,0x4B, - 0x0F,0xA4, - 0x99,0x4E, - 0xCE,0xB5, - 0x31,0xF4,0x05,0x79],{connectable:true,discoverable:true,interval:375}); - } - - var state = { - gatt:null, - ancs:null, - current:{cat:0,uid:0}, - notqueue:[], - msgTO:undefined, - com:new Uint8Array([0,0,0,0,0,1,20,0,3,100,0]), - buf:new Uint8Array(132), - inp:0, - store:function(b){ - var i = this.inp; - if (i+b.length<=132){ - this.buf.set(b,i); - this.inp+=b.length; - } - }, - gotmsg:function(){ - var n = this.inp; - var vw = DataView(this.buf.buffer); - if (n<8) return null; - var tn=vw.getUint16(6,true); - if (n<(tn+8)) return null; - var mn=vw.getUint16(9+tn,true); - if (n<(mn+tn+11)) return null; - return {tlen:tn, mlen:mn}; - } - }; - - //stop advertising when peripheral link disconnected - if (!NRF.getGattforCentralServer && ENABLED && typeof SCREENACCESS!='undefined') - NRF.on('disconnect',function(reason){ - NRF.sleep(); - }); - - if (ENABLED && typeof SCREENACCESS!='undefined') - NRF.on('connect',function(addr){ - if(NRF.getGattforCentralServer) - do_bond(NRF.getGattforCentralServer(addr)); - else - NRF.connect(addr).then(do_bond); - }); - - function do_bond(g) { - var tval, ival; - state.gatt = g; - function cleanup(){ - drawIcon(0); //disconnect from iPhone - delete state.gatt; - delete state.ancs; - if(!NRF.getGattforCentralServer) NRF.disconnect(); - setTimeout(()=>{NRF.wake();},500); - } - drawIcon(1); //connect from iPhone - state.gatt.device.on('gattserverdisconnected', function(reason) { - if (ival) clearInterval(ival); - if (tval) clearInterval(tval); - cleanup(); - }); - E.on("kill",function(){ - state.gatt.disconnect().then(function(){NRF.sleep();}); - }); - NRF.setSecurity({passkey:"123456",mitm:1,display:1}); - tval = setTimeout(function(){ - if (ival) clearInterval(ival); - state.gatt.disconnect().then(cleanup); - },10000); - state.gatt.startBonding().then(function(){ - ival = setInterval(function(){ - var sec = state.gatt.getSecurityStatus(); - if (!sec.connected) {clearInterval(ival); clearTimeout(tval); return;} - if (sec.connected && sec.encrypted){ - clearInterval(ival); - clearTimeout(tval); - drawIcon(2); //bonded to iPhone - do_ancs(); - return; - } - },1000); - }).catch(function(e){ - Terminal.println("ERROR "+e); - }); - } - - function do_ancs() { - state.ancs = {primary:null, notify:null, control:null, data:null}; - state.gatt.getPrimaryService("7905F431-B5CE-4E99-A40F-4B1E122D00D0").then(function(s) { - state.ancs.primary=s; - return s.getCharacteristic("9FBF120D-6301-42D9-8C58-25E699A21DBD"); - }).then(function(c) { - state.ancs.notify=c; - return state.ancs.primary.getCharacteristic("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9"); - }).then(function(c) { - state.ancs.control=c; - return state.ancs.primary.getCharacteristic("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB"); - }).then(function(c) { - state.ancs.data =c; - drawIcon(3);//got remote services - state.ancs.notify.on('characteristicvaluechanged', function(ev) { - getnotify(ev.target.value); - }); - state.ancs.data.on('characteristicvaluechanged', function(e) { - state.store(e.target.value.buffer); - var inds = state.gotmsg(); - if (inds) printmsg(state.buf,inds); - }); - state.ancs.notify.startNotifications().then(function(){ - state.ancs.data.startNotifications().then(function(){ - drawIcon(4); //ready for messages - }); - }); - }).catch(function(e){ - Terminal.println("ERROR "+e); - }); - } - - function wordwrap(s){ - var txt = s.split("\n"); - var MAXCHARS = 18; - for (var i = 0; i < txt.length; i++) { - txt[i] = txt[i].trim(); - var l = txt[i]; - if (l.length > MAXCHARS) { - var p = MAXCHARS; - while (p > MAXCHARS - 8 && !" \t-_".includes(l[p])) - p--; - if (p == MAXCHARS - 8) p = MAXCHARS; - txt[i] = l.substr(0, p); - txt.splice(i + 1, 0, l.substr(p)); - } - } - return txt.join("\n"); - } - - - var buzzing =false; - var screentimeout = undefined; - var inalert = false; - - function release_screen(){ - screentimeout= setTimeout(() => { - SCREENACCESS.release(); - screentimeout = undefined; - inalert=false; - next_notify(); - }, 500); - } - - function printmsg(buf,inds){ - - function send_action(tf){ - var bb = new Uint8Array(6); - var v = DataView(bb.buffer); - v.setUint8(0,2); - v.setUint32(1,state.current.uid,true); - v.setUint8(5,tf?0:1 ); - state.ancs.control.writeValue(bb).then(release_screen); - } - - if (state.msgTO) clearTimeout(state.msgTO); - var title=""; - for (var i=8;i<8+inds.tlen; ++i) title+=String.fromCharCode(buf[i]); - var message = ""; - for (var j=11+inds.tlen;j<11+inds.tlen+inds.mlen;++j) { - message+=String.fromCharCode(buf[j]); - } - message = wordwrap(message); - //we may already be displaying a prompt, so clear it - E.showPrompt(); - if (screentimeout) clearTimeout(screentimeout); - if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { - Bangle.setLCDPower(true); - } - SCREENACCESS.request(); - if (!buzzing && !(require('Storage').readJSON('setting.json',1)||{}).quiet){ - buzzing=true; - Bangle.buzz(500).then(()=>{buzzing=false;}); - } - if (state.current.cat!=1){ - E.showAlert(message,title).then(send_action.bind(null,false)); - } else { - E.showPrompt(message,{title:title,buttons:{"Accept":true,"Cancel":false}}).then(send_action); - } - } - - var notifyTO; - function getnotify(d){ - var eid = d.getUint8(0); - var ct = d.getUint8(2); - var id = d.getUint32(4,true); - if (eid>1) return; - if (notifyTO) clearTimeout(notifyTO); - if(!CATEGORY.includes(ct)) return; - var len = state.notqueue.length; - if (ct == 1) { // it's a call so pre-empt - if (inalert) {state.notqueue.push(state.current); inalert=false;} - state.notqueue.push({cat:ct, uid:id}); - } else if (len<16) - state.notqueue[len] = {cat:ct, uid:id}; - notifyTO = setTimeout(next_notify,1000); - } - - function next_notify(){ - if(state.notqueue.length==0 || inalert) return; - inalert=true; - state.current = state.notqueue.pop(); - var v = DataView(state.com.buffer); - if (state.current.cat==6) v.setUint8(8,2); else v.setUint8(8,3);//get email title - v.setUint32(1,state.current.uid,true); - state.inp=0; - state.ancs.control.writeValue(state.com).then(function(){ - state.msgTO=setTimeout(()=>{ - inalert=false; - state.msgTO=undefined; - next_notify(); - },1000); - }); - } - - var stage = 5; - //grey, pink, lightblue, yellow, green - function draw(){ - var colors = new Uint16Array([0xc618,0xf818,0x3ff,0xffe0,0x07e0,0x0000]); - var img = E.toArrayBuffer(atob("GBgBAAAABAAADgAAHwAAPwAAf4AAP4AAP4AAP4AAHwAAH4AAD8AAB+AAA/AAAfgAAf3gAH/4AD/8AB/+AA/8AAf4AAHwAAAgAAAA")); - g.setColor(colors[stage]); - g.drawImage(img,this.x,this.y); - } - - WIDGETS["ancs"] ={area:"tl", width:24,draw:draw}; - - function drawIcon(id){ - stage = id; - WIDGETS["ancs"].draw(); - } - - if (ENABLED && typeof SCREENACCESS!='undefined') { - stage = 0; - NRF.setServices(undefined,{uart:false}); - NRF.sleep(); - NRF.wake(); - advert(); - } - - })(); - - - \ No newline at end of file diff --git a/apps/widancs/ancs.min.js b/apps/widancs/ancs.min.js deleted file mode 100644 index 8ccf58e61..000000000 --- a/apps/widancs/ancs.min.js +++ /dev/null @@ -1,10 +0,0 @@ -(function(){function t(a){function e(){k(0);delete b.gatt;delete b.ancs;NRF.getGattforCentralServer||NRF.disconnect();setTimeout(function(){NRF.wake()},500)}var d;b.gatt=a;k(1);b.gatt.device.on("gattserverdisconnected",function(a){d&&clearInterval(d);c&&clearInterval(c);e()});E.on("kill",function(){b.gatt.disconnect().then(function(){NRF.sleep()})});NRF.setSecurity({passkey:"123456",mitm:1,display:1});var c=setTimeout(function(){d&&clearInterval(d);b.gatt.disconnect().then(e)},1E4);b.gatt.startBonding().then(function(){d= - setInterval(function(){var a=b.gatt.getSecurityStatus();a.connected?a.connected&&a.encrypted&&(clearInterval(d),clearTimeout(c),k(2),v()):(clearInterval(d),clearTimeout(c))},1E3)})["catch"](function(a){Terminal.println("ERROR "+a)})}function v(){b.ancs={primary:null,notify:null,control:null,data:null};b.gatt.getPrimaryService("7905F431-B5CE-4E99-A40F-4B1E122D00D0").then(function(a){b.ancs.primary=a;return a.getCharacteristic("9FBF120D-6301-42D9-8C58-25E699A21DBD")}).then(function(a){b.ancs.notify= - a;return b.ancs.primary.getCharacteristic("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9")}).then(function(a){b.ancs.control=a;return b.ancs.primary.getCharacteristic("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB")}).then(function(a){b.ancs.data=a;k(3);b.ancs.notify.on("characteristicvaluechanged",function(a){var e=a.target.value,c=e.getUint8(0);a=e.getUint8(2);e=e.getUint32(4,!0);1c&&(b.notqueue[c]={cat:a,uid:e}),m=setTimeout(n,1E3)))});b.ancs.data.on("characteristicvaluechanged",function(a){b.store(a.target.value.buffer);(a=b.gotmsg())&&x(b.buf,a)});b.ancs.notify.startNotifications().then(function(){b.ancs.data.startNotifications().then(function(){k(4)})})})["catch"](function(a){Terminal.println("ERROR "+a)})}function y(a){a=a.split("\n");for(var b=0;b=b+a.length&&(this.buf.set(a,b),this.inp+=a.length)},gotmsg:function(){var a=this.inp,b= - DataView(this.buf.buffer);if(8>a)return null;var d=b.getUint16(6,!0);if(a{return v!=n;}); - } - const menu = { - '': { 'title': 'Set Categories' } - }; - for (var i=0; iv?'Yes':'No', - onchange:setcat.bind(null,i) - }; - menu['< Back'] = ()=>{save(); showMain();}; - return E.showMenu(menu); - } - - function showMain(){ - return E.showMenu({ - 'Enable ANCS': { - value: s.enabled, - format: () => (s.enabled ? 'Yes' : 'No'), - onchange: () => { - s.enabled = !s.enabled; - save(); - }, - }, - 'Set Category':setcategory, - '< Back': back, - }); - } - - showMain(); -}); \ No newline at end of file diff --git a/apps/widancs/widget.png b/apps/widancs/widget.png deleted file mode 100644 index c6f57cc1e..000000000 Binary files a/apps/widancs/widget.png and /dev/null differ diff --git a/apps/widancs/widget_pic.jpg b/apps/widancs/widget_pic.jpg deleted file mode 100644 index 68b04f2e9..000000000 Binary files a/apps/widancs/widget_pic.jpg and /dev/null differ 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/metadata.json b/apps/widbars/metadata.json new file mode 100644 index 000000000..e8d52c90a --- /dev/null +++ b/apps/widbars/metadata.json @@ -0,0 +1,15 @@ +{ + "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"} +] +} 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/widbat/metadata.json b/apps/widbat/metadata.json new file mode 100644 index 000000000..0f040396f --- /dev/null +++ b/apps/widbat/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widbat", + "name": "Battery Level Widget", + "version": "0.09", + "description": "Show the current battery level and charging status in the top right of the clock", + "icon": "widget.png", + "type": "widget", + "tags": "widget,battery", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbat.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widbata/ChangeLog b/apps/widbata/ChangeLog new file mode 100644 index 000000000..f23705341 --- /dev/null +++ b/apps/widbata/ChangeLog @@ -0,0 +1,3 @@ +0.01: Created +0.02: Set sort order to -10 so always display in right hand corner +0.03: Set sort order from the code 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/metadata.json b/apps/widbata/metadata.json new file mode 100644 index 000000000..d8149d13f --- /dev/null +++ b/apps/widbata/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widbata", + "name": "Battery Level Widget (Themed)", + "shortName":"Battery Theme", + "icon": "widbata.png", + "screenshots": [{"url":"screenshot_widbata_1.png"}], + "version":"0.03", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "description": "Shows the current battery level status in the top right using the clocks colour theme", + "tags": "widget,battery", + "storage": [ + {"name":"widbata.wid.js","url":"widbata.wid.js"} + ] +} 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..1de4c2fe9 --- /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",sortorder:-10,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..e70093659 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -10,3 +10,6 @@ 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 +0.15: Added option to always display the icon when charging (useful if 'hide if charge greater than' is enabled) +0.16: Increase screen update rate when charging diff --git a/apps/widbatpc/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/metadata.json b/apps/widbatpc/metadata.json new file mode 100644 index 000000000..7da4e3e0c --- /dev/null +++ b/apps/widbatpc/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "widbatpc", + "name": "Battery Level Widget (with percentage)", + "shortName": "Battery Widget", + "version": "0.16", + "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", + "icon": "widget.png", + "type": "widget", + "tags": "widget,battery", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"widbatpc.full.jpg"},{"url":"widbatpc.part.jpg"}], + "storage": [ + {"name":"widbatpc.wid.js","url":"widget.js"}, + {"name":"widbatpc.settings.js","url":"settings.js"} + ], + "data": [{"name":"widbatpc.json"}] +} diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index b7a5db9e6..b45fc6749 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -13,6 +13,7 @@ 'fillbar': false, 'charger': true, 'hideifmorethan': 100, + 'alwaysoncharge': false, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -68,6 +69,11 @@ format: x => x+"%", onchange: save('hideifmorethan'), }, + 'Show on charge': { // Not sure if this is readable enough in the 'big' menu + value: s.alwaysoncharge, + format: onOffFormat, + onchange: save('alwaysoncharge'), + }, } E.showMenu(menu) }) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index caecf8ae4..529923386 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,6 +1,9 @@ (function(){ + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + let COLORS = {}; - + if (process.env.HWVERSION == 1) { COLORS = { 'white': -1, // White @@ -17,31 +20,32 @@ 'high': "#0f0", // Green 'ok': "#ff0", // Orange 'low': "#f00", // Red - }; + }; } - const SETTINGS_FILE = 'widbatpc.json' + const SETTINGS_FILE = 'widbatpc.json'; - let settings + let settings; function loadSettings() { - settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; const DEFAULTS = { 'color': 'By Level', 'percentage': true, 'charger': true, 'hideifmorethan': 100, + 'alwaysoncharge': false, }; Object.keys(DEFAULTS).forEach(k=>{ - if (settings[k]===undefined) settings[k]=DEFAULTS[k] + if (settings[k]===undefined) settings[k]=DEFAULTS[k]; }); } function setting(key) { - if (!settings) { loadSettings() } + if (!settings) { loadSettings(); } return settings[key]; } const levelColor = (l) => { // "charging" is very bright -> percentage is hard to read, "high" is ok(ish) - const green = setting('percentage') ? COLORS.high : COLORS.charging + const green = setting('percentage') ? COLORS.high : COLORS.charging; switch (setting('color')) { case 'Monochrome': return COLORS.white; // no chance of reading the percentage here :-( case 'Green': return green; @@ -58,41 +62,46 @@ if (l >= 15) return COLORS.ok; return COLORS.low; } - } + }; const chargerColor = () => { - return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging - } + return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging; + }; + // sets width, returns true if it changed function setWidth() { var w = 40; if (Bangle.isCharging() && setting('charger')) w += 16; - if (E.getBattery() > setting('hideifmorethan')) + if (E.getBattery() > setting('hideifmorethan')) { w = 0; + if( Bangle.isCharging() && setting('alwaysoncharge') === true) + w = 56; + } var changed = WIDGETS["batpc"].width != w; WIDGETS["batpc"].width = w; return changed; } + function draw() { // if hidden, don't draw if (!WIDGETS["batpc"].width) return; // 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); @@ -102,11 +111,11 @@ if (!setting('percentage')) { return; } - let gfx = g + let gfx = g; if (setting('color') === 'Monochrome') { // draw text inverted on battery level gfx = Graphics.createCallback(g.getWidth(),g.getHeight(), 1, - (x,y) => {g.setPixel(x,y,x<=xl?0:-1)}) + (x,y) => {g.setPixel(x,y,x<=xl?0:-1);}); } gfx.setFontAlign(-1,-1); if (l >= 100) { @@ -118,19 +127,24 @@ gfx.drawString(l, x + 6, y + 4); } } + // reload widget, e.g. when settings have changed function reload() { - loadSettings() + loadSettings(); // need to redraw all widgets, because changing the "charger" setting // can affect the width and mess with the whole widget layout - setWidth() + setWidth(); g.clear(); Bangle.drawWidgets(); } + // update widget - redraw just widget, or all widgets if size changed function update() { if (setWidth()) Bangle.drawWidgets(); else WIDGETS["batpc"].draw(); + + if (Bangle.isCharging()) changeInterval(id, intervalHigh); + else changeInterval(id, intervalLow); } Bangle.on('charging',function(charging) { @@ -138,20 +152,13 @@ update(); g.flip(); }); - var batteryInterval; + Bangle.on('lcdPower', function(on) { - if (on) { - update(); - // refresh once a minute if LCD on - if (!batteryInterval) - batteryInterval = setInterval(update, 60000); - } else { - if (batteryInterval) { - clearInterval(batteryInterval); - batteryInterval = undefined; - } - } + if (on) update(); }); + + var id = setInterval(()=>WIDGETS["batpc"].draw(), intervalLow); + WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; setWidth(); -})() +})(); diff --git a/apps/widbatv/metadata.json b/apps/widbatv/metadata.json new file mode 100644 index 000000000..37cf6197b --- /dev/null +++ b/apps/widbatv/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widbatv", + "name": "Battery Level Widget (Vertical)", + "version": "0.01", + "description": "Slim, vertical battery widget that only takes up 14px", + "icon": "widget.png", + "type": "widget", + "tags": "widget,battery", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbatv.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widbatwarn/metadata.json b/apps/widbatwarn/metadata.json new file mode 100644 index 000000000..959eeca08 --- /dev/null +++ b/apps/widbatwarn/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widbatwarn", + "name": "Battery Warning", + "shortName": "Battery Warning", + "version": "0.02", + "description": "Show a warning when the battery runs low.", + "icon": "widget.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "widget", + "tags": "tool,battery", + "supports": ["BANGLEJS"], + "dependencies": {"notify":"type"}, + "readme": "README.md", + "storage": [ + {"name":"widbatwarn.wid.js","url":"widget.js"}, + {"name":"widbatwarn.settings.js","url":"settings.js"} + ], + "data": [{"name":"widbatwarn.json"}] +} diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index 7aa96ce5c..4c2132122 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -4,3 +4,4 @@ 0.05: Make Bluetooth widget thinner, and when on a bright theme use light grey for disabled color 0.06: Tweaking colors for dark/light themes and low bpp screens 0.07: Memory usage improvements +0.08: Disable LCD on, on bluetooth status change diff --git a/apps/widbt/metadata.json b/apps/widbt/metadata.json new file mode 100644 index 000000000..e2d5082a5 --- /dev/null +++ b/apps/widbt/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widbt", + "name": "Bluetooth Widget", + "version": "0.08", + "description": "Show the current Bluetooth connection status in the top right of the clock", + "icon": "widget.png", + "type": "widget", + "tags": "widget,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbt.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index 88be3d5c9..c7ef8c0ad 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -7,7 +7,6 @@ WIDGETS["bluetooth"]={area:"tr",width:15,draw:function() { g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y); },changed:function() { WIDGETS["bluetooth"].draw(); - Bangle.setLCDPower(1); // turn screen on }}; NRF.on('connect',WIDGETS["bluetooth"].changed); NRF.on('disconnect',WIDGETS["bluetooth"].changed); diff --git a/apps/widchime/metadata.json b/apps/widchime/metadata.json new file mode 100644 index 000000000..db75e76ac --- /dev/null +++ b/apps/widchime/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widchime", + "name": "Hour Chime", + "version": "0.02", + "description": "Buzz or beep on every whole hour.", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widchime.wid.js","url":"widget.js"}, + {"name":"widchime.settings.js","url":"settings.js"} + ], + "data": [{"name":"widchime.json"}] +} diff --git a/apps/widclk/metadata.json b/apps/widclk/metadata.json new file mode 100644 index 000000000..6996f4080 --- /dev/null +++ b/apps/widclk/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widclk", + "name": "Digital clock widget", + "version": "0.06", + "description": "A simple digital clock widget", + "icon": "widget.png", + "type": "widget", + "tags": "widget,clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widclk.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widclkbttm/Changelog b/apps/widclkbttm/ChangeLog similarity index 100% rename from apps/widclkbttm/Changelog rename to apps/widclkbttm/ChangeLog diff --git a/apps/widclkbttm/metadata.json b/apps/widclkbttm/metadata.json new file mode 100644 index 000000000..9e92f7c46 --- /dev/null +++ b/apps/widclkbttm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widclkbttm", + "name": "Digital clock (Bottom) widget", + "shortName": "Digital clock Bottom Widget", + "version": "0.03", + "description": "Displays time in the bottom area.", + "icon": "widclkbttm.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widclkbttm.wid.js","url":"widclkbttm.wid.js"} + ] +} diff --git a/apps/widcom/metadata.json b/apps/widcom/metadata.json new file mode 100644 index 000000000..4f7473a70 --- /dev/null +++ b/apps/widcom/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widcom", + "name": "Compass Widget", + "version": "0.02", + "description": "Tiny widget to show the power on/off status of the Compass", + "icon": "widget.png", + "type": "widget", + "tags": "widget,compass", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widcom.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widgps/metadata.json b/apps/widgps/metadata.json new file mode 100644 index 000000000..87790a895 --- /dev/null +++ b/apps/widgps/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widgps", + "name": "GPS Widget", + "version": "0.03", + "description": "Tiny widget to show the power on/off status of the GPS", + "icon": "widget.png", + "type": "widget", + "tags": "widget,gps", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widgps.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widhrm/metadata.json b/apps/widhrm/metadata.json new file mode 100644 index 000000000..e566142d2 --- /dev/null +++ b/apps/widhrm/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widhrm", + "name": "Simple Heart Rate widget", + "version": "0.05", + "description": "When the screen is on, the widget turns on the heart rate monitor and displays the current heart rate (or last known in grey). For this to work well you'll need at least a 15 second LCD Timeout.", + "icon": "widget.png", + "type": "widget", + "tags": "health,widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widhrm.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widhrt/metadata.json b/apps/widhrt/metadata.json new file mode 100644 index 000000000..a8f030157 --- /dev/null +++ b/apps/widhrt/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widhrt", + "name": "HRM Widget", + "version": "0.03", + "description": "Tiny widget to show the power on/off status of the Heart Rate Monitor", + "icon": "widget.png", + "type": "widget", + "tags": "widget,hrm", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widhrt.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widhwt/ChangeLog b/apps/widhwt/ChangeLog index 4c21f3ace..76c4fcec9 100644 --- a/apps/widhwt/ChangeLog +++ b/apps/widhwt/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget! +0.02: Ported to Bangle.js2 diff --git a/apps/widhwt/app.js b/apps/widhwt/app.js new file mode 100644 index 000000000..f18e78643 --- /dev/null +++ b/apps/widhwt/app.js @@ -0,0 +1,13 @@ +// Replace the "Loading..." box +// with our own message +g.clearRect(38, 68, 138, 108); +g.drawRect(38, 68, 138, 108); +g.setFontVector(13); +g.setFontAlign(0, 0, 0); +g.drawString("Wash...", g.getWidth()/2, g.getHeight()/2); + +Bangle.buzz(); +setTimeout(() => { + Bangle.buzz(1E3, 1); + setTimeout(() => load(), 2E3); +}, 35E3); diff --git a/apps/widhwt/metadata.json b/apps/widhwt/metadata.json new file mode 100644 index 000000000..375ddf498 --- /dev/null +++ b/apps/widhwt/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widhwt", + "name": "Hand Wash Timer", + "version": "0.02", + "description": "On Bangle.js 1 swipe your wrist over the watch face to start your personal Bangle.js 1 hand wash timer. On Bangle.js2 the Pattern Launcher is recommended to start the timer. Start washing after the short buzz and stop after the long buzz 35sec. later.", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tool", + "allow_emulator": true, + "screenshots": [{"url":"wash-hand-timer-screenshot.png"}], + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"widhwt.app.js","url":"app.js"}, + {"name":"widhwt.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widhwt/wash-hand-timer-screenshot.png b/apps/widhwt/wash-hand-timer-screenshot.png new file mode 100644 index 000000000..05859cd7a Binary files /dev/null and b/apps/widhwt/wash-hand-timer-screenshot.png differ diff --git a/apps/widhwt/widget.js b/apps/widhwt/widget.js index d178a5b5d..5e1f95a41 100644 --- a/apps/widhwt/widget.js +++ b/apps/widhwt/widget.js @@ -6,9 +6,7 @@ g.reset().setColor(color).drawImage(require("heatshrink").decompress(atob("jEYwIKHgwCBhwCBh4CEggPCkACBmAXDBwVZ+EB+F4gEsjl8EgMP+EChk/gEMh+ehkA+YIBxwxBnF/4HggH/wEAj0AA==")), this.x + 1, 0); } - WIDGETS["widhwt"] = { area: "tr", width: 26, draw: draw }; - - Bangle.on('swipe', function() { + function startTimer() { color = 0x41f; Bangle.buzz(); Bangle.drawWidgets(); @@ -17,6 +15,14 @@ Bangle.buzz(1E3, 1); Bangle.drawWidgets(); }, 35E3); + } - }); + if (process.env.HWVERSION == 1) { + WIDGETS["widhwt"] = { + area: "tr", + width: 26, + draw: draw, + }; + Bangle.on('swipe', startTimer); + } })(); diff --git a/apps/widid/metadata.json b/apps/widid/metadata.json new file mode 100644 index 000000000..e8831604c --- /dev/null +++ b/apps/widid/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widid", + "name": "Bluetooth ID Widget", + "version": "0.03", + "description": "Display the last two tuple of your Bangle.js MAC address in the widget section. This is useful for figuring out which Bangle.js to connect to if you have more than one Bangle.js!", + "icon": "widget.png", + "type": "widget", + "tags": "widget,address,mac", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widid.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widlock/ChangeLog b/apps/widlock/ChangeLog index 3b1436feb..bb84c2d44 100644 --- a/apps/widlock/ChangeLog +++ b/apps/widlock/ChangeLog @@ -1,3 +1,6 @@ 0.01: First commit 0.02: Handle new firmwares with 'lock' event 0.03: Don't try to be fancy - just bail out on firmwares without a lock event +0.04: Set sortorder to -1 so that widget always takes up the furthest left position +0.05: Set sortorder to -10 so that others can take -1 etc +0.06: Set sortorder to -10 in widget code diff --git a/apps/widlock/metadata.json b/apps/widlock/metadata.json new file mode 100644 index 000000000..8635a5434 --- /dev/null +++ b/apps/widlock/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widlock", + "name": "Lock Widget", + "version": "0.06", + "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", + "icon": "widget.png", + "type": "widget", + "tags": "widget,lock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widlock.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widlock/widget.js b/apps/widlock/widget.js index 4c59e6575..592361cd9 100644 --- a/apps/widlock/widget.js +++ b/apps/widlock/widget.js @@ -4,7 +4,7 @@ WIDGETS["lock"].width = Bangle.isLocked()?16:0; Bangle.drawWidgets(); }); - WIDGETS["lock"]={area:"tl",width:Bangle.isLocked()?16:0,draw:function(w) { + WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:0,draw:function(w) { if (Bangle.isLocked()) g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x+1, w.y+4); }}; diff --git a/apps/widmp/metadata.json b/apps/widmp/metadata.json new file mode 100644 index 000000000..9c7506a09 --- /dev/null +++ b/apps/widmp/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widmp", + "name": "Moon Phase Widget", + "version": "0.02", + "description": "Display the current moon phase in blueish for the northern hemisphere in eight phases", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widmp.wid.js","url":"widget.js"} + ] +} 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/metadata.json b/apps/widmpsh/metadata.json new file mode 100644 index 000000000..e6ecf9e73 --- /dev/null +++ b/apps/widmpsh/metadata.json @@ -0,0 +1,13 @@ +{ + "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"} + ] +} 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/widnceu/metadata.json b/apps/widnceu/metadata.json new file mode 100644 index 000000000..c8e41f4c8 --- /dev/null +++ b/apps/widnceu/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widnceu", + "name": "NCEU Logo Widget", + "version": "0.02", + "description": "Show the NodeConf EU logo in the top left", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"widnceu.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widncr/metadata.json b/apps/widncr/metadata.json new file mode 100644 index 000000000..d710fb853 --- /dev/null +++ b/apps/widncr/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widncr", + "name": "NCR Logo Widget", + "version": "0.01", + "description": "Show the NodeConf Remote logo in the top left", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"widncr.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widpa/ChangeLog b/apps/widpa/ChangeLog new file mode 100644 index 000000000..4a8809923 --- /dev/null +++ b/apps/widpa/ChangeLog @@ -0,0 +1,4 @@ +0.01: First release +0.02: Size widget after step count is reset +0.03: set sortorder to -1 +0.04: set sortorder through code diff --git a/apps/widpa/README.md b/apps/widpa/README.md new file mode 100644 index 000000000..92fbb8c11 --- /dev/null +++ b/apps/widpa/README.md @@ -0,0 +1,16 @@ +# Simple Pedometer Widget + +*Displays the current step count from `Bangle.getHealthStatus("day").steps` in (6x8,2) font, Requires firmware v2.11.21 or later* + +* Designed to be small, minimal, does one thing well, no settings +* Supports Bangle 1 and Bangle 2 + +## Notes + +* Requires firmware v2.11.21 or later +* `Bangle.getHealthStatus("day").steps` is reset to zero if you reboot your watch with a long BTN Press +* The step count displayed may be a few steps more than that reported by widpedpm as widpedom may not always be loaded. + +![](screenshot_widpa.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/widpa/metadata.json b/apps/widpa/metadata.json new file mode 100644 index 000000000..95a8a8a9c --- /dev/null +++ b/apps/widpa/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widpa", + "name": "Simple Pedometer", + "shortName":"Simple Pedometer", + "icon": "screenshot_widpa.png", + "screenshots": [{"url":"screenshot_widpa.png"}], + "version":"0.04", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in 12x16 font, requires firmware v2.11.21 or later", + "tags": "widget,battery", + "storage": [ + {"name":"widpa.wid.js","url":"widpa.wid.js"} + ] +} diff --git a/apps/widpa/screenshot_widpa.png b/apps/widpa/screenshot_widpa.png new file mode 100644 index 000000000..e33550f50 Binary files /dev/null and b/apps/widpa/screenshot_widpa.png differ diff --git a/apps/widpa/widpa.wid.js b/apps/widpa/widpa.wid.js new file mode 100644 index 000000000..8cde29bc1 --- /dev/null +++ b/apps/widpa/widpa.wid.js @@ -0,0 +1,17 @@ +Bangle.on('step', function(s) { WIDGETS["widpa"].draw(); }); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["widpa"].draw(); +}); +WIDGETS["widpa"]={area:"tl",sortorder:-1,width:13,draw:function() { + if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off + var steps = Bangle.getHealthStatus("day").steps; + var w = 1 + (steps.toString().length)*12; + if (w != this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); + g.setColor(g.theme.fg); + g.setFont('6x8',2); + g.setFontAlign(-1, 0); + g.drawString(steps, this.x, this.y + 12); +}}; diff --git a/apps/widpb/ChangeLog b/apps/widpb/ChangeLog new file mode 100644 index 000000000..8a4a48b56 --- /dev/null +++ b/apps/widpb/ChangeLog @@ -0,0 +1,5 @@ +0.01: First release +0.02: Fixed widget id to wibpb, Size widget after step count is reset +0.03: Fixed widget id in onStep() come on get it right! +0.04: set sortorder to -1 +0.05: set sortorder through code diff --git a/apps/widpb/README.md b/apps/widpb/README.md new file mode 100644 index 000000000..bec127b6b --- /dev/null +++ b/apps/widpb/README.md @@ -0,0 +1,17 @@ +# Lato Pedometer Widget + +*Displays the current step count from `Bangle.getHealthStatus("day").steps` in the Lato font, Requires firmware v2.11.21 or later* + +* Designed to be minimal, does one thing well, no settings +* Supports Bangle 1 and Bangle 2 + +## Notes + +* Requires firmware v2.11.21 or later +* Uses the Lato custom font, so memory footprint is 500 bytes larger than 'Simple Pedometer Widget' +* `Bangle.getHealthStatus("day").steps` is reset to zero if you reboot your watch with a long BTN Press +* The step count displayed may be a few steps more than that reported by widpedpm as widpedom may not always be loaded. + +![](screenshot_widpb.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/widpb/metadata.json b/apps/widpb/metadata.json new file mode 100644 index 000000000..0c2ed747a --- /dev/null +++ b/apps/widpb/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widpb", + "name": "Lato Pedometer", + "shortName":"Lato Pedometer", + "icon": "screenshot_widpb.png", + "screenshots": [{"url":"screenshot_widpb.png"}], + "version":"0.05", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in the Lato font, requires firmware v2.11.21 or later", + "tags": "widget,battery", + "storage": [ + {"name":"widpb.wid.js","url":"widpb.wid.js"} + ] + } diff --git a/apps/widpb/screenshot_widpb.png b/apps/widpb/screenshot_widpb.png new file mode 100644 index 000000000..af1222e7e Binary files /dev/null and b/apps/widpb/screenshot_widpb.png differ diff --git a/apps/widpb/widpb.wid.js b/apps/widpb/widpb.wid.js new file mode 100644 index 000000000..ceebb9937 --- /dev/null +++ b/apps/widpb/widpb.wid.js @@ -0,0 +1,20 @@ +// on.step version +Bangle.on('step', function(s) { WIDGETS["widpb"].draw(); }); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["widpb"].draw(); +}); +WIDGETS["widpb"]={area:"tl",sortorder:-1,width:13,draw:function() { + if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off + var steps = Bangle.getHealthStatus("day").steps; + var w = 1 + (steps.toString().length)*12; + if (w != this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;} + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background + g.setColor(g.theme.fg); + // Lato from fonts.google.com, Actual height 17 (17 - 1), Numeric only + const scale = 1; + g.setFontCustom(atob("AAAAABwAAOAAAgAAHAADwAD4AB8AB8AA+AAeAADAAAAOAAP+AH/8B4DwMAGBgAwMAGBgAwOAOA//gD/4AD4AAAAAAAABgAAcAwDAGAwAwP/+B//wAAGAAAwAAGAAAAAAAAIAwHgOA4DwMA+BgOwMDmBg4wOeGA/gwDwGAAAAAAAAAGAHA8A4DwMAGBhAwMMGBjgwOcOA+/gDj4AAAAABgAAcAAHgADsAA5gAOMAHBgBwMAP/+B//wABgAAMAAAAAAAgD4OB/AwOYGBjAwMYGBjBwMe8Bh/AIHwAAAAAAAAAfAAP8AHxwB8GAdgwPMGBxgwMOOAB/gAH4AAAAAAABgAAMAABgAwMAeBgPgMHwBj4AN8AB+AAPAABAAAAAAAMfAH38B/xwMcGBhgwMMGBjgwP+OA+/gDj4AAAAAAAAOAAH4AA/gQMMGBgzwME8BhvAOPgA/4AD8AAEAAAAAAGAwA4OAHBwAAA="), 46, atob("BAgMDAwMDAwMDAwMBQ=="), 21+(scale<<8)+(1<<16)); + g.setFontAlign(-1, 0); + g.drawString(steps, this.x, this.y + 12); +}}; diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index 2f36c7647..54f6b203b 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -19,3 +19,6 @@ Stop goal drawing outside widget area Fix issue with widget overwrite in large font mode Memory usage enhancements +0.20: Fix issue where step count would randomly reset +0.21: Memory usage improvements, fix widget initial width (fix #1170) +0.22: Fix 'stps' regression for 0.21 (fix #1233) diff --git a/apps/widpedom/metadata.json b/apps/widpedom/metadata.json new file mode 100644 index 000000000..f49d3ba5b --- /dev/null +++ b/apps/widpedom/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widpedom", + "name": "Pedometer widget", + "version": "0.22", + "description": "Daily pedometer widget", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widpedom.wid.js","url":"widget.js"}, + {"name":"widpedom.settings.js","url":"settings.js"} + ] +} diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 3c861cf54..cc7fdb579 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -1,5 +1,4 @@ (() => { - const PEDOMFILE = "wpedom.json" // Last time Bangle.on('step' was called let lastUpdate = new Date(); // Last step count when Bangle.on('step' was called @@ -8,19 +7,14 @@ let settings; function loadSettings() { - const d = require('Storage').readJSON(PEDOMFILE, 1) || {}; - settings = d.settings || {}; - } - - function setting(key) { - if (!settings) { loadSettings() } - const DEFAULTS = { + const d = require('Storage').readJSON("wpedom.json", 1) || {}; + settings = Object.assign({ 'goal': 10000, 'progress': false, 'large': false, 'hide': false - } - return (key in settings) ? settings[key] : DEFAULTS[key]; + }, d.settings || {}); + return d; } Bangle.on('step', stepCount => { @@ -31,10 +25,10 @@ if (lastUpdate.getDate() == date.getDate()){ stp_today += steps; } else { - // TODO: could save this to PEDOMFILE for lastUpdate's day? + // TODO: could save this to "wpedom.json" for lastUpdate's day? stp_today = steps; } - if (stp_today === setting('goal') + if (stp_today === settings.goal && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { let b = 3, buzz = () => { if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) @@ -51,29 +45,31 @@ }); // When unloading, save state E.on('kill', () => { - if (!settings) { loadSettings() } - let d = { - lastUpdate : lastUpdate.toISOString(), + require("Storage").writeJSON("wpedom.json",{ + lastUpdate : lastUpdate.valueOf(), stepsToday : stp_today, settings : settings, - }; - require("Storage").write(PEDOMFILE,d); + }); }); // add your widget - WIDGETS["wpedom"]={area:"tl",width:26, - redraw:function() { // work out the width, and queue a full redraw if needed + WIDGETS["wpedom"]={area:"tl",width:0, + getWidth:function() { let stps = stp_today.toString(); let newWidth = 24; - if (setting('hide')) + if (settings.hide) newWidth = 0; else { - if (setting('large')) { + if (settings.large) { newWidth = 12 * stps.length + 3; - if (setting('progress')) + if (settings.progress) newWidth += 24; } } + return newWidth; + }, + redraw:function() { // work out the width, and queue a full redraw if needed + let newWidth = this.getWidth(); if (newWidth!=this.width) { // width has changed, re-layout all widgets this.width = newWidth; @@ -84,14 +80,14 @@ } }, draw:function() { - if (setting('hide')) return; + if (settings.hide) return; if (stp_today > 99999) stp_today = stp_today % 100000; // cap to five digits + comma = 6 characters let stps = stp_today.toString(); g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background - if (setting('progress')) { + if (settings.progress) { const width = 23, half = 11; - const goal = setting('goal'), left = Math.max(goal-stps,0); + const goal = settings.goal, left = Math.max(goal-stps,0); // blue or dark green g.setColor(left ? "#08f" : "#080").fillCircle(this.x + half, this.y + half, half); if (left) { @@ -113,10 +109,10 @@ } g.reset(); } - if (setting('large')) { + if (settings.large) { g.setFont("6x8",2); g.setFontAlign(-1, 0); - g.drawString(stps, this.x + (setting('progress')?28:4), this.y + 12); + g.drawString(stps, this.x + (settings.progress?28:4), this.y + 12); } else { let w = 24; if (stps.length > 3){ @@ -137,11 +133,12 @@ getSteps:()=>stp_today }; // Load data at startup - let pedomData = require("Storage").readJSON(PEDOMFILE,1); + let pedomData = loadSettings(); if (pedomData) { if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); stp_today = pedomData.stepsToday|0; delete pedomData; } + WIDGETS["wpedom"].width = WIDGETS["wpedom"].getWidth(); })() diff --git a/apps/widram/metadata.json b/apps/widram/metadata.json new file mode 100644 index 000000000..8ea309ebf --- /dev/null +++ b/apps/widram/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widram", + "name": "RAM Widget", + "shortName": "RAM Widget", + "version": "0.01", + "description": "Display your Bangle's available RAM percentage in a widget", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widram.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widtbat/metadata.json b/apps/widtbat/metadata.json new file mode 100644 index 000000000..7e882d247 --- /dev/null +++ b/apps/widtbat/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widtbat", + "name": "Tiny Battery Widget", + "version": "0.02", + "description": "Tiny blueish battery widget, vibs and changes level color when charging", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widtbat.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widver/metadata.json b/apps/widver/metadata.json new file mode 100644 index 000000000..061844a36 --- /dev/null +++ b/apps/widver/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widver", + "name": "Firmware Version Widget", + "version": "0.03", + "description": "Display the version of the installed firmware in the top widget section.", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widver.wid.js","url":"widget.js"} + ] +} 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/metadata.json b/apps/widviz/metadata.json new file mode 100644 index 000000000..ba9cf793b --- /dev/null +++ b/apps/widviz/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widviz", + "name": "Widget Visibility Widget", + "shortName": "Viz Widget", + "version": "0.03", + "description": "Swipe left to hide top bar widgets, swipe right to redisplay.", + "icon": "eye.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widviz.wid.js","url":"widget.js"} + ] +} 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/apps/widviztime/README.md b/apps/widviztime/README.md new file mode 100644 index 000000000..73e24b658 --- /dev/null +++ b/apps/widviztime/README.md @@ -0,0 +1,8 @@ +# Widget Autohide Widget +This widget is forked from the "Widget Visibility Widget" +It should make widgets completely hidden (except for 4 seconds after the watch is unlocked) + +Additional features that I want to implement: +- Only show widgets when in app launcher +- Make timeout adjustable +- Disable widgets completely diff --git a/apps/widviztime/changelog b/apps/widviztime/changelog new file mode 100644 index 000000000..287061d0c --- /dev/null +++ b/apps/widviztime/changelog @@ -0,0 +1 @@ +0.01: New Widget, forked from widviz diff --git a/apps/widviztime/eye.png b/apps/widviztime/eye.png new file mode 100644 index 000000000..9aec8ce89 Binary files /dev/null and b/apps/widviztime/eye.png differ diff --git a/apps/widviztime/metadata.json b/apps/widviztime/metadata.json new file mode 100644 index 000000000..b364bbd74 --- /dev/null +++ b/apps/widviztime/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widviztime", + "name": "Widget Autohide Widget", + "shortName": "Viz Time Widget", + "version": "0.01", + "description": "The widgets will be shown for four seconds after the device is unlocked.", + "icon": "eye.png", + "type": "widget", + "tags": "widget", + "readme":"README.md", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widviztime.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widviztime/widget.js b/apps/widviztime/widget.js new file mode 100644 index 000000000..5e81af611 --- /dev/null +++ b/apps/widviztime/widget.js @@ -0,0 +1,53 @@ +(() => { + + var saved = null; + + + function hide() { + if (!Bangle.isLCDOn() || saved) return; + saved = []; + for (var wd of WIDGETS) { + saved.push({ + d: wd.draw, + a: wd.area + }); + wd.draw = () => {}; + wd.area = ""; + } + g.setColor(0, 0, 0); + g.fillRect(0, 0, g.getWidth(), 23); + } + + function reveal() { + if (!Bangle.isLCDOn() || !saved) return; + for (var wd of WIDGETS) { + var o = saved.shift(); + wd.draw = o.d; + wd.area = o.a; + } + Bangle.drawWidgets(); + saved = null; + } + + function draw() { + g.setColor(0x07ff); + g.drawImage(atob("GBgBAAAAAAAAAAAAAAAAAH4AAf+AB4HgDgBwHDw4OH4cMOcMYMMGYMMGMOcMOH4cHDw4DgBwB4HgAf+AAH4AAAAAAAAAAAAAAAAA"), this.x, this.y); + } + + WIDGETS.viz = { + area: "tl", + width: 24, + draw: draw + }; + + + + Bangle.on('lock', (locked) => { + if (!locked) { + reveal(); + setTimeout(function() { + hide(); + }, 4000); + } + }); +})(); diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog index 084ca6ed5..2ca405365 100644 --- a/apps/wohrm/ChangeLog +++ b/apps/wohrm/ChangeLog @@ -5,4 +5,7 @@ 0.05: Improved buzz timing and rendering 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed 0.07: Home button fixed and README added -0.08: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) +0.08: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799) +0.09: Ported to Bangle.js2 + Home returns to clock, instead of menu + Add settings diff --git a/apps/wohrm/README.md b/apps/wohrm/README.md index ad9e82525..87b1a65da 100644 --- a/apps/wohrm/README.md +++ b/apps/wohrm/README.md @@ -8,6 +8,9 @@ and will notify you with a buzz whenever your heart rate falls below or jumps ab [Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html). ## Setting the limits + +Use the settings menu to set the limits. On the Bangle.js1 these can in addition be set with the buttons: + For setting the lower limit press button 4 (left part of the watch's touch screen). Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch. @@ -22,7 +25,7 @@ the received value: For 85% and above the bars are green, between 84% and 50% th and below 50% they turn red. ## Closing the app -Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher. +Pressing middle button will switch off the HRM of the watch and return you to the launcher. # HRM usage The HRM is switched on when the app is started. It stays switch on while the app is running, even diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index c9c060e99..ab579463c 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -1,327 +1,400 @@ -/* eslint-disable no-undef */ -const Setter = { - NONE: "none", - UPPER: 'upper', - LOWER: 'lower' -}; - -const shortBuzzTimeInMs = 80; -const longBuzzTimeInMs = 400; - -let upperLimit = 130; -let upperLimitChanged = true; - -let lowerLimit = 100; -let lowerLimitChanged = true; - -let limitSetter = Setter.NONE; - -let currentHeartRate = 0; -let hrConfidence = -1; -let hrChanged = true; -let confidenceChanged = true; - -let setterHighlightTimeout; - -function renderUpperLimitBackground() { - g.setColor(1,0,0); - g.fillRect(125,40, 210, 70); - g.fillRect(180,70, 210, 200); - - //Round top left corner - g.fillEllipse(115,40,135,70); - - //Round top right corner - g.setColor(0,0,0); - g.fillRect(205,40, 210, 45); - g.setColor(1,0,0); - g.fillEllipse(190,40,210,50); - - //Round inner corner - g.fillRect(174,71, 179, 76); - g.setColor(0,0,0); - g.fillEllipse(160,71,179,82); - - //Round bottom - g.setColor(1,0,0); - g.fillEllipse(180,190, 210, 210); -} - -function renderLowerLimitBackground() { - g.setColor(0,0,1); - g.fillRect(10, 180, 100, 210); - g.fillRect(10, 50, 40, 180); - - //Rounded top - g.setColor(0,0,1); - g.fillEllipse(10,40, 40, 60); - - //Round bottom right corner - g.setColor(0,0,1); - g.fillEllipse(90,180,110,210); - - //Round inner corner - g.setColor(0,0,1); - g.fillRect(40,175,45,180); - g.setColor(0,0,0); - g.fillEllipse(41,170,60,179); - - //Round bottom left corner - g.setColor(0,0,0); - g.fillRect(10,205, 15, 210); - g.setColor(0,0,1); - g.fillEllipse(10,200,30,210); -} - -function drawTrainingHeartRate() { - //Only redraw if the display is on - if (Bangle.isLCDOn()) { - renderUpperLimit(); - - renderCurrentHeartRate(); - - renderLowerLimit(); - - renderConfidenceBars(); - } - - buzz(); -} - -function renderUpperLimit() { - if(!upperLimitChanged) { return; } - - g.setColor(1,0,0); - g.fillRect(125,40, 210, 70); - - if(limitSetter === Setter.UPPER){ - g.setColor(255,255, 0); - } else { - g.setColor(255,255,255); - } - g.setFontVector(13); - g.drawString("Upper: " + upperLimit, 125, 50); - - upperLimitChanged = false; -} - -function renderCurrentHeartRate() { - if(!hrChanged) { return; } - - g.setColor(255,255,255); - g.fillRect(55, 110, 165, 150); - - g.setColor(0,0,0); - g.setFontVector(24); - g.setFontAlign(1, -1, 0); - g.drawString(currentHeartRate, 130, 117); - - //Reset alignment to defaults - g.setFontAlign(-1, -1, 0); - - hrChanged = false; -} - -function renderLowerLimit() { - if(!lowerLimitChanged) { return; } - - g.setColor(0,0,1); - g.fillRect(10, 180, 100, 210); - - if(limitSetter === Setter.LOWER){ - g.setColor(255,255, 0); - } else { - g.setColor(255,255,255); - } - g.setFontVector(13); - g.drawString("Lower: " + lowerLimit, 20,190); - - lowerLimitChanged = false; -} - -function renderConfidenceBars(){ - if(!confidenceChanged) { return; } - - if(hrConfidence >= 85){ - g.setColor(0, 255, 0); - } else if (hrConfidence >= 50) { - g.setColor(255, 255, 0); - } else if(hrConfidence >= 0){ - g.setColor(255, 0, 0); - } else { - g.setColor(255, 255, 255); - } - - g.fillRect(45, 110, 55, 150); - g.fillRect(165, 110, 175, 150); - - confidenceChanged = false; -} - -function renderPlusMinusIcons() { - if (limitSetter === Setter.NONE) { - g.setColor(0, 0, 0); - } else { - g.setColor(1, 1, 1); - } - - g.setFontVector(14); - - //+ for Btn1 - g.drawString("+", 222, 50); - - //- for Btn3 - g.drawString("-", 222,165); - - return; -} - -function renderHomeIcon() { - //Home for Btn2 - g.setColor(1, 1, 1); - g.drawLine(220, 118, 227, 110); - g.drawLine(227, 110, 234, 118); - - g.drawPoly([222,117,222,125,232,125,232,117], false); - g.drawRect(226,120,229,125); -} - -function buzz() { - // Do not buzz if not confident - if(hrConfidence < 85) { return; } - - if(currentHeartRate > upperLimit) - { - Bangle.buzz(shortBuzzTimeInMs); - setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); - } - - if(currentHeartRate < lowerLimit) - { - Bangle.buzz(longBuzzTimeInMs); - } -} - -function onHrm(hrm){ - if(currentHeartRate !== hrm.bpm){ - currentHeartRate = hrm.bpm; - hrChanged = true; - } - - if(hrConfidence !== hrm.confidence) { - hrConfidence = hrm.confidence; - confidenceChanged = true; - } -} - -function setLimitSetterToLower() { - resetHighlightTimeout(); - - limitSetter = Setter.LOWER; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderUpperLimit(); - renderLowerLimit(); - renderPlusMinusIcons(); -} - -function setLimitSetterToUpper() { - resetHighlightTimeout(); - - limitSetter = Setter.UPPER; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderLowerLimit(); - renderUpperLimit(); - renderPlusMinusIcons(); -} - -function setLimitSetterToNone() { - limitSetter = Setter.NONE; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderLowerLimit(); - renderUpperLimit(); - renderPlusMinusIcons(); -} - -function incrementLimit() { - resetHighlightTimeout(); - - if (limitSetter === Setter.UPPER) { - upperLimit++; - renderUpperLimit(); - upperLimitChanged = true; - } else if(limitSetter === Setter.LOWER) { - lowerLimit++; - renderLowerLimit(); - lowerLimitChanged = true; - } -} - -function decrementLimit(){ - resetHighlightTimeout(); - - if (limitSetter === Setter.UPPER) { - upperLimit--; - renderUpperLimit(); - upperLimitChanged = true; - } else if(limitSetter === Setter.LOWER) { - lowerLimit--; - renderLowerLimit(); - lowerLimitChanged = true; - } -} - -function resetHighlightTimeout() { - if (setterHighlightTimeout) { - clearTimeout(setterHighlightTimeout); - } - - setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); -} - -function switchOffApp(){ - Bangle.setHRMPower(0,"wohrm"); - Bangle.showLauncher(); -} - -Bangle.on('lcdPower', (on) => { - g.clear(); - if (on) { - Bangle.drawWidgets(); - - renderHomeIcon(); - renderLowerLimitBackground(); - renderUpperLimitBackground(); - lowerLimitChanged = true; - upperLimitChanged = true; - drawTrainingHeartRate(); - } -}); - -Bangle.setHRMPower(1,"wohrm"); -Bangle.on('HRM', onHrm); - -setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); -setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); -setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); -setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); - -setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); - -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -renderHomeIcon(); -renderLowerLimitBackground(); -renderUpperLimitBackground(); - -setInterval(drawTrainingHeartRate, 1000); +/* eslint-disable no-undef */ +const Setter = { + NONE: "none", + UPPER: 'upper', + LOWER: 'lower' +}; +const SETTINGS_FILE = "wohrm.setting.json"; +var settings = require('Storage').readJSON(SETTINGS_FILE, 1) || { + upperLimit: 130, + lowerLimit: 100 +}; + +const shortBuzzTimeInMs = 80; +const longBuzzTimeInMs = 400; + +let upperLimitChanged = true; +let lowerLimitChanged = true; + +let limitSetter = Setter.NONE; + +let currentHeartRate = 0; +let hrConfidence = -1; +let hrChanged = true; +let confidenceChanged = true; + +let setterHighlightTimeout; + +const isB1 = process.env.HWVERSION==1; +const upperLshape = isB1 ? { + right: 125, + left: 210, + bottom: 40, + top: 210, + rectWidth: 30, + cornerRoundness: 5, + orientation: -1, + color: '#f00' +} : { + right: Bangle.appRect.x2-100, + left: Bangle.appRect.x2, + bottom: 24, + top: Bangle.appRect.y2, + rectWidth: 26, + cornerRoundness: 4, + orientation: -1, // rotated 180° + color: '#f00' +}; + +const lowerLshape = { + left: isB1 ? 10 : Bangle.appRect.x, + right: 100, + bottom: upperLshape.top, + top: upperLshape.bottom, + rectWidth: upperLshape.rectWidth, + cornerRoundness: upperLshape.cornerRoundness, + orientation: 1, + color: '#00f' +}; + +const centerBar = { + minY: (upperLshape.bottom + upperLshape.top - upperLshape.rectWidth)/2, + maxY: (upperLshape.bottom + upperLshape.top + upperLshape.rectWidth)/2, + confidenceWidth: isB1 ? 10 : 8, + minX: isB1 ? 55 : upperLshape.rectWidth + 14, + maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14 +}; + +const fontSizes = isB1 ? { + limits: 13, + heartRate: 24 +} : { + limits: 12, + heartRate: 20 +}; + +function fillEllipse(x, y, x2, y2) { + g.fillEllipse(Math.min(x, x2), + Math.min(y, y2), + Math.max(x, x2), + Math.max(y, y2)); +} + +/** + * @param p.left: the X coordinate of the left side of the L in its orientation + * @param p.right: the X coordinate of the right side of the L in its orientation + * @param p.top: the Y coordinate of the top side of the L in its orientation + * @param p.bottom: the Y coordinate of the bottom side of the L in its orientation + * @param p.strokeWidth: how thick we draw the letter. + * @param p.cornerRoundness: how much the corners should be rounded + * @param p.orientation: 1 == turned 0°; -1 == turned 180° + * @param p.color: the color to draw the shape + */ +function renderLshape(p) { + g.setColor(p.color); + + g.fillRect(p.right, p.bottom, p.left, p.bottom-p.orientation*p.rectWidth); + g.fillRect(p.left+p.orientation*p.rectWidth, + p.bottom-p.orientation*p.rectWidth, + p.left, + p.top+p.orientation*p.cornerRoundness*2); + + //Round end of small line + fillEllipse(p.right+p.orientation*p.cornerRoundness*2, + p.bottom, + p.right-p.orientation*p.cornerRoundness*2, + p.bottom-p.orientation*p.rectWidth); + + //Round outer corner + g.setColor(g.theme.bg); + g.fillRect(p.left+p.orientation*p.cornerRoundness, + p.bottom, + p.left, + p.bottom-p.orientation*p.cornerRoundness); + g.setColor(p.color); + fillEllipse(p.left+p.orientation*p.cornerRoundness*4, + p.bottom, + p.left, + p.bottom-p.orientation*p.cornerRoundness*2); + + //Round inner corner + g.fillRect(p.left+p.orientation*(p.rectWidth+p.cornerRoundness+1), + p.bottom-p.orientation*(p.rectWidth+1), + p.left+p.orientation*(p.rectWidth+1), + p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness-1)); + g.setColor(g.theme.bg); + fillEllipse(p.left+p.orientation*(p.rectWidth+p.cornerRoundness*4), + p.bottom-p.orientation*(p.rectWidth+1), + p.left+p.orientation*(p.rectWidth+1), + p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness*3-1)); + + //Round end of long line + g.setColor(p.color); + fillEllipse(p.left+p.orientation*p.rectWidth, + p.top+p.orientation*p.cornerRoundness*4, + p.left, + p.top); +} + +function drawTrainingHeartRate() { + //Only redraw if the display is on + if (Bangle.isLCDOn()) { + renderUpperLimit(); + + renderCurrentHeartRate(); + + renderLowerLimit(); + + renderConfidenceBars(); + } + + buzz(); +} + +function renderUpperLimit() { + if(!upperLimitChanged) { return; } + + renderLshape(upperLshape); + + if(limitSetter === Setter.UPPER){ + g.setColor(1,1,0); + } else { + g.setColor(g.theme.fg); + } + g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0); + g.drawString("Upper: " + settings.upperLimit, + upperLshape.right, + upperLshape.bottom+upperLshape.rectWidth/2); + + upperLimitChanged = false; +} + +function renderCurrentHeartRate() { + if(!hrChanged) { return; } + + g.setColor(g.theme.fg); + g.fillRect(centerBar.minX, centerBar.minY, + centerBar.maxX, centerBar.maxY); + + g.setColor(g.theme.bg); + g.setFontVector(fontSizes.heartRate); + g.setFontAlign(1, 0, 0); + g.drawString(currentHeartRate, + Math.max(upperLshape.right+upperLshape.cornerRoundness, + lowerLshape.right-lowerLshape.cornerRoundness), + (centerBar.minY+centerBar.maxY)/2); + + //Reset alignment to defaults + g.setFontAlign(-1, -1, 0); + + hrChanged = false; +} + +function renderLowerLimit() { + if(!lowerLimitChanged) { return; } + + renderLshape(lowerLshape); + + if(limitSetter === Setter.LOWER){ + g.setColor(1,1,0); + } else { + g.setColor(g.theme.fg); + } + g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0); + g.drawString("Lower: " + settings.lowerLimit, + lowerLshape.left + lowerLshape.rectWidth/2, + lowerLshape.bottom - lowerLshape.rectWidth/2); + + lowerLimitChanged = false; +} + +function renderConfidenceBars(){ + if(!confidenceChanged) { return; } + + if(hrConfidence >= 85){ + g.setColor(0, 1, 0); + } else if (hrConfidence >= 50) { + g.setColor(1, 1, 0); + } else if(hrConfidence >= 0){ + g.setColor(1, 0, 0); + } else { + g.setColor(g.theme.fg); + } + + g.fillRect(centerBar.minX-centerBar.confidenceWidth, centerBar.minY, centerBar.minX, centerBar.maxY); + g.fillRect(centerBar.maxX, centerBar.minY, centerBar.maxX+centerBar.confidenceWidth, centerBar.maxY); + + confidenceChanged = false; +} + +function renderPlusMinusIcons() { + if (limitSetter === Setter.NONE) { + g.setColor(g.theme.bg); + } else { + g.setColor(g.theme.fg); + } + + g.setFontVector(14); + + //+ for Btn1 + g.drawString("+", 222, 50); + + //- for Btn3 + g.drawString("-", 222,165); + + return; +} + +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + +function buzz() { + // Do not buzz if not confident + if(hrConfidence < 85) { return; } + + if(currentHeartRate > settings.upperLimit) + { + Bangle.buzz(shortBuzzTimeInMs); + setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); + } + + if(currentHeartRate < settings.lowerLimit) + { + Bangle.buzz(longBuzzTimeInMs); + } +} + +function onHrm(hrm){ + if(currentHeartRate !== hrm.bpm){ + currentHeartRate = hrm.bpm; + hrChanged = true; + } + + if(hrConfidence !== hrm.confidence) { + hrConfidence = hrm.confidence; + confidenceChanged = true; + } +} + +function setLimitSetterToLower() { + resetHighlightTimeout(); + + limitSetter = Setter.LOWER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderUpperLimit(); + renderLowerLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToUpper() { + resetHighlightTimeout(); + + limitSetter = Setter.UPPER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToNone() { + limitSetter = Setter.NONE; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function incrementLimit() { + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + settings.upperLimit++; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + settings.lowerLimit++; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function decrementLimit(){ + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + settings.upperLimit--; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + settings.lowerLimit--; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function resetHighlightTimeout() { + if (setterHighlightTimeout) { + clearTimeout(setterHighlightTimeout); + } + + setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); +} + +function switchOffApp(){ + Bangle.setHRMPower(0,"wohrm"); + load(); +} + +Bangle.on('lcdPower', (on) => { + if (on) { + Bangle.drawWidgets(); + + if (typeof(BTN5) !== typeof(undefined)) { + renderHomeIcon(); + } + renderLshape(lowerLshape); + renderLshape(upperLshape); + lowerLimitChanged = true; + upperLimitChanged = true; + drawTrainingHeartRate(); + } +}); + +Bangle.setHRMPower(1,"wohrm"); +Bangle.on('HRM', onHrm); + +g.setTheme({bg:"#000",fg:"#fff",dark:true}); +g.reset(); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +if (typeof(BTN5) !== typeof(undefined)) { + renderHomeIcon(); + setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); + setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); + setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); + setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); + + setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); +} else { + setWatch(switchOffApp, BTN1, {edge:"falling", debounce:50, repeat:true}); +} + +setInterval(drawTrainingHeartRate, 1000); diff --git a/apps/wohrm/metadata.json b/apps/wohrm/metadata.json new file mode 100644 index 000000000..b2b89336c --- /dev/null +++ b/apps/wohrm/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "wohrm", + "name": "Workout HRM", + "version": "0.09", + "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", + "icon": "app.png", + "type": "app", + "tags": "hrm,workout", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}], + "storage": [ + {"name":"wohrm.app.js","url":"app.js"}, + {"name":"wohrm.settings.js","url":"settings.js"}, + {"name":"wohrm.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/wohrm/settings.js b/apps/wohrm/settings.js new file mode 100644 index 000000000..6d31688f4 --- /dev/null +++ b/apps/wohrm/settings.js @@ -0,0 +1,35 @@ +(function menu(back) { + const SETTINGS_FILE = "wohrm.setting.json"; + + // initialize with default settings... + const storage = require('Storage'); + var settings = storage.readJSON(SETTINGS_FILE, 1) || { + upperLimit: 130, + lowerLimit: 100 + }; + + function save() { + storage.write(SETTINGS_FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Workout HRM' }, + '< Back': back, + 'Upper limit': { + value: settings.upperLimit, + min: 100, max: 200, + onchange: v => { + settings.upperLimit = v; + save(); + } + }, + 'Lower limit': { + value: settings.lowerLimit, + min: 50, max: 150, + onchange: v => { + settings.lowerLimit = v; + save(); + } + } + }); +}) diff --git a/apps/worldclock/metadata.json b/apps/worldclock/metadata.json new file mode 100644 index 000000000..706831a09 --- /dev/null +++ b/apps/worldclock/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "worldclock", + "name": "World Clock - 4 time zones", + "shortName": "World Clock", + "version": "0.05", + "description": "Current time zone plus up to four others", + "icon": "app.png", + "screenshots": [{"url":"screenshot_world.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"worldclock.app.js","url":"app.js"}, + {"name":"worldclock.img","url":"worldclock-icon.js","evaluate":true} + ], + "data": [{"name":"worldclock.settings.json"}] +} diff --git a/bin/apploader.js b/bin/apploader.js index 6b4c2202d..427a0ef99 100755 --- a/bin/apploader.js +++ b/bin/apploader.js @@ -13,6 +13,7 @@ for Noble. var SETTINGS = { pretokenise : true }; +var APPSDIR = __dirname+"/../apps/"; var Utils = require("../core/js/utils.js"); var AppInfo = require("../core/js/appinfo.js"); var noble; @@ -29,18 +30,27 @@ if (!noble) { console.log(" npm install noble") } -var apps; +var apps = []; function ERROR(msg) { console.error(msg); process.exit(1); } -try { - apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json")); -} catch(e) { - ERROR("'apps.json' could not be loaded"); -} +var apps = []; +var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); +dirs.forEach(dir => { + var appsFile; + if (dir.name.startsWith("_example") || !dir.isDirectory()) + return; + try { + appsFile = require("fs").readFileSync(APPSDIR+dir.name+"/metadata.json").toString(); + } catch (e) { + ERROR(dir.name+"/metadata.json does not exist"); + return; + } + apps.push(JSON.parse(appsFile)); +}); var args = process.argv; diff --git a/bin/create_apps_json.sh b/bin/create_apps_json.sh new file mode 100755 index 000000000..dd883b22a --- /dev/null +++ b/bin/create_apps_json.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# ================================================================ +# apps.json used to contain the metadata for every app. Now the +# metadata is stored in each apps's directory - app/yourapp/metadata.js +# +# The app loader still wants all the data in one file, so normally +# with GitHub pages, Jekyll is automatically run and creates a working +# apps.json +# +# However if you're hosting locally, or not on GitHub pages then you +# will want to create apps.json yourself. You can do that by installing +# and running Jekyll, OR the easier option is just to run this script. +# +# If you do this, please do not attempt to commit your modified +# apps.json back into the main BangleApps repository! +# +# You can pass an optional filename to this script, and it will write +# to that instead, apps.local.json is used when opening the loader on localhost +outfile="${1:-apps.json}" + +cd `dirname $0`/.. +echo "[" > "$outfile" +first=1 +for app in apps/*/; do + echo "Processing $app..."; + if [[ "$app" =~ ^apps/_example.* ]]; then + echo "Ignoring $app" + else + if [ $first -eq 1 ]; then + first=0; + else + echo "," >> "$outfile" + fi; + cat ${app}metadata.json >> "$outfile" +# echo ",\"$app\"," >> apps.json # DEBUG ONLY + fi +done +echo "]" >> "$outfile" + +if [ -z "$1"]; then + # Running with no arguments: prevent accidental commit of modified apps.json. + # You can use `create_apps.json.sh apps.json` if you really want to both + # overwrite and still commit apps.json + git update-index --skip-worktree apps.json + echo "Told git to ignore modified apps.json." + # If you want to unignore it, use + # 'git update-index --no-skip-worktree apps.json' +fi \ No newline at end of file diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index 4bc2a70b2..9f7758ee5 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -10,7 +10,6 @@ var SETTINGS = { var path = require('path'); var ROOTDIR = path.join(__dirname, '..'); var APPDIR = ROOTDIR+'/apps'; -var APPJSON = ROOTDIR+'/apps.json'; var OUTFILE = ROOTDIR+'/firmware.js'; var DEVICE = "BANGLEJS"; var APPS = [ // IDs of apps to install @@ -28,7 +27,6 @@ global.Const = { }; var AppInfo = require(ROOTDIR+"/core/js/appinfo.js"); -var appjson = JSON.parse(fs.readFileSync(APPJSON).toString()); var appfiles = []; function fileGetter(url) { @@ -58,8 +56,11 @@ function fileGetter(url) { } Promise.all(APPS.map(appid => { - var app = appjson.find(app=>app.id==appid); - if (app===undefined) throw new Error(`App ${appid} not found`); + try { + var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "metadata.json").toString()); + } catch (e) { + throw new Error(`App ${appid} not found`); + } return AppInfo.getFiles(app, { fileGetter : fileGetter, settings : SETTINGS, diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 14ced9ef8..e7042d2c3 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -16,7 +16,6 @@ var DEVICE = process.argv[2]; var path = require('path'); var ROOTDIR = path.join(__dirname, '..'); var APPDIR = ROOTDIR+'/apps'; -var APPJSON = ROOTDIR+'/apps.json'; var MINIFY = true; var OUTFILE, APPS; @@ -86,7 +85,6 @@ function atob(input) { } var AppInfo = require(ROOTDIR+"/core/js/appinfo.js"); -var appjson = JSON.parse(fs.readFileSync(APPJSON).toString()); var appfiles = []; function fileGetter(url) { @@ -134,8 +132,11 @@ function evaluateFile(file) { } Promise.all(APPS.map(appid => { - var app = appjson.find(app=>app.id==appid); - if (app===undefined) throw new Error(`App ${appid} not found`); + try { + var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "metadata.json").toString()); + } catch (e) { + throw new Error(`App ${appid} not found`); + } return AppInfo.getFiles(app, { fileGetter : fileGetter, settings : SETTINGS, diff --git a/bin/language_scan.js b/bin/language_scan.js new file mode 100755 index 000000000..89af7a12b --- /dev/null +++ b/bin/language_scan.js @@ -0,0 +1,176 @@ +#!/usr/bin/nodejs +/* Scans for strings that may be in English in each app, and +outputs a list of strings that have been found. + +See https://github.com/espruino/BangleApps/issues/1311 +*/ + +var IGNORE_STRINGS = [ + "5x5","6x8","6x8:2","4x6","12x20","6x15","5x9Numeric7Seg", "Vector", // fonts + "---","...","*","##","00","GPS","ram", + "12hour","rising","falling","title", + "sortorder","tl","tr", + "function","object", // typeof=== + "txt", // layout styles + "play","stop","pause", // music state +]; + +var IGNORE_FUNCTION_PARAMS = [ + "read", + "readJSON", + "require", + "setFont","setUI","setLCDMode", + "on", + "RegExp","sendCommand", + "print","log" +]; +var IGNORE_ARRAY_ACCESS = [ + "WIDGETS" +]; + +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); +} +function log(s) { + console.log(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, wasFnCall, wasArrayAccess) { + if (s=="") return true; + // wasFnCall is set to the function name if 's' is the first argument to a function + if (wasFnCall && IGNORE_FUNCTION_PARAMS.includes(wasFnCall)) return true; + if (wasArrayAccess && IGNORE_ARRAY_ACCESS.includes(wasArrayAccess)) return true; + if (s=="Storage") console.log("isNotString",s,wasFnCall); + + 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 + if (IGNORE_STRINGS.includes(s)) return true; // one we know to ignore + return false; +} + +function getTextFromString(s) { + return s.replace(/^([.<>\-\n ]*)([^<>\!\?]*?)([.<>\!\?\-\n ]*)$/,"$2"); +} + +// A string that *could* be translated? +var untranslatedStrings = []; +// Strings that are marked with 'LANG' +var translatedStrings = []; + +function addString(list, str, file) { + str = getTextFromString(str); + var entry = list.find(e => e.str==str); + if (!entry) { + entry = { str:str, uses:0, files : [] }; + list.push(entry); + } + entry.uses++; + if (!entry.files.includes(file)) + entry.files.push(file) +} + +console.log("Scanning apps..."); +//apps = apps.filter(a=>a.id=="wid_edit"); +apps.forEach((app,appIdx) => { + var appDir = APPSDIR+app.id+"/"; + app.storage.forEach((file) => { + if (!file.url || !file.name.endsWith(".js")) return; + var filePath = appDir+file.url; + var shortFilePath = "apps/"+app.id+"/"+file.url; + var fileContents = fs.readFileSync(filePath).toString(); + var lex = Espruino.Core.Utils.getLexer(fileContents); + var lastIdx = 0; + var wasFnCall = undefined; // set to 'setFont' if we're at something like setFont(".." + var wasArrayAccess = undefined; // set to 'WIDGETS' if we're at something like WIDGETS[".." + var tok = lex.next(); + while (tok!==undefined) { + var previousString = fileContents.substring(lastIdx, tok.startIdx); + if (tok.type=="STRING") { + if (previousString.includes("/*LANG*/")) { // translated! + addString(translatedStrings, tok.value, shortFilePath); + } else { // untranslated - potential to translate? + if (!isNotString(tok.value, wasFnCall, wasArrayAccess)) { + addString(untranslatedStrings, tok.value, shortFilePath); + } + } + } else { + if (tok.value!="(") wasFnCall=undefined; + if (tok.value!="[") wasArrayAccess=undefined; + } + //console.log(wasFnCall,tok.type,tok.value); + if (tok.type=="ID") { + wasFnCall = tok.value; + wasArrayAccess = tok.value; + } + lastIdx = tok.endIdx; + tok = lex.next(); + } + }); +}); +untranslatedStrings.sort((a,b)=>a.uses - b.uses); +translatedStrings.sort((a,b)=>a.uses - b.uses); + + +var report = ""; + +log("Translated Strings that are not tagged with LANG"); +log("================================================================="); +log(""); +log("Maybe we should add /*LANG*/ to these automatically?"); +log(""); +log(untranslatedStrings.filter(e => translatedStrings.find(t=>t.str==e.str)).map(e=>`${JSON.stringify(e.str)} (${e.files.join(",")})`).join("\n")); +log(""); +//process.exit(1); +log("Possible English Strings that could be translated"); +log("================================================================="); +log(""); +log("Add these to IGNORE_STRINGS if they don't make sense..."); +log(""); + // ignore ones only used once or twice +log(untranslatedStrings.filter(e => e.uses>2).filter(e => !translatedStrings.find(t=>t.str==e.str)).map(e=>`${JSON.stringify(e.str)} (${e.uses} uses)`).join("\n")); +log(""); +//process.exit(1); + +var languages = JSON.parse(fs.readFileSync(BASEDIR+"/lang/index.json").toString()); +languages.forEach(language => { + if (language.code=="en_GB") { + console.log("Ignoring "+language.code); + return; + } + console.log("Scanning "+language.code); + log(language.code); + log("=========="); + var translations = JSON.parse(fs.readFileSync(BASEDIR+"/lang/"+language.url).toString()); + translatedStrings.forEach(str => { + if (!translations.GLOBAL[str]) + console.log(`Missing translation for ${JSON.stringify(str)}`); + }); + log(""); +}); +console.log("Done."); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index ea45dc19b..363e86922 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -3,6 +3,7 @@ */ var fs = require("fs"); +var heatshrink = require("../core/lib/heatshrink"); var acorn; try { acorn = require("acorn"); @@ -25,29 +26,35 @@ 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) { - console.log(e); - var m = e.toString().match(/in JSON at position (\d+)/); - if (m) { - var char = parseInt(m[1]); - console.log("==============================================="); - console.log("LINE "+appsFile.substr(0,char).split("\n").length); - console.log("==============================================="); - console.log(appsFile.substr(char-10, 20)); - console.log("==============================================="); +var apps = []; +var dirs = fs.readdirSync(APPSDIR, {withFileTypes: true}); +dirs.forEach(dir => { + var appsFile; + if (dir.name.startsWith("_example") || !dir.isDirectory()) + return; + try { + appsFile = fs.readFileSync(APPSDIR+dir.name+"/metadata.json").toString(); + } catch (e) { + ERROR(dir.name+"/metadata.json does not exist"); + return; } - console.log(m); - ERROR("apps.json not valid JSON"); - -} + try{ + apps.push(JSON.parse(appsFile)); + } catch (e) { + console.log(e); + var m = e.toString().match(/in JSON at position (\d+)/); + if (m) { + var char = parseInt(m[1]); + console.log("==============================================="); + console.log("LINE "+appsFile.substr(0,char).split("\n").length); + console.log("==============================================="); + console.log(appsFile.substr(char-10, 20)); + console.log("==============================================="); + } + console.log(m); + ERROR(dir.name+"/metadata.json not valid JSON"); + } +}); const APP_KEYS = [ 'id', 'name', 'shortName', 'version', 'icon', 'screenshots', 'description', 'tags', 'type', @@ -57,8 +64,10 @@ const APP_KEYS = [ ]; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; +const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports' const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; +const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"]; function globToRegex(pattern) { const ESCAPE = '.*+-?^${}()|[]\\'; @@ -74,8 +83,11 @@ function globToRegex(pattern) { const isGlob = f => /[?*]/.test(f) // All storage+data files in all apps: {app:,[file: | data:]} let allFiles = []; +let existingApps = []; apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); + if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`); + existingApps.push(app.id); //console.log(`Checking ${app.id}...`); var appDir = APPSDIR+app.id+"/"; if (!fs.existsSync(APPSDIR+app.id)) ERROR(`App ${app.id} has no directory`); @@ -85,7 +97,7 @@ apps.forEach((app,appIdx) => { if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`); else { app.supports.forEach(dev => { - if (!["BANGLEJS","BANGLEJS2"].includes(dev)) + if (!SUPPORTS_DEVICES.includes(dev)) ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`); }); } @@ -135,6 +147,13 @@ apps.forEach((app,appIdx) => { if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`) if (fileNames.includes(file.name) && !file.supports) // assume that there aren't duplicates if 'supports' is set ERROR(`App ${app.id} file ${file.name} is a duplicate`); + if (file.supports && !Array.isArray(file.supports)) + ERROR(`App ${app.id} file ${file.name} supports field must be an array`); + if (file.supports) + file.supports.forEach(dev => { + if (!SUPPORTS_DEVICES.includes(dev)) + ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`); + }); fileNames.push(file.name); allFiles.push({app: app.id, file: file.name}); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); @@ -175,6 +194,23 @@ apps.forEach((app,appIdx) => { for (const key in file) { if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`); } + // warn if JS icon is the wrong size + if (file.name == app.id+".img") { + let icon; + let match = fileContents.match(/E\.toArrayBuffer\(atob\(\"([^"]*)\"\)\)/); + if (match) icon = Buffer.from(match[1], 'base64'); + else { + match = fileContents.match(/require\(\"heatshrink\"\)\.decompress\(\s*atob\(\s*\"([^"]*)\"\s*\)\s*\)/); + if (match) icon = heatshrink.decompress(Buffer.from(match[1], 'base64')); + else ERROR(`JS icon ${file.name} does not match the pattern 'require("heatshrink").decompress(atob("..."))'`); + } + if (match) { + if (icon[0] > 48 || icon[0] < 24 || icon[1] > 48 || icon[1] < 24) { + if (GRANDFATHERED_ICONS.includes(app.id)) WARN(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`); + else ERROR(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`); + } + } + } }); let dataNames = []; (app.data||[]).forEach((data)=>{ @@ -207,10 +243,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`) @@ -249,7 +285,8 @@ while(fileA=allFiles.pop()) { if (globA.test(nameB)||globB.test(nameA)) { if (isGlob(nameA)||isGlob(nameB)) ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) - else WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + else if (fileA.app != fileB.app) + WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) } }) } diff --git a/core b/core index 50aa45f13..3093d78a5 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 50aa45f13f06cc2f40684971d4c0a68d061b1f3c +Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b diff --git a/index.html b/index.html index e22a1f9e7..7e818ee72 100644 --- a/index.html +++ b/index.html @@ -141,6 +141,11 @@ Always update time when we connect +
+   Translations (BETA - more info) +
diff --git a/lang/cs_CZ.json b/lang/cs_CZ.json new file mode 100644 index 000000000..3f4fc87f1 --- /dev/null +++ b/lang/cs_CZ.json @@ -0,0 +1,53 @@ +{ + "//":"Czech language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarm" : "Budík", + "Hour" : "Hodina", + "Hours" : "Hodiny", + "Minute" : "Minuta", + "Minutes" : "Minuty", + "Second" : "Sekunda", + "Seconds" : "Sekundy", + "Month" : "Měsíc", + "Enabled" : "Povoleno", + "Background" : "Pozadí", + "Connected" : "Připojeno", + "Settings" : "Nastavení", + "Save" : "Uložit", + "Back" : "Zpět", + "Repeat" : "Opakovat", + "Delete" : "Smazat", + "Sleep" : "Uspat", + "Alarms" : "Budíky", + "ALARM!" : "BUDÍK!", + + " (repeat)" : " (opakovat)", + "< Back" : "< Zpět", + "> Delete" : "> Smazat", + "> Save" : " > Uložit", + "ALARM " : "BUDÍK ", + + "Add Device" : "Přidat zařízení", + "App Settings" : "Nast. Aplikací", + "Apps" : "Aplikace" + + }, + "alarm": { + "//":"App-specific overrides", + "Alarm/Timer" : "Budik/Časovač", + "rpt" : "Opk.", + "New Alarm" : "Nový budík", + "New Timer" : "Nový časovač", + "Auto snooze" : "Auto odložit" + }, + "setting" : { + "Quiet Mode" : "Tichý režim" + + }, + "messages": { + "Are you sure?" : "Opravdu?" + } + + +} diff --git a/lang/de_DE.json b/lang/de_DE.json new file mode 100644 index 000000000..b4acfe675 --- /dev/null +++ b/lang/de_DE.json @@ -0,0 +1,28 @@ +{ + "//":"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!", + "Yes" : "Ja", + "No" : "Nein", + "On" : "Ein", + "Off" : "Aus", + "Ok" : "OK" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "Wdh." + } +} diff --git a/lang/en_GB.json b/lang/en_GB.json new file mode 100644 index 000000000..6bf1279d1 --- /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..a3e7ede3f --- /dev/null +++ b/lang/es_ES.json @@ -0,0 +1,28 @@ +{ + "//":"Spanish language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Alarmas", + "Hours" : "Horas", + "Minutes" : "Minutos", + "Enabled" : "Activados", + "Save" : "Ahorrar", + "Back" : "Regresa", + "Repeat" : "Repetición", + "Delete" : "Borrar", + "ALARM!" : "ALARM", + "Sleep" : "Dormir", + "Alarms" : "Alarmas", + "New Alarm" : "Nueva alarma", + "ALARM!" : "ALARM!", + "Yes" : "Si", + "No" : "No", + "On" : "Encendido", + "Off" : "Apagado", + "Ok" : "OK" + }, + "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..3d492783d --- /dev/null +++ b/lang/index.json @@ -0,0 +1,13 @@ +[ + {"code":"en_GB","name":"British English","url":"en_GB.json"}, + {"code":"cs_CZ","name":"Czech","url":"cs_CZ.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..96293cbf3 --- /dev/null +++ b/lang/it_IT.json @@ -0,0 +1,21 @@ +{ + "//":"Italian language translations", + "GLOBAL": { + "//":"Translations that apply for all apps", + "Alarms" : "Sveglie", + "Hours" : "Ore", + "Minutes" : "Minuti", + "Enabled" : "Attiva", + "New Alarm" : "Nuova sveglia", + "Save" : "Salva", + "Back" : "Indietro", + "Repeat" : "Ripeti", + "Delete" : "Cancella", + "ALARM!" : "SVEGLIA!", + "Sleep" : "Dormi" + }, + "alarm": { + "//":"App-specific overrides", + "rpt" : "ripeti" + } +} 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/loader.js b/loader.js index 680cd0f94..d8ba26269 100644 --- a/loader.js +++ b/loader.js @@ -5,13 +5,18 @@ if (window.location.host=="banglejs.com") { document.title += " [Development]"; document.getElementById("apploaderlinks").innerHTML = 'This is the development Bangle.js App Loader - you can also try the Official Version for stable apps.'; +} else if (window.location.hostname==='localhost') { + document.title += " [Local]"; + Const.APPS_JSON_FILE = "apps.local.json"; + document.getElementById("apploaderlinks").innerHTML = + 'This is your local Bangle.js App Loader - you can try the Official Version here.'; } else { document.title += " [Unofficial]"; document.getElementById("apploaderlinks").innerHTML = 'This is not the official Bangle.js App Loader - you can try the Official Version here.'; } -var RECOMMENDED_VERSION = "2v10"; +var RECOMMENDED_VERSION = "2v11"; // could check http://www.espruino.com/json/BANGLEJS.json for this // We're only interested in Bangles @@ -40,7 +45,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) { if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { - showToast(`You're using an old Bangle.js firmware (${deviceVersion}). You can update with the instructions here` ,"warning", 20000); + showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update with the instructions here` ,"warning", 20000); } @@ -164,6 +169,48 @@ window.addEventListener('load', (event) => { showToast("App Install failed, "+err,"error"); }); }); + + // Load language list + httpGet("lang/index.json").then(languagesJSON=>{ + var languages; + try { + languages = JSON.parse(languagesJSON); + } catch(e) { + console.error("lang/index.json Corrupted", e); + } + + function reloadLanguage() { + LANGUAGE = undefined; + if (SETTINGS.language) { + var language = languages.find(l=>l.code==SETTINGS.language); + if (language) { + var langURL = "lang/"+language.url; + httpGet(langURL).then(languageJSON=>{ + try { + LANGUAGE = JSON.parse(languageJSON); + console.log(`${langURL} loaded successfully`); + } catch(e) { + console.error(`${langURL} Corrupted`, e); + } + }); + } else { + console.error(`Language code ${JSON.stringify(SETTINGS.language)} not found in lang/index.json`); + } + } + } + + var selectLang = document.getElementById("settings-lang"); + console.log(languages); + languages.forEach(lang => { + selectLang.innerHTML += ``; + }); + selectLang.addEventListener("change",event=>{ + SETTINGS.language = event.target.value; + reloadLanguage(); + saveSettings(); + }); + reloadLanguage(); + }); }); function onAppJSONLoaded() { diff --git a/modules/Layout.js b/modules/Layout.js index 6dc4b6368..cb64183ea 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -34,7 +34,7 @@ layoutObject has: optional `scale` specifies if image should be scaled up or not * `"custom"` - a custom block where `render(layoutObj)` is called to render * `"h"` - Horizontal layout, `c` is an array of more `layoutObject` - * `"v"` - Veritical layout, `c` is an array of more `layoutObject` + * `"v"` - Vertical layout, `c` is an array of more `layoutObject` * A `id` field. If specified the object is added with this name to the returned `layout` object, so can be referenced as `layout.foo` * A `font` field, eg `6x8` or `30%` to use a percentage of screen height @@ -42,8 +42,8 @@ layoutObject has: and `fillx`/`filly` to be set. Not compatible with text rotation. * A `col` field, eg `#f00` for red * A `bgCol` field for background color (will automatically fill on render) -* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center -* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center +* A `halign` field to set horizontal alignment WITHIN a `v` container. `-1`=left, `1`=right, `0`=center +* A `valign` field to set vertical alignment WITHIN a `h` container. `-1`=top, `1`=bottom, `0`=center * A `pad` integer field to set pixels padding * A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space * A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space @@ -261,6 +261,7 @@ Layout.prototype.render = function (l) { x,y+4 ], bg = l.selected?g.theme.bgH:g.theme.bg2; g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly); + if (l.col) g.setColor(l.col); if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad)); else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); }, "img":function(l){ 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/package.json b/package.json index b796044c9..32c96e3ea 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,17 @@ "scripts": { "lint-apps": "eslint ./apps --ext .js", "test": "node bin/sanitycheck.js && eslint ./apps --ext .js", + "update-local-apps": "./bin/create_apps_json.sh apps.local.json", + "local": "npm-watch & npx http-server -a localhost -c-1", "start": "npx http-server -c-1" }, + "watch": { + "update-local-apps": "apps/*/metadata.json" + }, "dependencies": { "acorn": "^7.2.0" + }, + "devDpendencies": { + "npm-watch": "^0.11.0" } } diff --git a/apps/cube/cube-icon.js b/testing/cube/cube-icon.js similarity index 100% rename from apps/cube/cube-icon.js rename to testing/cube/cube-icon.js diff --git a/apps/cube/cube.js b/testing/cube/cube.js similarity index 100% rename from apps/cube/cube.js rename to testing/cube/cube.js diff --git a/apps/cube/cube.png b/testing/cube/cube.png similarity index 100% rename from apps/cube/cube.png rename to testing/cube/cube.png 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
-
-
-
- - -

-
-  
-
-   
- 
-
diff --git a/apps/nyancat/code.js b/testing/nyancat/code.js
similarity index 100%
rename from apps/nyancat/code.js
rename to testing/nyancat/code.js